fred-runtime 2.0.0__tar.gz → 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/PKG-INFO +1 -1
  2. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/agent_app.py +159 -69
  3. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime.egg-info/PKG-INFO +1 -1
  4. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/pyproject.toml +1 -1
  5. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_agent_app.py +150 -0
  6. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/README.md +0 -0
  7. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/__init__.py +0 -0
  8. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/__init__.py +0 -0
  9. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/_catalogs.py +0 -0
  10. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/config.py +0 -0
  11. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/config_loader.py +0 -0
  12. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/container.py +0 -0
  13. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/context.py +0 -0
  14. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/dependencies.py +0 -0
  15. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/mcp_config.py +0 -0
  16. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/observability_factory.py +0 -0
  17. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/app/openai_compat_router.py +0 -0
  18. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/cli/__init__.py +0 -0
  19. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/cli/completion.py +0 -0
  20. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/cli/entrypoint.py +0 -0
  21. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/cli/history_display.py +0 -0
  22. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/cli/kpi_display.py +0 -0
  23. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/cli/pod_client.py +0 -0
  24. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/cli/repl.py +0 -0
  25. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/cli/repl_helpers.py +0 -0
  26. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/cli/url_helpers.py +0 -0
  27. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/client.py +0 -0
  28. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/__init__.py +0 -0
  29. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/context_aware_tool.py +0 -0
  30. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/kf_base_client.py +0 -0
  31. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/kf_fast_text_client.py +0 -0
  32. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/kf_http_client.py +0 -0
  33. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/kf_logs_client.py +0 -0
  34. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  35. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  36. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/kf_workspace_client.py +0 -0
  37. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/mcp_interceptors.py +0 -0
  38. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/mcp_runtime.py +0 -0
  39. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/mcp_toolkit.py +0 -0
  40. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/mcp_utils.py +0 -0
  41. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/structures.py +0 -0
  42. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/token_expiry.py +0 -0
  43. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/common/tool_node_utils.py +0 -0
  44. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/deep/__init__.py +0 -0
  45. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/deep/deep_runtime.py +0 -0
  46. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/graph/__init__.py +0 -0
  47. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/graph/graph_runtime.py +0 -0
  48. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/integrations/__init__.py +0 -0
  49. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  50. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/integrations/v2_runtime/adapters.py +0 -0
  51. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/model_routing/__init__.py +0 -0
  52. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/model_routing/catalog.py +0 -0
  53. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/model_routing/contracts.py +0 -0
  54. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/model_routing/provider.py +0 -0
  55. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/model_routing/resolver.py +0 -0
  56. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/__init__.py +0 -0
  57. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_langchain_adapter.py +0 -0
  58. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_message_codec.py +0 -0
  59. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_model_adapter.py +0 -0
  60. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_prompting.py +0 -0
  61. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_runtime.py +0 -0
  62. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_stream_adapter.py +0 -0
  63. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_tool_binding.py +0 -0
  64. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_tool_loop.py +0 -0
  65. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_tool_rendering.py +0 -0
  66. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_tool_resolution.py +0 -0
  67. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_tool_utils.py +0 -0
  68. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/react/react_tracing.py +0 -0
  69. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/runtime_context.py +0 -0
  70. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/runtime_support/__init__.py +0 -0
  71. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/runtime_support/checkpoints.py +0 -0
  72. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/runtime_support/model_metadata.py +0 -0
  73. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  74. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
  75. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  76. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/support/__init__.py +0 -0
  77. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/support/filesystem_context.py +0 -0
  78. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/support/tool_approval.py +0 -0
  79. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime/support/tool_loop.py +0 -0
  80. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime.egg-info/SOURCES.txt +0 -0
  81. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime.egg-info/dependency_links.txt +0 -0
  82. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime.egg-info/entry_points.txt +0 -0
  83. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime.egg-info/requires.txt +0 -0
  84. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/fred_runtime.egg-info/top_level.txt +0 -0
  85. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/setup.cfg +0 -0
  86. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_client.py +0 -0
  87. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_config_loader.py +0 -0
  88. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_context.py +0 -0
  89. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_graph_runtime_observability.py +0 -0
  90. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_history.py +0 -0
  91. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_kf_workspace_client.py +0 -0
  92. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_kpi_display.py +0 -0
  93. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_mcp_config.py +0 -0
  94. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_openai_compat_router.py +0 -0
  95. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_smoke.py +0 -0
  96. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/tests/test_url_helpers.py +0 -0
  97. {fred_runtime-2.0.0 → fred_runtime-2.0.1}/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.0
