fred-runtime 2.0.2__tar.gz → 2.0.4__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 (107) hide show
  1. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/PKG-INFO +1 -1
  2. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/agent_app.py +105 -9
  3. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/cli/completion.py +19 -0
  4. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/cli/history_display.py +3 -0
  5. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/cli/pod_client.py +18 -0
  6. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/cli/repl.py +97 -1
  7. fred_runtime-2.0.4/fred_runtime/cli/repl_helpers.py +401 -0
  8. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/graph/graph_runtime.py +22 -10
  9. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_message_codec.py +3 -4
  10. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_model_adapter.py +2 -3
  11. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_prompting.py +12 -4
  12. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_runtime.py +47 -22
  13. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_tool_binding.py +1 -2
  14. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_tool_resolution.py +2 -3
  15. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime.egg-info/PKG-INFO +1 -1
  16. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime.egg-info/SOURCES.txt +6 -0
  17. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/pyproject.toml +13 -1
  18. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_agent_app.py +2 -0
  19. fred_runtime-2.0.4/tests/test_conversational_memory.py +359 -0
  20. fred_runtime-2.0.4/tests/test_eval_collector.py +242 -0
  21. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_eval_trace.py +1 -2
  22. fred_runtime-2.0.4/tests/test_model_routing.py +572 -0
  23. fred_runtime-2.0.4/tests/test_pod_client.py +334 -0
  24. fred_runtime-2.0.4/tests/test_repl_helpers.py +123 -0
  25. fred_runtime-2.0.4/tests/test_token_expiry.py +218 -0
  26. fred_runtime-2.0.2/fred_runtime/cli/repl_helpers.py +0 -188
  27. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/README.md +0 -0
  28. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/__init__.py +0 -0
  29. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/__init__.py +0 -0
  30. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/_catalogs.py +0 -0
  31. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/config.py +0 -0
  32. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/config_loader.py +0 -0
  33. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/container.py +0 -0
  34. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/context.py +0 -0
  35. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/dependencies.py +0 -0
  36. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/mcp_config.py +0 -0
  37. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/observability_factory.py +0 -0
  38. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/app/openai_compat_router.py +0 -0
  39. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/cli/__init__.py +0 -0
  40. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/cli/entrypoint.py +0 -0
  41. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/cli/kpi_display.py +0 -0
  42. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/cli/url_helpers.py +0 -0
  43. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/client.py +0 -0
  44. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/__init__.py +0 -0
  45. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/context_aware_tool.py +0 -0
  46. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/kf_base_client.py +0 -0
  47. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/kf_fast_text_client.py +0 -0
  48. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/kf_http_client.py +0 -0
  49. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/kf_logs_client.py +0 -0
  50. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  51. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  52. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/kf_workspace_client.py +0 -0
  53. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/mcp_interceptors.py +0 -0
  54. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/mcp_runtime.py +0 -0
  55. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/mcp_toolkit.py +0 -0
  56. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/mcp_utils.py +0 -0
  57. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/structures.py +0 -0
  58. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/token_expiry.py +0 -0
  59. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/common/tool_node_utils.py +0 -0
  60. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/deep/__init__.py +0 -0
  61. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/deep/deep_runtime.py +3 -3
  62. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/eval/__init__.py +0 -0
  63. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/eval/collector.py +0 -0
  64. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/graph/__init__.py +0 -0
  65. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/integrations/__init__.py +0 -0
  66. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  67. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/integrations/v2_runtime/adapters.py +0 -0
  68. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/model_routing/__init__.py +0 -0
  69. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/model_routing/catalog.py +0 -0
  70. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/model_routing/contracts.py +0 -0
  71. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/model_routing/provider.py +0 -0
  72. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/model_routing/resolver.py +0 -0
  73. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/__init__.py +0 -0
  74. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_langchain_adapter.py +0 -0
  75. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_stream_adapter.py +2 -2
  76. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_tool_loop.py +5 -5
  77. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_tool_rendering.py +0 -0
  78. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_tool_utils.py +0 -0
  79. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/react/react_tracing.py +0 -0
  80. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/runtime_context.py +0 -0
  81. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/runtime_support/__init__.py +0 -0
  82. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/runtime_support/checkpoints.py +0 -0
  83. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/runtime_support/model_metadata.py +0 -0
  84. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  85. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
  86. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  87. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/support/__init__.py +0 -0
  88. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/support/filesystem_context.py +0 -0
  89. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/support/tool_approval.py +0 -0
  90. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime/support/tool_loop.py +0 -0
  91. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime.egg-info/dependency_links.txt +0 -0
  92. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime.egg-info/entry_points.txt +0 -0
  93. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime.egg-info/requires.txt +0 -0
  94. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/fred_runtime.egg-info/top_level.txt +0 -0
  95. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/setup.cfg +0 -0
  96. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_client.py +0 -0
  97. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_config_loader.py +0 -0
  98. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_context.py +0 -0
  99. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_graph_runtime_observability.py +1 -1
  100. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_history.py +0 -0
  101. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_kf_workspace_client.py +0 -0
  102. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_kpi_display.py +0 -0
  103. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_mcp_config.py +0 -0
  104. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_openai_compat_router.py +0 -0
  105. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_smoke.py +0 -0
  106. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_url_helpers.py +0 -0
  107. {fred_runtime-2.0.2 → fred_runtime-2.0.4}/tests/test_user_token_refresher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-runtime
