fred-runtime 3.3.6__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.
Files changed (161) hide show
  1. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/PKG-INFO +1 -1
  2. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/__main__.py +0 -1
  3. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/agent_app.py +139 -60
  4. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/__init__.py +2 -0
  5. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/assembly.py +65 -13
  6. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/demo.py +28 -31
  7. fred_runtime-3.3.7/fred_runtime/capabilities/document_access/capability.py +886 -0
  8. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/errors.py +12 -0
  9. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/mcp.py +12 -2
  10. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/registry.py +61 -0
  11. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/context_aware_tool.py +6 -2
  12. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/kf_base_client.py +20 -1
  13. fred_runtime-3.3.7/fred_runtime/common/kf_document_client.py +213 -0
  14. fred_runtime-3.3.7/fred_runtime/common/kf_tag_client.py +186 -0
  15. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/graph/graph_runtime.py +164 -7
  16. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/integrations/v2_runtime/adapters.py +257 -0
  17. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/middleware/tool_observability.py +68 -0
  18. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_prompting.py +7 -1
  19. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_runtime.py +11 -7
  20. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_tool_loop.py +1 -1
  21. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/runtime_context.py +8 -1
  22. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/support/tool_loop.py +39 -3
  23. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime.egg-info/PKG-INFO +1 -1
  24. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime.egg-info/SOURCES.txt +5 -0
  25. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/pyproject.toml +1 -1
  26. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_agent_app.py +482 -16
  27. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_capability_agent_1973.py +169 -18
  28. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_capability_chat_parts_1977.py +5 -2
  29. fred_runtime-3.3.7/tests/test_capability_document_access_1906.py +1050 -0
  30. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_capability_endpoints_1974.py +119 -1
  31. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_capability_registry_1973.py +108 -0
  32. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_capability_selection_1974.py +4 -0
  33. fred_runtime-3.3.7/tests/test_capability_tool_return_convention.py +213 -0
  34. fred_runtime-3.3.7/tests/test_graph_capability_bridge.py +524 -0
  35. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_react_loop_regressions_1972.py +22 -13
  36. fred_runtime-3.3.7/tests/test_tool_loop_trim.py +105 -0
  37. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_tool_observability_middleware.py +179 -0
  38. fred_runtime-3.3.6/fred_runtime/capabilities/document_access/capability.py +0 -395
  39. fred_runtime-3.3.6/tests/test_capability_document_access_1906.py +0 -515
  40. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/README.md +0 -0
  41. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/__init__.py +0 -0
  42. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/__init__.py +0 -0
  43. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/_catalogs.py +0 -0
  44. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/config.py +0 -0
  45. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/config_loader.py +0 -0
  46. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/container.py +0 -0
  47. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/context.py +0 -0
  48. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/dependencies.py +0 -0
  49. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/mcp_config.py +0 -0
  50. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/observability_factory.py +0 -0
  51. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/app/openai_compat_router.py +0 -0
  52. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/assets.py +0 -0
  53. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
  54. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
  55. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/document_access/__init__.py +0 -0
  56. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/capabilities/openapi_dump.py +0 -0
  57. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/cli/__init__.py +0 -0
  58. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/cli/completion.py +0 -0
  59. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/cli/entrypoint.py +0 -0
  60. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/cli/history_display.py +0 -0
  61. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/cli/kpi_display.py +0 -0
  62. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/cli/pod_client.py +0 -0
  63. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/cli/repl.py +0 -0
  64. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/cli/repl_helpers.py +0 -0
  65. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/cli/url_helpers.py +0 -0
  66. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/client.py +0 -0
  67. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/__init__.py +0 -0
  68. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/kf_fast_text_client.py +0 -0
  69. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/kf_http_client.py +0 -0
  70. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  71. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  72. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/kf_workspace_client.py +0 -0
  73. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/mcp_interceptors.py +0 -0
  74. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/mcp_runtime.py +0 -0
  75. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/mcp_toolkit.py +0 -0
  76. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/mcp_utils.py +0 -0
  77. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/structures.py +0 -0
  78. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/token_expiry.py +0 -0
  79. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/common/tool_node_utils.py +0 -0
  80. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/deep/__init__.py +0 -0
  81. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/deep/deep_runtime.py +0 -0
  82. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/eval/__init__.py +0 -0
  83. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/eval/collector.py +0 -0
  84. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/graph/__init__.py +0 -0
  85. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/integrations/__init__.py +0 -0
  86. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
  87. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
  88. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
  89. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  90. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/migrations.py +0 -0
  91. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/model_routing/__init__.py +0 -0
  92. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/model_routing/catalog.py +0 -0
  93. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/model_routing/contracts.py +0 -0
  94. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/model_routing/provider.py +0 -0
  95. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/model_routing/resolver.py +0 -0
  96. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/__init__.py +0 -0
  97. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/middleware/__init__.py +0 -0
  98. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/middleware/checkpoint_hygiene.py +0 -0
  99. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
  100. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/middleware/frame.py +0 -0
  101. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/middleware/hitl.py +0 -0
  102. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/middleware/model_routing.py +0 -0
  103. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/middleware/shared.py +0 -0
  104. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
  105. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_langchain_adapter.py +0 -0
  106. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_message_codec.py +0 -0
  107. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_model_adapter.py +0 -0
  108. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_stream_adapter.py +0 -0
  109. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_tool_binding.py +0 -0
  110. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_tool_rendering.py +0 -0
  111. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_tool_resolution.py +0 -0
  112. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_tool_utils.py +0 -0
  113. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/react/react_tracing.py +0 -0
  114. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/runtime_support/__init__.py +0 -0
  115. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/runtime_support/checkpoints.py +0 -0
  116. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/runtime_support/model_metadata.py +0 -0
  117. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  118. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
  119. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  120. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/support/__init__.py +0 -0
  121. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/support/filesystem_context.py +0 -0
  122. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime/support/thinking.py +0 -0
  123. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime.egg-info/dependency_links.txt +0 -0
  124. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime.egg-info/entry_points.txt +0 -0
  125. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime.egg-info/requires.txt +0 -0
  126. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/fred_runtime.egg-info/top_level.txt +0 -0
  127. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/setup.cfg +0 -0
  128. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_capability_chat_controls_1976.py +0 -0
  129. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_capability_routes_tables_1979.py +0 -0
  130. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_client.py +0 -0
  131. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_config_loader.py +0 -0
  132. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_context.py +0 -0
  133. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_context_aware_tool.py +0 -0
  134. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_conversational_memory.py +0 -0
  135. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_deep_agent_middleware.py +0 -0
  136. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_eval_collector.py +0 -0
  137. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_eval_trace.py +0 -0
  138. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_fred_workspace_fs.py +0 -0
  139. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_graph_runtime_invoke_agent.py +0 -0
  140. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_graph_runtime_observability.py +0 -0
  141. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_history.py +0 -0
  142. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_kf_vector_search_toolkit.py +0 -0
  143. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_kf_workspace_client.py +0 -0
  144. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
  145. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_kpi_display.py +0 -0
  146. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_mcp_config.py +0 -0
  147. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_model_routing.py +0 -0
  148. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_openai_compat_router.py +0 -0
  149. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_pod_client.py +0 -0
  150. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_react_middleware_frame.py +0 -0
  151. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_react_prompting.py +0 -0
  152. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_react_thinking.py +0 -0
  153. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_repl_helpers.py +0 -0
  154. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_runtime_context_prompt_injection.py +0 -0
  155. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_smoke.py +0 -0
  156. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_spike_capability_state_1971.py +0 -0
  157. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_sql_checkpointer_owner.py +0 -0
  158. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_token_expiry.py +0 -0
  159. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_tracing_kpi_content_confinement.py +0 -0
  160. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_url_helpers.py +0 -0
  161. {fred_runtime-3.3.6 → fred_runtime-3.3.7}/tests/test_user_token_refresher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-runtime
