fred-runtime 2.0.3__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.3 → fred_runtime-2.0.4}/PKG-INFO +1 -1
  2. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/agent_app.py +98 -9
  3. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/cli/completion.py +19 -0
  4. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/cli/history_display.py +3 -0
  5. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/cli/pod_client.py +18 -0
  6. {fred_runtime-2.0.3 → 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.3 → fred_runtime-2.0.4}/fred_runtime/graph/graph_runtime.py +12 -8
  9. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_message_codec.py +3 -4
  10. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_model_adapter.py +2 -3
  11. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_prompting.py +12 -4
  12. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_runtime.py +14 -9
  13. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_tool_binding.py +1 -2
  14. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_tool_resolution.py +2 -3
  15. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime.egg-info/PKG-INFO +1 -1
  16. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime.egg-info/SOURCES.txt +6 -0
  17. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/pyproject.toml +13 -1
  18. {fred_runtime-2.0.3 → 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.3 → 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.3/fred_runtime/cli/repl_helpers.py +0 -188
  27. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/README.md +0 -0
  28. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/__init__.py +0 -0
  29. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/__init__.py +0 -0
  30. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/_catalogs.py +0 -0
  31. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/config.py +0 -0
  32. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/config_loader.py +0 -0
  33. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/container.py +0 -0
  34. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/context.py +0 -0
  35. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/dependencies.py +0 -0
  36. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/mcp_config.py +0 -0
  37. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/observability_factory.py +0 -0
  38. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/app/openai_compat_router.py +0 -0
  39. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/cli/__init__.py +0 -0
  40. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/cli/entrypoint.py +0 -0
  41. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/cli/kpi_display.py +0 -0
  42. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/cli/url_helpers.py +0 -0
  43. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/client.py +0 -0
  44. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/__init__.py +0 -0
  45. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/context_aware_tool.py +0 -0
  46. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/kf_base_client.py +0 -0
  47. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/kf_fast_text_client.py +0 -0
  48. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/kf_http_client.py +0 -0
  49. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/kf_logs_client.py +0 -0
  50. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  51. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  52. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/kf_workspace_client.py +0 -0
  53. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/mcp_interceptors.py +0 -0
  54. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/mcp_runtime.py +0 -0
  55. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/mcp_toolkit.py +0 -0
  56. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/mcp_utils.py +0 -0
  57. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/structures.py +0 -0
  58. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/token_expiry.py +0 -0
  59. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/common/tool_node_utils.py +0 -0
  60. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/deep/__init__.py +0 -0
  61. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/deep/deep_runtime.py +3 -3
  62. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/eval/__init__.py +0 -0
  63. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/eval/collector.py +0 -0
  64. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/graph/__init__.py +0 -0
  65. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/integrations/__init__.py +0 -0
  66. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  67. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/integrations/v2_runtime/adapters.py +0 -0
  68. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/model_routing/__init__.py +0 -0
  69. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/model_routing/catalog.py +0 -0
  70. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/model_routing/contracts.py +0 -0
  71. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/model_routing/provider.py +0 -0
  72. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/model_routing/resolver.py +0 -0
  73. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/__init__.py +0 -0
  74. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_langchain_adapter.py +0 -0
  75. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_stream_adapter.py +2 -2
  76. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_tool_loop.py +5 -5
  77. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_tool_rendering.py +0 -0
  78. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_tool_utils.py +0 -0
  79. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/react/react_tracing.py +0 -0
  80. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/runtime_context.py +0 -0
  81. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/runtime_support/__init__.py +0 -0
  82. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/runtime_support/checkpoints.py +0 -0
  83. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/runtime_support/model_metadata.py +0 -0
  84. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  85. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
  86. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  87. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/support/__init__.py +0 -0
  88. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/support/filesystem_context.py +0 -0
  89. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/support/tool_approval.py +0 -0
  90. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime/support/tool_loop.py +0 -0
  91. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime.egg-info/dependency_links.txt +0 -0
  92. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime.egg-info/entry_points.txt +0 -0
  93. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime.egg-info/requires.txt +0 -0
  94. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/fred_runtime.egg-info/top_level.txt +0 -0
  95. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/setup.cfg +0 -0
  96. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_client.py +0 -0
  97. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_config_loader.py +0 -0
  98. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_context.py +0 -0
  99. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_graph_runtime_observability.py +1 -1
  100. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_history.py +0 -0
  101. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_kf_workspace_client.py +0 -0
  102. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_kpi_display.py +0 -0
  103. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_mcp_config.py +0 -0
  104. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_openai_compat_router.py +0 -0
  105. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_smoke.py +0 -0
  106. {fred_runtime-2.0.3 → fred_runtime-2.0.4}/tests/test_url_helpers.py +0 -0
  107. {fred_runtime-2.0.3 → 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.3
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,7 +62,6 @@ 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,
@@ -72,6 +71,7 @@ from fred_sdk.contracts.context import (
72
71
  PortableEnvironment,
73
72
  RuntimeContext,
74
73
  )
74
+ from fred_sdk.contracts.eval import EvalStep, EvalTrace
75
75
  from fred_sdk.contracts.execution import (
76
76
  ExecutionGrantAction,
77
77
  ExecutionGrantViolation,
@@ -84,6 +84,7 @@ from fred_sdk.contracts.models import (
84
84
  GraphAgentDefinition,
85
85
  MCPServerConfiguration,
86
86
  ReActAgentDefinition,
87
+ TuningValue,
87
88
  )
88
89
  from fred_sdk.contracts.react_contract import ReActInput, ReActMessage, ReActMessageRole
89
90
  from fred_sdk.contracts.runtime import (
@@ -95,9 +96,6 @@ from fred_sdk.contracts.runtime import (
95
96
  RuntimeEvent,
96
97
  RuntimeServices,
97
98
  )
98
- from fred_runtime.graph.graph_runtime import GraphRuntime
99
- from fred_runtime.react.react_runtime import ReActRuntime
100
- from fred_runtime.runtime_support.checkpoints import load_checkpoint
101
99
  from fred_sdk.support.authored_toolsets import (
102
100
  AuthoredToolRuntimePorts,
103
101
  build_authored_tool_handlers,
@@ -105,6 +103,9 @@ from fred_sdk.support.authored_toolsets import (
105
103
  from pydantic import BaseModel, Field, TypeAdapter, model_validator
106
104
 
107
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
108
109
 
109
110
  from ..common.structures import AgentSettingsLike
110
111
  from ..integrations.v2_runtime.adapters import (
@@ -754,6 +755,10 @@ class _AgentExecuteRequest(BaseModel):
754
755
  default=(),
755
756
  description="Prior conversation turns forwarded by the calling agent.",
756
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
+ )
757
762
 
758
763
  @model_validator(mode="after")
759
764
  def _require_message_or_resume(self) -> "_AgentExecuteRequest":
@@ -800,6 +805,8 @@ def _to_internal_request(r: RuntimeExecuteRequest) -> "_AgentExecuteRequest":
800
805
  context=r.to_legacy_context() or None,
801
806
  checkpoint_id=r.checkpoint_id,
802
807
  resume_payload=r.resume_payload,
808
+ invocation_turns=r.invocation_turns,
809
+ inline_tuning=r.inline_tuning,
803
810
  )
804
811
 
805
812
 
@@ -812,6 +819,18 @@ class _AgentTemplateSummary(BaseModel):
812
819
  available_mcp_servers: list[MCPServerConfiguration] = Field(default_factory=list)
813
820
 
814
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
+
815
834
  class _ResolvedAgentInstance(BaseModel):
816
835
  agent_instance_id: str
817
836
  template_agent_id: str
@@ -833,7 +852,7 @@ def _apply_runtime_tuning(
833
852
  definition: ReActAgentDefinition | GraphAgentDefinition, tuning: AgentTuning
834
853
  ) -> ReActAgentDefinition | GraphAgentDefinition:
835
854
  """
836
- Overlay persisted business tuning onto one registered ReAct template.
855
+ Overlay persisted business tuning onto one registered agent template.
837
856
 
838
857
  Why this exists:
839
858
  - control-plane stores the full effective tuning for a managed agent
@@ -847,18 +866,29 @@ def _apply_runtime_tuning(
847
866
  - `definition = _apply_runtime_tuning(template_definition, resolution.tuning)`
848
867
  """
849
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
+
850
874
  update: dict[str, object] = {
851
875
  "role": tuning.role,
852
876
  "description": tuning.description,
853
877
  "tags": tuple(tuning.tags),
854
878
  "fields": tuple(field.model_copy(deep=True) for field in tuning.fields),
855
879
  "default_mcp_servers": tuple(
856
- server.model_copy(deep=True) for server in tuning.mcp_servers
880
+ server.model_copy(deep=True) for server in mcp_servers
857
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),
858
886
  }
859
- system_prompt = tuning.values.get("prompts.system")
860
- if isinstance(system_prompt, str) and system_prompt.strip():
861
- 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
862
892
  return definition.model_copy(update=update)
863
893
 
864
894
 
@@ -921,6 +951,18 @@ async def _resolve_agent_instance(
921
951
  detail=f"Unknown agent_id: {request.agent_id!r}. "
922
952
  f"Known agents: {list(registry.keys())}",
923
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
+ )
924
966
  return _ResolvedExecutionTarget(
925
967
  definition=definition,
926
968
  effective_agent_id=definition.agent_id,
@@ -1751,11 +1793,24 @@ async def _iterate_runtime_event_payloads(
1751
1793
  user_groups=ctx.get("user_groups"),
1752
1794
  language=ctx.get("language"),
1753
1795
  access_token=access_token,
1796
+ refresh_token=ctx.get("refresh_token"),
1797
+ access_token_expires_at=ctx.get("access_token_expires_at"),
1754
1798
  trace_id=ctx.get("trace_id"),
1755
1799
  correlation_id=correlation_id,
1756
1800
  agent_instance_id=request.agent_instance_id,
1757
1801
  template_agent_id=definition.agent_id,
1758
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"),
1759
1814
  )
1760
1815
 
1761
1816
  binding = BoundRuntimeContext(
@@ -1979,6 +2034,40 @@ def _build_agent_router(
1979
2034
  for definition in registry.values()
1980
2035
  ]
1981
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
+
1982
2071
  @router.get("/sessions", dependencies=_auth_deps)
1983
2072
  async def list_sessions(user_id: str) -> list[str]:
1984
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.")