3
- Version: 2.0.2
3
+ Version: 2.0.4
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
@@ -62,15 +62,16 @@ from fred_core.logs.log_setup import log_setup
62
62
  from fred_core.logs.memory_log_store import RamLogStore
63
63
  from fred_core.security.oidc import get_keycloak_client_id, get_keycloak_url
64
64
  from fred_core.security.structure import KeycloakUser
65
- from fred_sdk.contracts.eval import EvalStep, EvalTrace
66
65
  from fred_sdk.contracts.context import (
67
66
  AgentInvocationRequest,
68
67
  AgentInvocationResult,
69
68
  BoundRuntimeContext,
69
+ ConversationTurn,
70
70
  PortableContext,
71
71
  PortableEnvironment,
72
72
  RuntimeContext,
73
73
  )
74
+ from fred_sdk.contracts.eval import EvalStep, EvalTrace
74
75
  from fred_sdk.contracts.execution import (
75
76
  ExecutionGrantAction,
76
77
  ExecutionGrantViolation,
@@ -83,6 +84,7 @@ from fred_sdk.contracts.models import (
83
84
  GraphAgentDefinition,
84
85
  MCPServerConfiguration,
85
86
  ReActAgentDefinition,
87
+ TuningValue,
86
88
  )
87
89
  from fred_sdk.contracts.react_contract import ReActInput, ReActMessage, ReActMessageRole
88
90
  from fred_sdk.contracts.runtime import (
@@ -94,9 +96,6 @@ from fred_sdk.contracts.runtime import (
94
96
  RuntimeEvent,
95
97
  RuntimeServices,
96
98
  )
97
- from fred_runtime.graph.graph_runtime import GraphRuntime
98
- from fred_runtime.react.react_runtime import ReActRuntime
99
- from fred_runtime.runtime_support.checkpoints import load_checkpoint
100
99
  from fred_sdk.support.authored_toolsets import (
101
100
  AuthoredToolRuntimePorts,
102
101
  build_authored_tool_handlers,
@@ -104,6 +103,9 @@ from fred_sdk.support.authored_toolsets import (
104
103
  from pydantic import BaseModel, Field, TypeAdapter, model_validator
105
104
 
106
105
  from fred_runtime.common.kf_markdown_media_client import KfMarkdownMediaClient
106
+ from fred_runtime.graph.graph_runtime import GraphRuntime
107
+ from fred_runtime.react.react_runtime import ReActRuntime
108
+ from fred_runtime.runtime_support.checkpoints import load_checkpoint
107
109
 
108
110
  from ..common.structures import AgentSettingsLike
109
111
  from ..integrations.v2_runtime.adapters import (
@@ -567,6 +569,7 @@ class LocalRegistryAgentInvoker(AgentInvokerPort):
567
569
  message=request.message,
568
570
  context=context_dict,
569
571
  resume_payload=None,
572
+ invocation_turns=request.prior_turns,
570
573
  )
571
574
 
572
575
  content_parts: list[str] = []
@@ -748,6 +751,14 @@ class _AgentExecuteRequest(BaseModel):
748
751
  "LangGraph Command(resume=...) — the message field is ignored."
749
752
  ),
750
753
  )
754
+ invocation_turns: tuple[ConversationTurn, ...] = Field(
755
+ default=(),
756
+ description="Prior conversation turns forwarded by the calling agent.",
757
+ )
758
+ inline_tuning: dict[str, TuningValue] | None = Field(
759
+ default=None,
760
+ description="Optional inline tuning overrides. Honored only in agent_id (direct template) mode.",
761
+ )
751
762
 
752
763
  @model_validator(mode="after")
753
764
  def _require_message_or_resume(self) -> "_AgentExecuteRequest":
@@ -794,6 +805,8 @@ def _to_internal_request(r: RuntimeExecuteRequest) -> "_AgentExecuteRequest":
794
805
  context=r.to_legacy_context() or None,
795
806
  checkpoint_id=r.checkpoint_id,
796
807
  resume_payload=r.resume_payload,
808
+ invocation_turns=r.invocation_turns,
809
+ inline_tuning=r.inline_tuning,
797
810
  )
798
811
 
799
812
 
@@ -806,6 +819,18 @@ class _AgentTemplateSummary(BaseModel):
806
819
  available_mcp_servers: list[MCPServerConfiguration] = Field(default_factory=list)
807
820
 
808
821
 
822
+ class _McpCatalogEntry(BaseModel):
823
+ id: str
824
+ name: str
825
+ description: str | None = None
826
+ enabled: bool
827
+ transport: str | None = None
828
+
829
+
830
+ class _McpCatalogResponse(BaseModel):
831
+ servers: list[_McpCatalogEntry]
832
+
833
+
809
834
  class _ResolvedAgentInstance(BaseModel):
810
835
  agent_instance_id: str
811
836
  template_agent_id: str
@@ -827,7 +852,7 @@ def _apply_runtime_tuning(
827
852
  definition: ReActAgentDefinition | GraphAgentDefinition, tuning: AgentTuning
828
853
  ) -> ReActAgentDefinition | GraphAgentDefinition:
829
854
  """
830
- Overlay persisted business tuning onto one registered ReAct template.
855
+ Overlay persisted business tuning onto one registered agent template.
831
856
 
832
857
  Why this exists:
833
858
  - control-plane stores the full effective tuning for a managed agent
@@ -841,18 +866,29 @@ def _apply_runtime_tuning(
841
866
  - `definition = _apply_runtime_tuning(template_definition, resolution.tuning)`
842
867
  """
843
868
 
869
+ mcp_servers = tuning.mcp_servers
870
+ if tuning.selected_mcp_server_ids:
871
+ selected = frozenset(tuning.selected_mcp_server_ids)
872
+ mcp_servers = [s for s in mcp_servers if s.id in selected]
873
+
844
874
  update: dict[str, object] = {
845
875
  "role": tuning.role,
846
876
  "description": tuning.description,
847
877
  "tags": tuple(tuning.tags),
848
878
  "fields": tuple(field.model_copy(deep=True) for field in tuning.fields),
849
879
  "default_mcp_servers": tuple(
850
- server.model_copy(deep=True) for server in tuning.mcp_servers
880
+ server.model_copy(deep=True) for server in mcp_servers
851
881
  ),
882
+ # Forward all values for all agent types so every execution surface can
883
+ # read admin-set tuning (graph steps via context.tuning_values, ReAct
884
+ # prompting via definition.tuning_values).
885
+ "tuning_values": dict(tuning.values),
852
886
  }
853
- system_prompt = tuning.values.get("prompts.system")
854
- if isinstance(system_prompt, str) and system_prompt.strip():
855
- update["system_prompt_template"] = system_prompt
887
+ if isinstance(definition, ReActAgentDefinition):
888
+ # Also overlay system_prompt_template directly for ReAct runtime compatibility.
889
+ system_prompt = tuning.values.get("prompts.system")
890
+ if isinstance(system_prompt, str) and system_prompt.strip():
891
+ update["system_prompt_template"] = system_prompt
856
892
  return definition.model_copy(update=update)
857
893
 
858
894
 
@@ -915,6 +951,18 @@ async def _resolve_agent_instance(
915
951
  detail=f"Unknown agent_id: {request.agent_id!r}. "
916
952
  f"Known agents: {list(registry.keys())}",
917
953
  )
954
+ if request.inline_tuning:
955
+ definition = _apply_runtime_tuning(
956
+ definition,
957
+ AgentTuning(
958
+ role=definition.role,
959
+ description=definition.description,
960
+ tags=list(definition.tags),
961
+ fields=list(definition.fields),
962
+ mcp_servers=list(definition.default_mcp_servers),
963
+ values=request.inline_tuning,
964
+ ),
965
+ )
918
966
  return _ResolvedExecutionTarget(
919
967
  definition=definition,
920
968
  effective_agent_id=definition.agent_id,
@@ -1745,11 +1793,24 @@ async def _iterate_runtime_event_payloads(
1745
1793
  user_groups=ctx.get("user_groups"),
1746
1794
  language=ctx.get("language"),
1747
1795
  access_token=access_token,
1796
+ refresh_token=ctx.get("refresh_token"),
1797
+ access_token_expires_at=ctx.get("access_token_expires_at"),
1748
1798
  trace_id=ctx.get("trace_id"),
1749
1799
  correlation_id=correlation_id,
1750
1800
  agent_instance_id=request.agent_instance_id,
1751
1801
  template_agent_id=definition.agent_id,
1752
1802
  execution_action=execution_action,
1803
+ # Chat options forwarded from the frontend RuntimeContext.
1804
+ # These were present in ctx but were silently dropped, causing
1805
+ # ContextAwareTool and all KF search helpers to always use defaults.
1806
+ selected_document_libraries_ids=ctx.get("selected_document_libraries_ids"),
1807
+ selected_document_uids=ctx.get("selected_document_uids"),
1808
+ selected_chat_context_ids=ctx.get("selected_chat_context_ids"),
1809
+ search_policy=ctx.get("search_policy"),
1810
+ search_rag_scope=ctx.get("search_rag_scope"),
1811
+ include_session_scope=ctx.get("include_session_scope"),
1812
+ include_corpus_scope=ctx.get("include_corpus_scope"),
1813
+ deep_search=ctx.get("deep_search"),
1753
1814
  )
1754
1815
 
1755
1816
  binding = BoundRuntimeContext(
@@ -1783,6 +1844,7 @@ async def _iterate_runtime_event_payloads(
1783
1844
  session_id=ctx.get("session_id") or request_id,
1784
1845
  checkpoint_id=request.checkpoint_id,
1785
1846
  resume_payload=request.resume_payload,
1847
+ invocation_turns=getattr(request, "invocation_turns", ()),
1786
1848
  )
1787
1849
 
1788
1850
  try:
@@ -1972,6 +2034,40 @@ def _build_agent_router(
1972
2034
  for definition in registry.values()
1973
2035
  ]
1974
2036
 
2037
+ @router.get("/mcp-catalog")
2038
+ async def get_mcp_catalog() -> _McpCatalogResponse:
2039
+ """
2040
+ Return the full MCP server catalog declared in mcp_catalog.yaml.
2041
+
2042
+ Why this endpoint exists:
2043
+ - control-plane drift detection needs to compare stored instance
2044
+ selections against the live pod catalog at listing time
2045
+ - returns ALL servers (enabled and disabled) so the caller can
2046
+ distinguish "configured but disabled" from "absent from catalog"
2047
+
2048
+ How to use it:
2049
+ - call from control-plane agent-instance listing to populate
2050
+ catalog_warnings when stored mcp_server_ids are no longer present
2051
+
2052
+ Example:
2053
+ - `GET /fred/agents/v2/agents/mcp-catalog`
2054
+ """
2055
+ mcp_configuration = get_runtime_context().config.mcp_configuration
2056
+ if mcp_configuration is None:
2057
+ return _McpCatalogResponse(servers=[])
2058
+ return _McpCatalogResponse(
2059
+ servers=[
2060
+ _McpCatalogEntry(
2061
+ id=srv.id,
2062
+ name=srv.name,
2063
+ description=srv.description,
2064
+ enabled=srv.enabled,
2065
+ transport=srv.transport,
2066
+ )
2067
+ for srv in mcp_configuration.servers
2068
+ ]
2069
+ )
2070
+
1975
2071
  @router.get("/sessions", dependencies=_auth_deps)
1976
2072
  async def list_sessions(user_id: str) -> list[str]:
1977
2073
  """
@@ -14,6 +14,7 @@ _COMMANDS: tuple[str, ...] = (
14
14
  "/context",
15
15
  "/delete-session",
16
16
  "/delete-checkpoint",
17
+ "/inspect",
17
18
  "/purge-session",
18
19
  "/execution-context",
19
20
  "/history",
@@ -21,17 +22,32 @@ _COMMANDS: tuple[str, ...] = (
21
22
  "/login",
22
23
  "/login-password",
23
24
  "/mode",
25
+ "/run",
24
26
  "/session",
25
27
  "/session-info",
26
28
  "/session-new",
27
29
  "/sessions",
28
30
  "/stats",
29
31
  "/team",
32
+ "/tune",
33
+ "/tuning",
30
34
  "/logout",
31
35
  "/quit",
32
36
  "/whoami",
33
37
  )
34
38
 
39
+ # Scenario keywords for fred.test.assistant — used for /run tab-completion.
40
+ _TEST_ASSISTANT_SCENARIOS: tuple[str, ...] = (
41
+ "echo",
42
+ "error",
43
+ "hitl choice",
44
+ "hitl text",
45
+ "long",
46
+ "model planning",
47
+ "model routing",
48
+ "trace",
49
+ )
50
+
35
51
 
36
52
  def completion_candidates(
37
53
  line_buffer: str,
@@ -50,6 +66,9 @@ def completion_candidates(
50
66
  if stripped.startswith("/mode "):
51
67
  prefix = stripped.removeprefix("/mode ").strip()
52
68
  return [mode for mode in ("eval", "final", "stream") if mode.startswith(prefix)]
69
+ if stripped.startswith("/run "):
70
+ prefix = stripped.removeprefix("/run ").strip()
71
+ return [s for s in _TEST_ASSISTANT_SCENARIOS if s.startswith(prefix)]
53
72
  if stripped.startswith("/"):
54
73
  return complete_slash_commands(stripped, commands=_COMMANDS)
55
74
  return []
@@ -312,6 +312,7 @@ def run_single_turn(
312
312
  stream: bool,
313
313
  color_enabled: bool,
314
314
  resume_payload: Any = None,
315
+ inline_tuning: dict[str, Any] | None = None,
315
316
  ) -> tuple[int, dict[str, Any] | None]:
316
317
  """
317
318
  Execute one prompt and print the most useful runtime output.
@@ -327,6 +328,7 @@ def run_single_turn(
327
328
  user_id=user_id,
328
329
  team_id=team_id,
329
330
  resume_payload=resume_payload,
331
+ inline_tuning=inline_tuning,
330
332
  )
331
333
  if "error" in payload:
332
334
  print(
@@ -362,6 +364,7 @@ def run_single_turn(
362
364
  user_id=user_id,
363
365
  team_id=team_id,
364
366
  resume_payload=resume_payload,
367
+ inline_tuning=inline_tuning,
365
368
  ):
366
369
  if verbose:
367
370
  print(json.dumps(event, ensure_ascii=False))
@@ -58,6 +58,16 @@ class AgentPodClient:
58
58
  raise RuntimeError("Agent list response must be a JSON array of strings.")
59
59
  return payload
60
60
 
61
+ def list_templates(self) -> list[dict[str, Any]]:
62
+ response = self.http_client.get(
63
+ f"{self.base_url}/agents/templates", headers=self._auth_headers()
64
+ )
65
+ response.raise_for_status()
66
+ payload = response.json()
67
+ if not isinstance(payload, list):
68
+ raise RuntimeError("Templates response must be a JSON array.")
69
+ return payload
70
+
61
71
  def execute(
62
72
  self,
63
73
  *,
@@ -69,6 +79,7 @@ class AgentPodClient:
69
79
  agent_instance_id: str | None = None,
70
80
  checkpoint_id: str | None = None,
71
81
  resume_payload: Any = None,
82
+ inline_tuning: dict[str, Any] | None = None,
72
83
  ) -> dict[str, Any]:
73
84
  runtime_context: dict[str, Any] = {"user_id": user_id}
74
85
  if team_id:
@@ -85,6 +96,8 @@ class AgentPodClient:
85
96
  payload["checkpoint_id"] = checkpoint_id
86
97
  if resume_payload is not None:
87
98
  payload["resume_payload"] = resume_payload
99
+ if inline_tuning:
100
+ payload["inline_tuning"] = inline_tuning
88
101
  response = self.http_client.post(
89
102
  f"{self.base_url}/agents/execute",
90
103
  json=payload,
@@ -142,6 +155,7 @@ class AgentPodClient:
142
155
  agent_instance_id: str | None = None,
143
156
  checkpoint_id: str | None = None,
144
157
  resume_payload: Any = None,
158
+ inline_tuning: dict[str, Any] | None = None,
145
159
  ) -> list[dict[str, Any]]:
146
160
  events: list[dict[str, Any]] = []
147
161
  for event in self.iter_stream_events(
@@ -153,6 +167,7 @@ class AgentPodClient:
153
167
  agent_instance_id=agent_instance_id,
154
168
  checkpoint_id=checkpoint_id,
155
169
  resume_payload=resume_payload,
170
+ inline_tuning=inline_tuning,
156
171
  ):
157
172
  events.append(event)
158
173
  return events
@@ -168,6 +183,7 @@ class AgentPodClient:
168
183
  agent_instance_id: str | None = None,
169
184
  checkpoint_id: str | None = None,
170
185
  resume_payload: Any = None,
186
+ inline_tuning: dict[str, Any] | None = None,
171
187
  ) -> Iterator[dict[str, Any]]:
172
188
  runtime_context: dict[str, Any] = {"user_id": user_id}
173
189
  if team_id:
@@ -184,6 +200,8 @@ class AgentPodClient:
184
200
  payload["checkpoint_id"] = checkpoint_id
185
201
  if resume_payload is not None:
186
202
  payload["resume_payload"] = resume_payload
203
+ if inline_tuning:
204
+ payload["inline_tuning"] = inline_tuning
187
205
  with self.http_client.stream(
188
206
  "POST",
189
207
  f"{self.base_url}/agents/execute/stream",
@@ -33,7 +33,10 @@ from .repl_helpers import (
33
33
  execution_mode_label,
34
34
  fmt_bytes,
35
35
  parse_mode_command,
36
+ parse_tuning_value,
36
37
  print_help,
38
+ print_inspect,
39
+ print_tuning_table,
37
40
  )
38
41
 
39
42
 
@@ -109,11 +112,21 @@ def run_interactive_chat(
109
112
  current_session_id = session_id
110
113
  current_mode: ExecutionMode = mode
111
114
  current_team_id = team_id
115
+ current_inline_tuning: dict[str, Any] = {}
112
116
  while True:
113
117
  try:
118
+ tuning_badge = (
119
+ colorize(
120
+ f" ~{len(current_inline_tuning)}",
121
+ color=ANSI_YELLOW,
122
+ enabled=color_enabled,
123
+ )
124
+ if current_inline_tuning
125
+ else ""
126
+ )
114
127
  prompt = (
115
128
  f"{colorize(current_agent, color=ANSI_CYAN, enabled=color_enabled, bold=True)}"
116
- "> "
129
+ f"{tuning_badge}> "
117
130
  )
118
131
  message = input(prompt).strip()
119
132
  except EOFError:
@@ -1161,6 +1174,87 @@ def run_interactive_chat(
1161
1174
  if message in {"/quit", "/exit"}:
1162
1175
  return 0
1163
1176
 
1177
+ # ── /inspect ───────────────────────────────────────────────────────
1178
+ if message == "/inspect":
1179
+ try:
1180
+ templates = client.list_templates()
1181
+ except Exception as exc:
1182
+ print(
1183
+ colorize(
1184
+ f" Could not load templates: {exc}",
1185
+ color=ANSI_RED,
1186
+ enabled=color_enabled,
1187
+ )
1188
+ )
1189
+ continue
1190
+ print_inspect(templates, current_agent, color_enabled=color_enabled)
1191
+ continue
1192
+
1193
+ # ── /run <scenario> ────────────────────────────────────────────────
1194
+ if message.startswith("/run"):
1195
+ scenario = message.removeprefix("/run").strip()
1196
+ if not scenario:
1197
+ print(
1198
+ colorize(
1199
+ " Usage: /run <scenario> (tab-complete for available scenarios)",
1200
+ color=ANSI_DIM,
1201
+ enabled=color_enabled,
1202
+ )
1203
+ )
1204
+ continue
1205
+ message = scenario
1206
+
1207
+ # ── /tuning / /tune ────────────────────────────────────────────────
1208
+ if message == "/tuning":
1209
+ print_tuning_table(current_inline_tuning, color_enabled=color_enabled)
1210
+ continue
1211
+ if message.startswith("/tune"):
1212
+ arg = message.removeprefix("/tune").strip()
1213
+ if not arg:
1214
+ print_tuning_table(current_inline_tuning, color_enabled=color_enabled)
1215
+ continue
1216
+ if "=" not in arg:
1217
+ print(
1218
+ colorize(
1219
+ " Usage: /tune key=value (clear with key=)",
1220
+ color=ANSI_DIM,
1221
+ enabled=color_enabled,
1222
+ )
1223
+ )
1224
+ continue
1225
+ key, _, raw_val = arg.partition("=")
1226
+ key = key.strip()
1227
+ if not key:
1228
+ print(
1229
+ colorize(
1230
+ " Key cannot be empty.",
1231
+ color=ANSI_YELLOW,
1232
+ enabled=color_enabled,
1233
+ )
1234
+ )
1235
+ continue
1236
+ if not raw_val:
1237
+ current_inline_tuning.pop(key, None)
1238
+ print(
1239
+ colorize(
1240
+ f" Cleared tuning override for {key!r}.",
1241
+ color=ANSI_DIM,
1242
+ enabled=color_enabled,
1243
+ )
1244
+ )
1245
+ else:
1246
+ value = parse_tuning_value(raw_val)
1247
+ current_inline_tuning[key] = value
1248
+ val_repr = repr(value) if not isinstance(value, str) else f'"{value}"'
1249
+ print(
1250
+ colorize(
1251
+ f" Set {key} = {val_repr}",
1252
+ color=ANSI_GREEN,
1253
+ enabled=color_enabled,
1254
+ )
1255
+ )
1256
+ continue
1257
+
1164
1258
  if message.startswith("/"):
1165
1259
  bare = message.split()[0]
1166
1260
  _USAGE_HINTS: dict[str, str] = {
@@ -1199,6 +1293,7 @@ def run_interactive_chat(
1199
1293
  verbose=verbose,
1200
1294
  stream=(current_mode == "stream"),
1201
1295
  color_enabled=color_enabled,
1296
+ inline_tuning=current_inline_tuning or None,
1202
1297
  )
1203
1298
  while hitl is not None:
1204
1299
  req = hitl.get("request") or {}
@@ -1241,6 +1336,7 @@ def run_interactive_chat(
1241
1336
  stream=(current_mode == "stream"),
1242
1337
  color_enabled=color_enabled,
1243
1338
  resume_payload=resume_value,
1339
+ inline_tuning=current_inline_tuning or None,
1244
1340
  )
1245
1341
  if exit_code != 0:
1246
1342
  print("The request failed. Use /help for commands or try another agent.")