3
+ Version: 2.0.1
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
@@ -558,12 +558,8 @@ class LocalRegistryAgentInvoker(AgentInvokerPort):
558
558
  is_error=True,
559
559
  )
560
560
 
561
- execute_request = _AgentExecuteRequest.model_construct(
562
- agent_id=request.agent_id,
563
- agent_instance_id=None,
564
- message=request.message,
565
- context=request.context.model_dump(mode="json"),
566
- resume_payload=None,
561
+ execute_request = _to_internal_request(
562
+ _build_runtime_execute_request_from_invocation(request)
567
563
  )
568
564
 
569
565
  content_parts: list[str] = []
@@ -770,6 +766,13 @@ class _AgentExecuteRequest(BaseModel):
770
766
  return self
771
767
 
772
768
 
769
+ @dataclass(slots=True)
770
+ class _PreparedRuntimeExecution:
771
+ runtime: ReActRuntime | GraphRuntime
772
+ execution_config: ExecutionConfig
773
+ executor_input: Any
774
+
775
+
773
776
  def _to_internal_request(r: RuntimeExecuteRequest) -> "_AgentExecuteRequest":
774
777
  """
775
778
  Bridge a public RuntimeExecuteRequest to the internal execution model.
@@ -794,6 +797,36 @@ def _to_internal_request(r: RuntimeExecuteRequest) -> "_AgentExecuteRequest":
794
797
  )
795
798
 
796
799
 
800
+ def _build_runtime_execute_request_from_invocation(
801
+ request: AgentInvocationRequest,
802
+ ) -> RuntimeExecuteRequest:
803
+ """
804
+ Project one in-process agent invocation onto the public execute contract.
805
+
806
+ Why this exists:
807
+ - pod-local agent-to-agent calls should follow the same request projection
808
+ path as HTTP execution, rather than hand-constructing a second private
809
+ request shape
810
+ - future continuity fields should therefore land once on the typed runtime
811
+ contract, then flow through both local and remote invocation paths
812
+
813
+ How to use it:
814
+ - call from `LocalRegistryAgentInvoker.invoke(...)`
815
+ - pass the result through `_to_internal_request(...)` until the remaining
816
+ internal helpers consume `RuntimeExecuteRequest` directly
817
+
818
+ Example:
819
+ - `runtime_request = _build_runtime_execute_request_from_invocation(request)`
820
+ """
821
+
822
+ return RuntimeExecuteRequest(
823
+ agent_id=request.agent_id,
824
+ input=request.message,
825
+ session_id=request.context.session_id,
826
+ runtime_context=request.context.model_dump(mode="json"),
827
+ )
828
+
829
+
797
830
  class _AgentTemplateSummary(BaseModel):
798
831
  template_agent_id: str
799
832
  title: str
@@ -838,17 +871,23 @@ def _apply_runtime_tuning(
838
871
  - `definition = _apply_runtime_tuning(template_definition, resolution.tuning)`
839
872
  """
840
873
 
841
- return definition.model_copy(
842
- update={
843
- "role": tuning.role,
844
- "description": tuning.description,
845
- "tags": tuple(tuning.tags),
846
- "fields": tuple(field.model_copy(deep=True) for field in tuning.fields),
847
- "default_mcp_servers": tuple(
848
- server.model_copy(deep=True) for server in tuning.mcp_servers
849
- ),
850
- }
851
- )
874
+ update: dict[str, Any] = {
875
+ "role": tuning.role,
876
+ "description": tuning.description,
877
+ "tags": tuple(tuning.tags),
878
+ "fields": tuple(field.model_copy(deep=True) for field in tuning.fields),
879
+ "default_mcp_servers": tuple(
880
+ server.model_copy(deep=True) for server in tuning.mcp_servers
881
+ ),
882
+ }
883
+ system_prompt = tuning.values.get("prompts.system")
884
+ if (
885
+ isinstance(definition, ReActAgentDefinition)
886
+ and isinstance(system_prompt, str)
887
+ and system_prompt.strip()
888
+ ):
889
+ update["system_prompt_template"] = system_prompt
890
+ return definition.model_copy(update=update)
852
891
 
853
892
 
854
893
  def _available_mcp_servers_for_definition(
@@ -1565,7 +1604,42 @@ async def _stream(
1565
1604
  )
1566
1605
 
1567
1606
 
1568
- async def _iterate_runtime_event_payloads(
1607
+ def _build_executor_input(
1608
+ definition: ReActAgentDefinition | GraphAgentDefinition,
1609
+ request: _AgentExecuteRequest,
1610
+ ) -> Any:
1611
+ """
1612
+ Normalize one turn into the executor input expected by the selected runtime.
1613
+
1614
+ Why this exists:
1615
+ - `ReActRuntime` and `GraphRuntime` accept different input shapes
1616
+ - resume turns also bypass normal message validation, so the mapping should
1617
+ live in one helper instead of being repeated inline in the execution loop
1618
+
1619
+ How to use it:
1620
+ - call while assembling one prepared runtime execution
1621
+ - pass the returned object unchanged to `executor.stream(...)`
1622
+
1623
+ Example:
1624
+ - `executor_input = _build_executor_input(definition, request)`
1625
+ """
1626
+
1627
+ if isinstance(definition, GraphAgentDefinition):
1628
+ input_cls = definition.input_model()
1629
+ if request.resume_payload is not None:
1630
+ return input_cls.model_construct(message="")
1631
+ return input_cls.model_validate({"message": request.message or ""})
1632
+
1633
+ return ReActInput(
1634
+ messages=(
1635
+ ()
1636
+ if request.resume_payload is not None
1637
+ else (ReActMessage(role=ReActMessageRole.USER, content=request.message),)
1638
+ ),
1639
+ )
1640
+
1641
+
1642
+ def _prepare_runtime_execution(
1569
1643
  definition: ReActAgentDefinition | GraphAgentDefinition,
1570
1644
  request: _AgentExecuteRequest,
1571
1645
  access_token: str | None = None,
@@ -1573,26 +1647,24 @@ async def _iterate_runtime_event_payloads(
1573
1647
  team_id: str | None = None,
1574
1648
  registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition] | None = None,
1575
1649
  exchange_id: str | None = None,
1576
- ) -> AsyncIterator[dict[str, Any]]:
1650
+ ) -> _PreparedRuntimeExecution:
1577
1651
  """
1578
- Execute one agent turn and yield runtime-event payloads as JSON-ready dicts.
1652
+ Build the bound runtime, executor input, and execution config for one turn.
1579
1653
 
1580
- Why this helper exists:
1581
- - both `/agents/execute` and `/agents/execute/stream` share the same runtime
1582
- wiring and event production path
1583
- - keeping the generator payload-oriented lets the HTTP layer choose whether
1584
- it renders SSE or returns a terminal JSON response
1654
+ Why this exists:
1655
+ - `execute`, `execute/stream`, and in-process agent invocation all converge
1656
+ on `_iterate_runtime_event_payloads`, so this is the narrowest place to
1657
+ centralize request projection before memory fields are added
1658
+ - it removes one long block of binding/runtime setup from the event loop and
1659
+ gives future continuity fields a single place to enter the runtime stack
1585
1660
 
1586
- team_id:
1587
- - callers are responsible for resolving the effective team before calling this
1588
- function; see _stream() for the standalone "personal" default logic
1589
- - None is accepted for agent-to-agent (AgentInvoker) invocations where no
1590
- team scope is required
1661
+ How to use it:
1662
+ - call from `_iterate_runtime_event_payloads(...)`
1663
+ - activate the returned runtime, obtain its executor, then stream with the
1664
+ returned `executor_input` and `execution_config`
1591
1665
 
1592
- access_token:
1593
- - the user's JWT forwarded via the Authorization header
1594
- - stored in RuntimeContext so KF tool adapters can use it for outbound calls
1595
- - None in local dev when security is disabled
1666
+ Example:
1667
+ - `prepared = _prepare_runtime_execution(definition, request, team_id="fredlab")`
1596
1668
  """
1597
1669
 
1598
1670
  request_id = str(uuid4())
@@ -1651,7 +1723,6 @@ async def _iterate_runtime_event_payloads(
1651
1723
  runtime_context=runtime_context,
1652
1724
  portable_context=portable_context,
1653
1725
  )
1654
-
1655
1726
  services = _build_runtime_services(
1656
1727
  definition,
1657
1728
  binding,
@@ -1659,8 +1730,9 @@ async def _iterate_runtime_event_payloads(
1659
1730
  registry=registry,
1660
1731
  access_token=access_token,
1661
1732
  )
1733
+ runtime: ReActRuntime | GraphRuntime
1662
1734
  if isinstance(definition, GraphAgentDefinition):
1663
- runtime: ReActRuntime | GraphRuntime = GraphRuntime(
1735
+ runtime = GraphRuntime(
1664
1736
  definition=definition,
1665
1737
  services=services,
1666
1738
  )
@@ -1679,40 +1751,58 @@ async def _iterate_runtime_event_payloads(
1679
1751
  checkpoint_id=request.checkpoint_id,
1680
1752
  resume_payload=request.resume_payload,
1681
1753
  )
1754
+ return _PreparedRuntimeExecution(
1755
+ runtime=runtime,
1756
+ execution_config=execution_config,
1757
+ executor_input=_build_executor_input(definition, request),
1758
+ )
1759
+
1760
+
1761
+ async def _iterate_runtime_event_payloads(
1762
+ definition: ReActAgentDefinition | GraphAgentDefinition,
1763
+ request: _AgentExecuteRequest,
1764
+ access_token: str | None = None,
1765
+ *,
1766
+ team_id: str | None = None,
1767
+ registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition] | None = None,
1768
+ exchange_id: str | None = None,
1769
+ ) -> AsyncIterator[dict[str, Any]]:
1770
+ """
1771
+ Execute one agent turn and yield runtime-event payloads as JSON-ready dicts.
1772
+
1773
+ Why this helper exists:
1774
+ - both `/agents/execute` and `/agents/execute/stream` share the same runtime
1775
+ wiring and event production path
1776
+ - keeping the generator payload-oriented lets the HTTP layer choose whether
1777
+ it renders SSE or returns a terminal JSON response
1778
+
1779
+ team_id:
1780
+ - callers are responsible for resolving the effective team before calling this
1781
+ function; see _stream() for the standalone "personal" default logic
1782
+ - None is accepted for agent-to-agent (AgentInvoker) invocations where no
1783
+ team scope is required
1784
+
1785
+ access_token:
1786
+ - the user's JWT forwarded via the Authorization header
1787
+ - stored in RuntimeContext so KF tool adapters can use it for outbound calls
1788
+ - None in local dev when security is disabled
1789
+ """
1790
+ prepared = _prepare_runtime_execution(
1791
+ definition,
1792
+ request,
1793
+ access_token=access_token,
1794
+ team_id=team_id,
1795
+ registry=registry,
1796
+ exchange_id=exchange_id,
1797
+ )
1682
1798
 
1683
1799
  try:
1684
- await runtime.activate()
1685
- executor = await runtime.get_executor()
1686
- if isinstance(definition, GraphAgentDefinition):
1687
- # Graph agents receive their typed input schema; the agent's
1688
- # build_turn_state() maps it to graph state before the first node runs.
1689
- # The standard contract is a single "message" field in the input schema.
1690
- # On a HITL resume the runtime ignores input entirely (state is loaded
1691
- # from the checkpoint), so bypass validation with model_construct.
1692
- input_cls = definition.input_model()
1693
- if request.resume_payload is not None:
1694
- graph_input = input_cls.model_construct(message="")
1695
- else:
1696
- graph_input = input_cls.model_validate(
1697
- {"message": request.message or ""}
1698
- )
1699
- executor_input: ReActInput | object = graph_input
1700
- else:
1701
- # On HITL resume, messages are ignored by the codec — the graph
1702
- # resumes from its checkpointed interrupt via Command(resume=...).
1703
- # On a normal turn, the user message is the only input.
1704
- executor_input = ReActInput(
1705
- messages=(
1706
- ()
1707
- if request.resume_payload is not None
1708
- else (
1709
- ReActMessage(
1710
- role=ReActMessageRole.USER, content=request.message
1711
- ),
1712
- )
1713
- ),
1714
- )
1715
- async for event in executor.stream(executor_input, execution_config):
1800
+ await prepared.runtime.activate()
1801
+ executor = await prepared.runtime.get_executor()
1802
+ async for event in executor.stream(
1803
+ prepared.executor_input,
1804
+ prepared.execution_config,
1805
+ ):
1716
1806
  payload = event.model_dump(mode="json")
1717
1807
  if not isinstance(payload, dict):
1718
1808
  raise RuntimeError(
@@ -1725,7 +1815,7 @@ async def _iterate_runtime_event_payloads(
1725
1815
  )
1726
1816
  yield RuntimeErrorEvent(message=str(exc)).model_dump(mode="json")
1727
1817
  finally:
1728
- await runtime.dispose()
1818
+ await prepared.runtime.dispose()
1729
1819
 
1730
1820
 
1731
1821
  def _terminal_execute_payload(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-runtime
3
- Version: 2.0.0
3
+ Version: 2.0.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fred-runtime"
3
- version = "2.0.0"
3
+ version = "2.0.1"
4
4
  description = "Runtime adapters and infrastructure wiring for Fred v2 agents."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.13"
@@ -4,6 +4,7 @@ import asyncio
4
4
  import json
5
5
  import time
6
6
  from types import SimpleNamespace
7
+ from typing import cast
7
8
 
8
9
  from conftest import StaticChatModelFactory, ToolFriendlyFakeChatModel
9
10
  from fastapi.testclient import TestClient
@@ -14,6 +15,11 @@ from fred_core.kpi.prometheus_kpi_store import PrometheusKPIStore
14
15
  from fred_core.users.store import postgres_user_store
15
16
  from fred_sdk.authoring import ReActAgent, tool
16
17
  from fred_sdk.authoring.api import ToolContext
18
+ from fred_sdk.contracts.context import (
19
+ AgentInvocationRequest,
20
+ PortableContext,
21
+ PortableEnvironment,
22
+ )
17
23
  from fred_sdk.contracts.execution import ExecutionGrant, ExecutionGrantAction
18
24
  from fred_sdk.contracts.models import ReActAgentDefinition
19
25
  from langchain_core.messages import AIMessage
@@ -884,6 +890,88 @@ def test_execute_route_propagates_checkpoint_and_observability_context(
884
890
  }
885
891
 
886
892
 
893
+ def test_local_registry_invoker_reuses_runtime_execute_projection(monkeypatch) -> None:
894
+ """
895
+ Ensure local agent invocation flows through the typed runtime request bridge.
896
+
897
+ Why this exists:
898
+ - the multi-agent memory work needs one request-projection path for HTTP and
899
+ in-process agent calls, or new continuity fields will be duplicated again
900
+ - this regression proves `LocalRegistryAgentInvoker` no longer hand-builds a
901
+ separate private request payload
902
+
903
+ How to use it:
904
+ - run in the default offline `fred-runtime` test suite
905
+
906
+ Example:
907
+ - `pytest tests/test_agent_app.py -q`
908
+ """
909
+
910
+ seen: dict[str, object] = {}
911
+
912
+ async def _fake_iterate_runtime_event_payloads(
913
+ definition,
914
+ request,
915
+ access_token=None,
916
+ *,
917
+ team_id=None,
918
+ registry=None,
919
+ exchange_id=None,
920
+ ):
921
+ _ = (definition, access_token, team_id, registry, exchange_id)
922
+ seen["checkpoint_id"] = request.checkpoint_id
923
+ seen["context"] = dict(request.context or {})
924
+ yield {"kind": "final", "sequence": 0, "content": "ok"}
925
+
926
+ monkeypatch.setattr(
927
+ agent_app_module,
928
+ "_iterate_runtime_event_payloads",
929
+ _fake_iterate_runtime_event_payloads,
930
+ )
931
+
932
+ definition = _EchoAgent()
933
+ invoker = agent_app_module.LocalRegistryAgentInvoker(
934
+ registry={definition.agent_id: definition},
935
+ access_token="token-1",
936
+ )
937
+
938
+ result = asyncio.run(
939
+ invoker.invoke(
940
+ AgentInvocationRequest(
941
+ agent_id=definition.agent_id,
942
+ message="hello",
943
+ context=PortableContext(
944
+ request_id="req-1",
945
+ correlation_id="corr-1",
946
+ actor="alice",
947
+ tenant="tenant-a",
948
+ environment=PortableEnvironment.DEV,
949
+ trace_id="trace-1",
950
+ session_id="session-1",
951
+ user_id="alice",
952
+ team_id="fredlab",
953
+ ),
954
+ )
955
+ )
956
+ )
957
+
958
+ assert result.content == "ok"
959
+ assert result.is_error is False
960
+ assert seen["checkpoint_id"] is None
961
+ context = seen["context"]
962
+ assert isinstance(context, dict)
963
+ assert context["request_id"] == "req-1"
964
+ assert context["correlation_id"] == "corr-1"
965
+ assert context["actor"] == "alice"
966
+ assert context["tenant"] == "tenant-a"
967
+ assert context["environment"] == "dev"
968
+ assert context["trace_id"] == "trace-1"
969
+ assert context["session_id"] == "session-1"
970
+ assert context["user_id"] == "alice"
971
+ assert context["team_id"] == "fredlab"
972
+ assert context["execution_action"] == "execute"
973
+
974
+
887
975
  def test_resume_rejects_non_pending_checkpoint(monkeypatch, tmp_path) -> None:
888
976
  """
889
977
  Ensure resume requests fail fast when the checkpoint is not waiting for input.
@@ -1071,3 +1159,65 @@ def test_no_security_resolves_personal_team_in_portable_context(
1071
1159
  assert any("team:personal" in e.get("content", "") for e in tool_results), (
1072
1160
  f"Expected team:personal in tool_result events, got: {tool_results}"
1073
1161
  )
1162
+
1163
+
1164
+ def test_apply_runtime_tuning_applies_system_prompt_from_values() -> None:
1165
+ """
1166
+ Ensure _apply_runtime_tuning writes prompts.system into system_prompt_template.
1167
+
1168
+ Why this exists:
1169
+ - control-plane stores user-set field values in AgentTuning.values; the
1170
+ runtime must apply them at execution time, not silently drop them
1171
+
1172
+ How to use it:
1173
+ - run in the default offline fred-runtime test suite
1174
+
1175
+ Example:
1176
+ - `pytest tests/test_agent_app.py::test_apply_runtime_tuning_applies_system_prompt_from_values -q`
1177
+ """
1178
+ from fred_sdk.contracts.models import AgentTuning
1179
+ from fred_runtime.app.agent_app import _apply_runtime_tuning
1180
+
1181
+ definition = _EchoAgent()
1182
+ assert (
1183
+ definition.system_prompt_template
1184
+ == "Use the demo_echo tool, then answer briefly."
1185
+ )
1186
+
1187
+ tuning = AgentTuning(
1188
+ role=definition.role,
1189
+ description=definition.description,
1190
+ values={"prompts.system": "Custom override prompt."},
1191
+ )
1192
+ result = cast(_EchoAgent, _apply_runtime_tuning(definition, tuning))
1193
+ assert result.system_prompt_template == "Custom override prompt."
1194
+ assert result.policy().system_prompt_template == "Custom override prompt."
1195
+
1196
+
1197
+ def test_apply_runtime_tuning_ignores_blank_system_prompt() -> None:
1198
+ """
1199
+ Ensure _apply_runtime_tuning does not override when prompts.system is blank.
1200
+
1201
+ Why this exists:
1202
+ - an empty or whitespace-only value means "use the agent default"; the
1203
+ control-plane UI stores an empty string when the field is cleared
1204
+
1205
+ How to use it:
1206
+ - run in the default offline fred-runtime test suite
1207
+ """
1208
+ from fred_sdk.contracts.models import AgentTuning
1209
+ from fred_runtime.app.agent_app import _apply_runtime_tuning
1210
+
1211
+ definition = _EchoAgent()
1212
+ original = definition.system_prompt_template
1213
+
1214
+ for blank in ("", " "):
1215
+ tuning = AgentTuning(
1216
+ role=definition.role,
1217
+ description=definition.description,
1218
+ values={"prompts.system": blank},
1219
+ )
1220
+ result = cast(_EchoAgent, _apply_runtime_tuning(definition, tuning))
1221
+ assert result.system_prompt_template == original, (
1222
+ f"blank {blank!r} should not override"
1223
+ )
File without changes
File without changes