3
- Version: 3.3.6
3
+ Version: 3.3.7
4
4
  Summary: Runtime adapters and infrastructure wiring for Fred v2 agents.
5
5
  Author-email: Thales <noreply@thalesgroup.com>
6
6
  License: Apache-2.0
@@ -77,7 +77,6 @@ def main(argv: list[str] | None = None) -> int:
77
77
  if args.command == "dump-openapi":
78
78
  return _cmd_dump_openapi(args)
79
79
  parser.error(f"unknown command: {args.command}")
80
- return 2
81
80
 
82
81
 
83
82
  if __name__ == "__main__":
@@ -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 intrinsic ownership, not a stored
1264
- OpenFGA tuple: a human caller acting on their own canonical
1265
- `personal_team_id(authenticated_user.uid)` is authorized without an OpenFGA
1266
- call; any other `personal-*` id (another user's space, or the bare
1267
- `"personal"` alias) is explicitly denied never routed to OpenFGA (AUTHZ-05
1268
- item 8b watch item).
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
- if team_id == "personal" or is_personal_team_id(team_id):
1345
- # Personal spaces are intrinsic ownership (AUTHZ-05 item 8b): the id is
1346
- # derived from the JWT subject via `personal_team_id`, is never persisted
1347
- # as an OpenFGA tuple, and must never be resolved by OpenFGA. Only an
1348
- # exact match against the caller's own canonical id is authorized; any
1349
- # other personal-* id (another user's space) or the bare "personal"
1350
- # alias (ambiguous once ReBAC is active) is denied here, before OpenFGA
1351
- # ever sees it.
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`, bound to the
2174
- saving user's identity/team, but none of the execution-only services
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. Non-ReAct templates carry no capabilities.
2227
- When the registry is absent the raw selection is returned unfiltered — the
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. The effective selection mirrors
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-configured agent would
2329
- silently lose its non-negotiable grounding contract.
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
- available_capabilities = (
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
- available_capabilities=available_capabilities,
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
- stack = list(capability.middleware(ctx))
335
- middleware.extend(stack)
336
-
337
- tools_by_name: dict[str, Any] = {}
338
- for mw in stack:
339
- for candidate in getattr(mw, "tools", None) or []:
340
- candidate_name = getattr(candidate, "name", None)
341
- if isinstance(candidate_name, str):
342
- tools_by_name[candidate_name] = candidate
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
- owner = hitl.get(spec.tool)
346
- if owner is not None:
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(middleware=tuple(middleware), hitl=hitl)
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 middleware(
186
+ def tools(
215
187
  self, ctx: CapabilityContext[DemoEchoConfig, EmptyModel]
216
- ) -> list[AgentMiddleware]:
217
- return [_DemoEchoMiddleware(ctx)]
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]