railtracks

The Railtracks Framework for building resilient agentic systems in simple python

  1#   -------------------------------------------------------------
  2#   Copyright (c) Railtown AI. All rights reserved.
  3#   Licensed under the MIT License. See LICENSE in project root for information.
  4#   -------------------------------------------------------------
  5"""The Railtracks Framework for building resilient agentic systems in simple python"""
  6
  7from __future__ import annotations
  8
  9import importlib
 10import logging
 11from typing import TYPE_CHECKING
 12
 13from dotenv import load_dotenv
 14
 15if TYPE_CHECKING:
 16    from railtracks import retrieval
 17    from railtracks.interaction import interactive as interactive
 18
 19__all__ = [
 20    "Session",
 21    "session",
 22    "call",
 23    "broadcast",
 24    "call_batch",
 25    "ExecutionInfo",
 26    "ExecutorConfig",
 27    "llm",
 28    "guardrails",
 29    "context",
 30    "set_config",
 31    "context",
 32    "function_node",
 33    "agent_node",
 34    "integrations",
 35    "prebuilt",
 36    "MCPStdioParams",
 37    "MCPHttpParams",
 38    "connect_mcp",
 39    "create_mcp_server",
 40    "ToolManifest",
 41    "session_id",
 42    "evaluations",
 43    "observability",
 44    "retrieval",
 45    "Flow",
 46    "enable_logging",
 47    "escape_braces",
 48]
 49
 50from railtracks.built_nodes import (
 51    agent_node,
 52    function_node,
 53)
 54
 55from . import (
 56    context,
 57    evaluations,
 58    guardrails,
 59    integrations,
 60    llm,
 61    observability,
 62    prebuilt,
 63)
 64from ._session import ExecutionInfo, Session, session
 65from .context.central import session_id, set_config
 66from .interaction import broadcast, call, call_batch
 67from .llm.prompt_injection_utils import escape_braces
 68from .nodes.manifest import ToolManifest
 69from .orchestration.flow import Flow
 70from .rt_mcp import MCPHttpParams, MCPStdioParams, connect_mcp, create_mcp_server
 71from .utils.config import ExecutorConfig
 72from .utils.deprecation import warn_pending_change
 73from .utils.logging.config import enable_logging
 74
 75load_dotenv()
 76
 77# Library does not configure logging by default. Add NullHandler so the RT logger
 78# never emits "No handlers could be found". Call enable_logging() to opt in.
 79logging.getLogger("RT").addHandler(logging.NullHandler())
 80
 81# Do not worry about changing this version number manually. It will updated on release.
 82__version__ = "1.0.0"
 83
 84
 85def __getattr__(name: str):
 86    if name == "interactive":
 87        # Not cached in globals()
 88        warn_pending_change(
 89            "rt.interactive",
 90            change="is removed",
 91            detail="There is no replacement; the local chat UI is going away.",
 92        )
 93        return importlib.import_module("railtracks.interaction.interactive")
 94    if name == "retrieval":
 95        try:
 96            module = importlib.import_module("railtracks.retrieval")
 97        except ImportError as exc:
 98            raise ImportError(
 99                "railtracks.retrieval requires the retrieval extras. "
100                "Install with: pip install 'railtracks[retrieval]'"
101            ) from exc
102        globals()[name] = module
103        return module
104    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
105
106
107def __dir__() -> list[str]:
108    # "interactive" is not in __all__ but is still reachable
109    return sorted({*__all__, "interactive"})
class Session:
 37class Session:
 38    """
 39    The main class for managing an execution session.
 40
 41    This class is responsible for setting up all the necessary components for running a Railtracks execution, including the coordinator, publisher, and state management.
 42
 43    For the configuration parameters of the setting. It will follow this precedence:
 44    1. The parameters in the `Session` constructor.
 45    2. The parameters in global context variables.
 46    3. The default values.
 47
 48    Default Values:
 49    - `name`: None
 50    - `timeout`: 150.0 seconds
 51    - `end_on_error`: False
 52    - `broadcast_callback`: None (no callback for broadcast messages)
 53    - `prompt_injection`: True (the prompt will be automatically injected from context variables)
 54    - `save_state`: True (the state of the execution will be saved to a file at the end of the run in the `.railtracks/data/sessions/` directory)
 55
 56
 57    Args:
 58        name (str | None, optional): Optional name for the session. This name will be included in the saved state file if `save_state` is True.
 59        context (Dict[str, Any], optional): A dictionary of global context variables to be used during the execution.
 60        flow_name (str | None, optional): The name of the flow this session is associated with.
 61        flow_id (str | None, optional): The unique identifier of the flow this session is associated with.
 62        timeout (float, optional): The maximum number of seconds to wait for a response to your top-level request.
 63        end_on_error (bool, optional): If True, the execution will stop when an exception is encountered.
 64        broadcast_callback (Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None, optional): A callback function that will be called with the broadcast messages.
 65        prompt_injection (bool, optional): If True, the prompt will be automatically injected from context variables.
 66        save_state (bool, optional): If True, the state of the execution will be saved to a file at the end of the run in the `.railtracks/data/sessions/` directory.
 67    """
 68
 69    def __init__(
 70        self,
 71        context: Dict[str, Any] | None = None,
 72        *,
 73        flow_name: str | None = None,
 74        flow_id: str | None = None,
 75        name: str | None = None,
 76        timeout: float | None = None,
 77        end_on_error: bool | None = None,
 78        broadcast_callback: (
 79            Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
 80        ) = None,
 81        prompt_injection: bool | None = None,
 82        save_state: bool | None = None,
 83        payload_callback: Callable[[dict[str, Any]], None] | None = None,
 84    ):
 85        # first lets read from defaults if nessecary for the provided input config
 86
 87        if flow_name is None:
 88            warnings.warn(
 89                "Sessions should be tied to a flow for better observability and state management. Please use the Flow object to create and manage your sessions (see __ for more details). This warning will become an error in future versions.",
 90                DeprecationWarning,
 91            )
 92
 93        self.executor_config = self.global_config_precedence(
 94            timeout=timeout,
 95            end_on_error=end_on_error,
 96            broadcast_callback=broadcast_callback,
 97            prompt_injection=prompt_injection,
 98            save_state=save_state,
 99            payload_callback=payload_callback,
100        )
101
102        if context is None:
103            context = {}
104
105        self.name = name
106        self.flow_name = flow_name
107        self.flow_id = flow_id
108
109        self.publisher: RTPublisher = RTPublisher()
110
111        self._identifier = str(uuid.uuid4())
112
113        executor_info = ExecutionInfo.create_new()
114        self.coordinator = Coordinator(
115            execution_modes={"async": AsyncioExecutionStrategy()}
116        )
117        self.rt_state = RTState(
118            executor_info, self.executor_config, self.coordinator, self.publisher
119        )
120
121        self.coordinator.start(self.publisher)
122        self._setup_subscriber()
123        register_globals(
124            session_id=self._identifier,
125            rt_publisher=self.publisher,
126            parent_id=None,
127            executor_config=self.executor_config,
128            global_context_vars=context,
129        )
130
131        self._start_time = time.time()
132
133        logger.debug("Session %s is initialized" % self._identifier)
134
135    @classmethod
136    def global_config_precedence(
137        cls,
138        timeout: float | None,
139        end_on_error: bool | None,
140        broadcast_callback: (
141            Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
142        ),
143        prompt_injection: bool | None,
144        save_state: bool | None,
145        payload_callback: Callable[[dict[str, Any]], None] | None,
146    ) -> ExecutorConfig:
147        """
148        Uses the following precedence order to determine the configuration parameters:
149        1. The parameters in the method parameters.
150        2. The parameters in global context variables.
151        3. The default values.
152        """
153        global_executor_config = get_global_config()
154
155        return global_executor_config.precedence_overwritten(
156            timeout=timeout,
157            end_on_error=end_on_error,
158            subscriber=broadcast_callback,
159            prompt_injection=prompt_injection,
160            save_state=save_state,
161            payload_callback=payload_callback,
162        )
163
164    def __enter__(self):
165        return self
166
167    def __exit__(self, exc_type, exc_val, exc_tb):
168        if self.executor_config.save_state:
169            try:
170                railtracks_dir = resolve_railtracks_home()
171                sessions_dir = railtracks_dir / "data" / "sessions"
172                sessions_dir.mkdir(
173                    parents=True, exist_ok=True
174                )  # Creates directory structure if doesn't exist, skips otherwise.
175
176                # Try to create file path with name, fallback to identifier only if there's an issue
177                if self.flow_name is not None:
178                    name = self.flow_name
179                elif self.name is not None:
180                    name = self.name
181                else:
182                    name = ""
183
184                candidate = sessions_dir / f"{name}_{self._identifier}.json"
185                try:
186                    candidate.touch()
187                    candidate.unlink()
188                    file_path = candidate
189                except OSError:
190                    logger.warning(
191                        get_message(
192                            ExceptionMessageKey.INVALID_SESSION_FILE_NAME_WARN
193                        ).format(name=name, identifier=self._identifier)
194                    )
195                    file_path = sessions_dir / f"{self._identifier}.json"
196
197                logger.info("Saving execution info to %s" % file_path)
198
199                content = json.dumps(self.payload())
200                file_path.write_text(content)
201
202            except Exception as e:
203                logger.error(
204                    "Error while saving execution info to file: %s",
205                    e,
206                    exc_info=True,
207                )
208        try:
209            if self.executor_config.payload_callback is not None:
210                self.executor_config.payload_callback(self.payload())
211        except Exception:
212            # TODO: add logging here.
213            pass
214
215        self._close()
216
217    def _setup_subscriber(self):
218        """
219        Prepares and attaches the saved broadcast_callback to the publisher attached to this runner.
220        """
221
222        if self.executor_config.subscriber is not None:
223            self.publisher.subscribe(
224                stream_subscriber(self.executor_config.subscriber),
225                name="Streaming Subscriber",
226            )
227
228    def _close(self):
229        """
230        Closes the runner and cleans up all resources.
231
232        - Shuts down the state object
233        - Deletes all the global variables that were registered in the context
234        """
235        # FIX: Resource leak - publisher background task wasn't being shut down on Session exit
236        # VISION: Session owns publisher lifecycle and must clean up all resources when exiting
237        if self.publisher.is_running():
238            try:
239                # Signal shutdown by setting the flag - the loop will check this and exit
240                self.publisher._running = False
241
242                # Try to cancel the background task if it exists and isn't done
243                if (
244                    self.publisher.pub_loop is not None
245                    and not self.publisher.pub_loop.done()
246                ):
247                    try:
248                        # Cancel the task - it will check _running and exit naturally
249                        self.publisher.pub_loop.cancel()
250                    except Exception:
251                        # Task might be done or in a different loop, that's okay
252                        pass
253            except Exception:
254                # If shutdown fails for any reason, log it but don't crash
255                logger.warning(
256                    "Failed to shutdown publisher during Session cleanup. "
257                    "This may indicate a resource leak.",
258                    exc_info=True,
259                )
260
261        self.rt_state.shutdown()
262
263        delete_globals()
264        # by deleting all of the state variables we are ensuring that the next time we create a runner it is fresh
265
266    @property
267    def info(self) -> ExecutionInfo:
268        """
269        Returns the current state of the runner.
270
271        This is useful for debugging and viewing the current state of the run.
272        """
273        return self.rt_state.info
274
275    def payload(self) -> Dict[str, Any]:
276        """
277        Gets the complete json payload tied to this session.
278
279        The outputted json schema is maintained in (link here)
280        """
281        info = self.info
282
283        run_list = info.graph_serialization()
284
285        full_dict = {
286            "flow_name": self.flow_name,
287            "flow_id": self.flow_id,
288            "session_id": self._identifier,
289            "session_name": self.name,
290            "start_time": self._start_time,
291            "end_time": time.time(),
292            "runs": run_list,
293        }
294
295        return json.loads(json.dumps(full_dict))

The main class for managing an execution session.

This class is responsible for setting up all the necessary components for running a Railtracks execution, including the coordinator, publisher, and state management.

For the configuration parameters of the setting. It will follow this precedence:

  1. The parameters in the Session constructor.
  2. The parameters in global context variables.
  3. The default values.

Default Values:

  • name: None
  • timeout: 150.0 seconds
  • end_on_error: False
  • broadcast_callback: None (no callback for broadcast messages)
  • prompt_injection: True (the prompt will be automatically injected from context variables)
  • save_state: True (the state of the execution will be saved to a file at the end of the run in the .railtracks/data/sessions/ directory)
Arguments:
  • name (str | None, optional): Optional name for the session. This name will be included in the saved state file if save_state is True.
  • context (Dict[str, Any], optional): A dictionary of global context variables to be used during the execution.
  • flow_name (str | None, optional): The name of the flow this session is associated with.
  • flow_id (str | None, optional): The unique identifier of the flow this session is associated with.
  • timeout (float, optional): The maximum number of seconds to wait for a response to your top-level request.
  • end_on_error (bool, optional): If True, the execution will stop when an exception is encountered.
  • broadcast_callback (Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None, optional): A callback function that will be called with the broadcast messages.
  • prompt_injection (bool, optional): If True, the prompt will be automatically injected from context variables.
  • save_state (bool, optional): If True, the state of the execution will be saved to a file at the end of the run in the .railtracks/data/sessions/ directory.
Session( context: Optional[Dict[str, Any]] = None, *, flow_name: str | None = None, flow_id: str | None = None, name: str | None = None, timeout: float | None = None, end_on_error: bool | None = None, broadcast_callback: Union[Callable[[str], NoneType], Callable[[str], Coroutine[NoneType, NoneType, NoneType]], NoneType] = None, prompt_injection: bool | None = None, save_state: bool | None = None, payload_callback: Optional[Callable[[dict[str, Any]], NoneType]] = None)
 69    def __init__(
 70        self,
 71        context: Dict[str, Any] | None = None,
 72        *,
 73        flow_name: str | None = None,
 74        flow_id: str | None = None,
 75        name: str | None = None,
 76        timeout: float | None = None,
 77        end_on_error: bool | None = None,
 78        broadcast_callback: (
 79            Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
 80        ) = None,
 81        prompt_injection: bool | None = None,
 82        save_state: bool | None = None,
 83        payload_callback: Callable[[dict[str, Any]], None] | None = None,
 84    ):
 85        # first lets read from defaults if nessecary for the provided input config
 86
 87        if flow_name is None:
 88            warnings.warn(
 89                "Sessions should be tied to a flow for better observability and state management. Please use the Flow object to create and manage your sessions (see __ for more details). This warning will become an error in future versions.",
 90                DeprecationWarning,
 91            )
 92
 93        self.executor_config = self.global_config_precedence(
 94            timeout=timeout,
 95            end_on_error=end_on_error,
 96            broadcast_callback=broadcast_callback,
 97            prompt_injection=prompt_injection,
 98            save_state=save_state,
 99            payload_callback=payload_callback,
100        )
101
102        if context is None:
103            context = {}
104
105        self.name = name
106        self.flow_name = flow_name
107        self.flow_id = flow_id
108
109        self.publisher: RTPublisher = RTPublisher()
110
111        self._identifier = str(uuid.uuid4())
112
113        executor_info = ExecutionInfo.create_new()
114        self.coordinator = Coordinator(
115            execution_modes={"async": AsyncioExecutionStrategy()}
116        )
117        self.rt_state = RTState(
118            executor_info, self.executor_config, self.coordinator, self.publisher
119        )
120
121        self.coordinator.start(self.publisher)
122        self._setup_subscriber()
123        register_globals(
124            session_id=self._identifier,
125            rt_publisher=self.publisher,
126            parent_id=None,
127            executor_config=self.executor_config,
128            global_context_vars=context,
129        )
130
131        self._start_time = time.time()
132
133        logger.debug("Session %s is initialized" % self._identifier)
executor_config
name
flow_name
flow_id
publisher: railtracks.pubsub.publisher.RTPublisher
coordinator
rt_state
@classmethod
def global_config_precedence( cls, timeout: float | None, end_on_error: bool | None, broadcast_callback: Union[Callable[[str], NoneType], Callable[[str], Coroutine[NoneType, NoneType, NoneType]], NoneType], prompt_injection: bool | None, save_state: bool | None, payload_callback: Optional[Callable[[dict[str, Any]], NoneType]]) -> ExecutorConfig:
135    @classmethod
136    def global_config_precedence(
137        cls,
138        timeout: float | None,
139        end_on_error: bool | None,
140        broadcast_callback: (
141            Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
142        ),
143        prompt_injection: bool | None,
144        save_state: bool | None,
145        payload_callback: Callable[[dict[str, Any]], None] | None,
146    ) -> ExecutorConfig:
147        """
148        Uses the following precedence order to determine the configuration parameters:
149        1. The parameters in the method parameters.
150        2. The parameters in global context variables.
151        3. The default values.
152        """
153        global_executor_config = get_global_config()
154
155        return global_executor_config.precedence_overwritten(
156            timeout=timeout,
157            end_on_error=end_on_error,
158            subscriber=broadcast_callback,
159            prompt_injection=prompt_injection,
160            save_state=save_state,
161            payload_callback=payload_callback,
162        )

Uses the following precedence order to determine the configuration parameters:

  1. The parameters in the method parameters.
  2. The parameters in global context variables.
  3. The default values.
info: ExecutionInfo
266    @property
267    def info(self) -> ExecutionInfo:
268        """
269        Returns the current state of the runner.
270
271        This is useful for debugging and viewing the current state of the run.
272        """
273        return self.rt_state.info

Returns the current state of the runner.

This is useful for debugging and viewing the current state of the run.

def payload(self) -> Dict[str, Any]:
275    def payload(self) -> Dict[str, Any]:
276        """
277        Gets the complete json payload tied to this session.
278
279        The outputted json schema is maintained in (link here)
280        """
281        info = self.info
282
283        run_list = info.graph_serialization()
284
285        full_dict = {
286            "flow_name": self.flow_name,
287            "flow_id": self.flow_id,
288            "session_id": self._identifier,
289            "session_name": self.name,
290            "start_time": self._start_time,
291            "end_time": time.time(),
292            "runs": run_list,
293        }
294
295        return json.loads(json.dumps(full_dict))

Gets the complete json payload tied to this session.

The outputted json schema is maintained in (link here)

def session( func: Optional[Callable[~_P, Coroutine[Any, Any, ~_TOutput]]] = None, *, name: str | None = None, context: Optional[Dict[str, Any]] = None, timeout: float | None = None, end_on_error: bool | None = None, broadcast_callback: Union[Callable[[str], NoneType], Callable[[str], Coroutine[NoneType, NoneType, NoneType]], NoneType] = None, prompt_injection: bool | None = None, save_state: bool | None = None) -> Union[Callable[~_P, Coroutine[Any, Any, Tuple[~_TOutput, Session]]], Callable[[Callable[~_P, Coroutine[Any, Any, ~_TOutput]]], Callable[~_P, Coroutine[Any, Any, Tuple[~_TOutput, Session]]]]]:
352def session(
353    func: Callable[_P, Coroutine[Any, Any, _TOutput]] | None = None,
354    *,
355    name: str | None = None,
356    context: Dict[str, Any] | None = None,
357    timeout: float | None = None,
358    end_on_error: bool | None = None,
359    broadcast_callback: (
360        Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
361    ) = None,
362    prompt_injection: bool | None = None,
363    save_state: bool | None = None,
364) -> (
365    Callable[_P, Coroutine[Any, Any, Tuple[_TOutput, Session]]]
366    | Callable[
367        [Callable[_P, Coroutine[Any, Any, _TOutput]]],
368        Callable[_P, Coroutine[Any, Any, Tuple[_TOutput, Session]]],
369    ]
370):
371    """
372    This decorator automatically creates and manages a Session context for the decorated function,
373    allowing async functions to use Railtracks operations without manually managing the session lifecycle.
374
375    Can be used as:
376    - @session (without parentheses) - uses default settings
377    - @session() (with empty parentheses) - uses default settings
378    - @session(name="my_task", timeout=30) (with configuration parameters)
379
380    When using this decorator, the function returns a tuple containing:
381    1. The original function's return value
382    2. The Session object used during execution
383
384    This allows access to session information (like execution state, logs, etc.) after the function completes,
385    while maintaining the simplicity of decorator usage.
386
387    Args:
388        name (str | None, optional): Optional name for the session. This name will be included in the saved state file if `save_state` is True.
389        context (Dict[str, Any], optional): A dictionary of global context variables to be used during the execution.
390        timeout (float, optional): The maximum number of seconds to wait for a response to your top-level request.
391        end_on_error (bool, optional): If True, the execution will stop when an exception is encountered.
392        broadcast_callback (Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None, optional): A callback function that will be called with the broadcast messages.
393        prompt_injection (bool, optional): If True, the prompt will be automatically injected from context variables.
394        save_state (bool, optional): If True, the state of the execution will be saved to a file at the end of the run in the `.railtracks/data/sessions/` directory.
395
396    Returns:
397        When used as @session (without parentheses): Returns the decorated function that returns (result, session).
398        When used as @session(...) (with parameters): Returns a decorator function that takes an async function
399        and returns a new async function that returns (result, session).
400    """
401
402    def decorator(
403        target_func: Callable[_P, Coroutine[Any, Any, _TOutput]],
404    ) -> Callable[_P, Coroutine[Any, Any, Tuple[_TOutput, Session]]]:
405        # Validate that the decorated function is async
406        if not inspect.iscoroutinefunction(target_func):
407            raise TypeError(
408                f"@session decorator can only be applied to async functions. "
409                f"Function '{target_func.__name__}' is not async. "
410                f"Add 'async' keyword to your function definition."
411            )
412
413        @wraps(target_func)
414        async def wrapper(
415            *args: _P.args, **kwargs: _P.kwargs
416        ) -> Tuple[_TOutput, Session]:
417            session_obj = Session(
418                context=context,
419                timeout=timeout,
420                end_on_error=end_on_error,
421                broadcast_callback=broadcast_callback,
422                name=name,
423                prompt_injection=prompt_injection,
424                save_state=save_state,
425            )
426
427            with session_obj:
428                result = await target_func(*args, **kwargs)
429                return result, session_obj
430
431        return wrapper
432
433    # If used as @session without parentheses
434    if func is not None:
435        return decorator(func)
436
437    # If used as @session(...)
438    return decorator

This decorator automatically creates and manages a Session context for the decorated function, allowing async functions to use Railtracks operations without manually managing the session lifecycle.

Can be used as:

  • @session (without parentheses) - uses default settings
  • @session() (with empty parentheses) - uses default settings
  • @session(name="my_task", timeout=30) (with configuration parameters)

When using this decorator, the function returns a tuple containing:

  1. The original function's return value
  2. The Session object used during execution

This allows access to session information (like execution state, logs, etc.) after the function completes, while maintaining the simplicity of decorator usage.

Arguments:
  • name (str | None, optional): Optional name for the session. This name will be included in the saved state file if save_state is True.
  • context (Dict[str, Any], optional): A dictionary of global context variables to be used during the execution.
  • timeout (float, optional): The maximum number of seconds to wait for a response to your top-level request.
  • end_on_error (bool, optional): If True, the execution will stop when an exception is encountered.
  • broadcast_callback (Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None, optional): A callback function that will be called with the broadcast messages.
  • prompt_injection (bool, optional): If True, the prompt will be automatically injected from context variables.
  • save_state (bool, optional): If True, the state of the execution will be saved to a file at the end of the run in the .railtracks/data/sessions/ directory.
Returns:

When used as @session (without parentheses): Returns the decorated function that returns (result, session). When used as @session(...) (with parameters): Returns a decorator function that takes an async function and returns a new async function that returns (result, session).

async def call( node_: Union[Callable[~_P, railtracks.nodes.nodes.Node[~_TOutput]], railtracks.built_nodes.concrete.function_base.RTFunction[~_P, ~_TOutput]], *args: _P.args, **kwargs: _P.kwargs) -> ~_TOutput:
 59async def call(
 60    node_: Callable[_P, Node[_TOutput]] | RTFunction[_P, _TOutput],
 61    *args: _P.args,
 62    **kwargs: _P.kwargs,
 63) -> _TOutput:
 64    """
 65    Call a node from within a node inside the framework. This will return a coroutine that you can interact with
 66    in whatever way using async/await logic.
 67
 68    Usage:
 69    ```python
 70    # for sequential operation
 71    result = await call(NodeA, "hello world", 42)
 72
 73    # for parallel operation
 74    tasks = [call(NodeA, "hello world", i) for i in range(10)]
 75    results = await asyncio.gather(*tasks)
 76    ```
 77
 78    Args:
 79        node: The node type you would like to create. This could be a function decorated with `@function_node`, a function, or a Node instance.
 80        *args: The arguments to pass to the node
 81        **kwargs: The keyword arguments to pass to the node
 82    """
 83    node: Callable[_P, Node[_TOutput]]
 84    # this entire section is a bit of a typing nightmare becuase all overloads we provide.
 85    if isinstance(node_, FunctionType):
 86        node = extract_node_from_function(node_)
 87    else:
 88        node = node_
 89    # if the context is none then we will need to create a wrapper for the state object to work with.
 90    if not is_context_present():
 91        # we have to use lazy import here to prevent a circular import issue. This is a must have unfortunately.
 92        from railtracks import Session
 93
 94        with Session():
 95            result = await _start(node, args=args, kwargs=kwargs)
 96            return result
 97
 98    # if the context is not active then we know this is the top level request
 99    if not is_context_active():
100        result = await _start(node, args=args, kwargs=kwargs)
101        return result
102
103    # if the context is active then we can just run the node
104    result = await _run(node, args=args, kwargs=kwargs)
105    return result

Call a node from within a node inside the framework. This will return a coroutine that you can interact with in whatever way using async/await logic.

Usage:

# for sequential operation
result = await call(NodeA, "hello world", 42)

# for parallel operation
tasks = [call(NodeA, "hello world", i) for i in range(10)]
results = await asyncio.gather(*tasks)
Arguments:
  • node: The node type you would like to create. This could be a function decorated with @function_node, a function, or a Node instance.
  • *args: The arguments to pass to the node
  • **kwargs: The keyword arguments to pass to the node
async def broadcast(item: str):
 6async def broadcast(item: str):
 7    """
 8    Streams the given message
 9
10    This will trigger the broadcast_callback callback you have already provided.
11
12    Args:
13        item (str): The item you want to stream.
14    """
15    publisher = get_publisher()
16
17    await publisher.publish(Streaming(node_id=get_parent_id(), streamed_object=item))

Streams the given message

This will trigger the broadcast_callback callback you have already provided.

Arguments:
  • item (str): The item you want to stream.
async def call_batch( node: Union[Callable[..., railtracks.nodes.nodes.Node[~_TOutput]], Callable[..., ~_TOutput]], *iterables: Iterable[Any], return_exceptions: bool = True):
21async def call_batch(
22    node: Callable[..., Node[_TOutput]] | Callable[..., _TOutput],
23    *iterables: Iterable[Any],
24    return_exceptions: bool = True,
25):
26    """
27    Complete a node over multiple iterables, allowing for parallel execution.
28
29    Note the results will be returned in the order of the iterables, not the order of completion.
30
31    If one of the nodes returns an exception, the thrown exception will be included as a response.
32
33    Args:
34        node: The node type to create.
35        *iterables: The iterables to map the node over.
36        return_exceptions: If True, exceptions will be returned as part of the results.
37            If False, exceptions will be raised immediately, and you will lose access to the results.
38            Defaults to true.
39
40    Returns:
41        An iterable of results from the node.
42
43    Usage:
44        ```python
45        results = await batch(NodeA, ["hello world"] * 10)
46        for result in results:
47            handle(result)
48        ```
49    """
50    # this is big typing disaster but there is no way around it. Try if if you want to.
51    contracts = [call(node, *args) for args in zip(*iterables)]
52
53    results = await asyncio.gather(*contracts, return_exceptions=return_exceptions)
54    return results

Complete a node over multiple iterables, allowing for parallel execution.

Note the results will be returned in the order of the iterables, not the order of completion.

If one of the nodes returns an exception, the thrown exception will be included as a response.

Arguments:
  • node: The node type to create.
  • *iterables: The iterables to map the node over.
  • return_exceptions: If True, exceptions will be returned as part of the results. If False, exceptions will be raised immediately, and you will lose access to the results. Defaults to true.
Returns:

An iterable of results from the node.

Usage:
results = await batch(NodeA, ["hello world"] * 10)
for result in results:
    handle(result)
class ExecutionInfo:
 19class ExecutionInfo:
 20    """
 21    A class that contains the full details of the state of a run at any given point in time.
 22
 23    The class is designed to be used as a snapshot of state that can be used to display the state of the run, or to
 24    create a graphical representation of the system.
 25    """
 26
 27    def __init__(
 28        self,
 29        request_forest: RequestForest,
 30        node_forest: NodeForest,
 31        stamper: StampManager,
 32    ):
 33        self.request_forest = request_forest
 34        self.node_forest = node_forest
 35        self.stamper = stamper
 36
 37    @classmethod
 38    def default(cls) -> ExecutionInfo:
 39        """Creates a new "empty" instance of the ExecutionInfo class with the default values."""
 40        return cls.create_new()
 41
 42    @classmethod
 43    def create_new(
 44        cls,
 45    ) -> ExecutionInfo:
 46        """
 47        Creates a new empty instance of state variables with the provided executor configuration.
 48
 49        """
 50        request_heap = RequestForest()
 51        node_heap = NodeForest()
 52        stamper = StampManager()
 53
 54        return ExecutionInfo(
 55            request_forest=request_heap,
 56            node_forest=node_heap,
 57            stamper=stamper,
 58        )
 59
 60    @property
 61    def answer(self):
 62        """Convenience method to access the answer of the run."""
 63        return self.request_forest.answer
 64
 65    @property
 66    def all_stamps(self) -> List[Stamp]:
 67        """Convenience method to access all the stamps of the run."""
 68        return self.stamper.all_stamps
 69
 70    @property
 71    def name(self):
 72        """
 73        Gets the name of the graph by pulling the name of the insertion request. It will raise a ValueError if the insertion
 74        request is not present or there are multiple insertion requests.
 75        """
 76        insertion_requests = self.insertion_requests
 77
 78        # The name is only defined for the length of 1.
 79        # NOTE: Maybe we should send a warning once to user in other cases.
 80        if len(insertion_requests) != 1:
 81            return None
 82
 83        i_r = insertion_requests[0]
 84
 85        return self.node_forest.get_node_type(i_r.sink_id).name()
 86
 87    @property
 88    def insertion_requests(self):
 89        """A convenience method to access all the insertion requests of the run."""
 90        return self.request_forest.insertion_request
 91
 92    def _get_info(self, ids: List[str] | str | None = None) -> ExecutionInfo:
 93        """
 94        Gets a subset of the current state based on the provided node ids. It will contain all the children of the provided node ids
 95
 96        Note: If no ids are provided, the full state is returned.
 97
 98        Args:
 99            ids (List[str] | str | None): A list of node ids to filter the state by. If None, the full state is returned.
100
101        Returns:
102            ExecutionInfo: A new instance of ExecutionInfo containing only the children of the provided ids.
103
104        """
105        if ids is None:
106            return self
107        else:
108            # firstly lets
109            if isinstance(ids, str):
110                ids = [ids]
111
112            # we need to quickly check to make sure these ids are valid
113            for identifier in ids:
114                if identifier not in self.request_forest:
115                    raise ValueError(
116                        f"Identifier '{identifier}' not found in the current state."
117                    )
118
119            new_node_forest, new_request_forest = create_sub_state_info(
120                self.node_forest.heap(),
121                self.request_forest.heap(),
122                ids,
123            )
124            return ExecutionInfo(
125                node_forest=new_node_forest,
126                request_forest=new_request_forest,
127                stamper=self.stamper,
128            )
129
130    def _to_graph(self) -> Tuple[List[Vertex], List[Edge]]:
131        """
132        Converts the current state into its graph representation.
133
134        Returns:
135            List[Node]: An iterable of nodes in the graph.
136            List[Edge]: An iterable of edges in the graph.
137        """
138        return self.node_forest.to_vertices(), self.request_forest.to_edges()
139
140    def graph_serialization(self) -> dict[str, Any]:
141        """
142                Creates a string (JSON) representation of this info object designed to be used to construct a graph for this
143                info object.
144
145                Some important notes about its structure are outlined below:
146                - The `nodes` key contains a list of all the nodes in the graph, represented as `Vertex` objects.
147                - The `edges` key contains a list of all the edges in the graph, represented as `Edge` objects.
148                - The `stamps` key contains an ease of use list of all the stamps associated with the run, represented as `Stamp` objects.
149
150                - The "nodes" and "requests" key will be outlined with normal graph details like connections and identifiers in addition to a loose details object.
151                - However, both will carry an addition param called "stamp" which is a timestamp style object.
152                - They also will carry a "parent" param which is a recursive structure that allows you to traverse the graph in time.
153
154
155        ```
156        """
157        parent_nodes = [x.identifier for x in self.insertion_requests]
158
159        infos = [self._get_info(parent_node) for parent_node in parent_nodes]
160
161        runs = []
162
163        for info, parent_node_id in zip(infos, parent_nodes):
164            insertion_requests = info.request_forest.insertion_request
165
166            assert len(insertion_requests) == 1
167            parent_request = insertion_requests[0]
168
169            all_parents = parent_request.get_all_parents()
170
171            start_time = all_parents[-1].stamp.time
172
173            assert len([x for x in all_parents if x.status == "Completed"]) <= 1
174            end_time = None
175            for req in all_parents:
176                if req.status in ["Completed", "Failed"]:
177                    end_time = req.stamp.time
178                    break
179
180            entry = {
181                "name": info.name,
182                "run_id": parent_node_id,
183                "nodes": info.node_forest.to_vertices(),
184                "status": parent_request.status,
185                "edges": info.request_forest.to_edges(),
186                "steps": _get_stamps_from_forests(
187                    info.node_forest, info.request_forest
188                ),
189                "start_time": start_time,
190                "end_time": end_time,
191            }
192            runs.append(entry)
193
194        return json.loads(
195            json.dumps(
196                runs,
197                cls=RTJSONEncoder,
198            )
199        )

A class that contains the full details of the state of a run at any given point in time.

The class is designed to be used as a snapshot of state that can be used to display the state of the run, or to create a graphical representation of the system.

ExecutionInfo( request_forest: railtracks.state.request.RequestForest, node_forest: railtracks.state.node.NodeForest, stamper: railtracks.utils.profiling.StampManager)
27    def __init__(
28        self,
29        request_forest: RequestForest,
30        node_forest: NodeForest,
31        stamper: StampManager,
32    ):
33        self.request_forest = request_forest
34        self.node_forest = node_forest
35        self.stamper = stamper
request_forest
node_forest
stamper
@classmethod
def default(cls) -> ExecutionInfo:
37    @classmethod
38    def default(cls) -> ExecutionInfo:
39        """Creates a new "empty" instance of the ExecutionInfo class with the default values."""
40        return cls.create_new()

Creates a new "empty" instance of the ExecutionInfo class with the default values.

@classmethod
def create_new(cls) -> ExecutionInfo:
42    @classmethod
43    def create_new(
44        cls,
45    ) -> ExecutionInfo:
46        """
47        Creates a new empty instance of state variables with the provided executor configuration.
48
49        """
50        request_heap = RequestForest()
51        node_heap = NodeForest()
52        stamper = StampManager()
53
54        return ExecutionInfo(
55            request_forest=request_heap,
56            node_forest=node_heap,
57            stamper=stamper,
58        )

Creates a new empty instance of state variables with the provided executor configuration.

answer
60    @property
61    def answer(self):
62        """Convenience method to access the answer of the run."""
63        return self.request_forest.answer

Convenience method to access the answer of the run.

all_stamps: List[railtracks.utils.profiling.Stamp]
65    @property
66    def all_stamps(self) -> List[Stamp]:
67        """Convenience method to access all the stamps of the run."""
68        return self.stamper.all_stamps

Convenience method to access all the stamps of the run.

name
70    @property
71    def name(self):
72        """
73        Gets the name of the graph by pulling the name of the insertion request. It will raise a ValueError if the insertion
74        request is not present or there are multiple insertion requests.
75        """
76        insertion_requests = self.insertion_requests
77
78        # The name is only defined for the length of 1.
79        # NOTE: Maybe we should send a warning once to user in other cases.
80        if len(insertion_requests) != 1:
81            return None
82
83        i_r = insertion_requests[0]
84
85        return self.node_forest.get_node_type(i_r.sink_id).name()

Gets the name of the graph by pulling the name of the insertion request. It will raise a ValueError if the insertion request is not present or there are multiple insertion requests.

insertion_requests
87    @property
88    def insertion_requests(self):
89        """A convenience method to access all the insertion requests of the run."""
90        return self.request_forest.insertion_request

A convenience method to access all the insertion requests of the run.

def graph_serialization(self) -> dict[str, typing.Any]:
140    def graph_serialization(self) -> dict[str, Any]:
141        """
142                Creates a string (JSON) representation of this info object designed to be used to construct a graph for this
143                info object.
144
145                Some important notes about its structure are outlined below:
146                - The `nodes` key contains a list of all the nodes in the graph, represented as `Vertex` objects.
147                - The `edges` key contains a list of all the edges in the graph, represented as `Edge` objects.
148                - The `stamps` key contains an ease of use list of all the stamps associated with the run, represented as `Stamp` objects.
149
150                - The "nodes" and "requests" key will be outlined with normal graph details like connections and identifiers in addition to a loose details object.
151                - However, both will carry an addition param called "stamp" which is a timestamp style object.
152                - They also will carry a "parent" param which is a recursive structure that allows you to traverse the graph in time.
153
154
155        ```
156        """
157        parent_nodes = [x.identifier for x in self.insertion_requests]
158
159        infos = [self._get_info(parent_node) for parent_node in parent_nodes]
160
161        runs = []
162
163        for info, parent_node_id in zip(infos, parent_nodes):
164            insertion_requests = info.request_forest.insertion_request
165
166            assert len(insertion_requests) == 1
167            parent_request = insertion_requests[0]
168
169            all_parents = parent_request.get_all_parents()
170
171            start_time = all_parents[-1].stamp.time
172
173            assert len([x for x in all_parents if x.status == "Completed"]) <= 1
174            end_time = None
175            for req in all_parents:
176                if req.status in ["Completed", "Failed"]:
177                    end_time = req.stamp.time
178                    break
179
180            entry = {
181                "name": info.name,
182                "run_id": parent_node_id,
183                "nodes": info.node_forest.to_vertices(),
184                "status": parent_request.status,
185                "edges": info.request_forest.to_edges(),
186                "steps": _get_stamps_from_forests(
187                    info.node_forest, info.request_forest
188                ),
189                "start_time": start_time,
190                "end_time": end_time,
191            }
192            runs.append(entry)
193
194        return json.loads(
195            json.dumps(
196                runs,
197                cls=RTJSONEncoder,
198            )
199        )

Creates a string (JSON) representation of this info object designed to be used to construct a graph for this info object.

    Some important notes about its structure are outlined below:
    - The `nodes` key contains a list of all the nodes in the graph, represented as `Vertex` objects.
    - The `edges` key contains a list of all the edges in the graph, represented as `Edge` objects.
    - The `stamps` key contains an ease of use list of all the stamps associated with the run, represented as `Stamp` objects.

    - The "nodes" and "requests" key will be outlined with normal graph details like connections and identifiers in addition to a loose details object.
    - However, both will carry an addition param called "stamp" which is a timestamp style object.
    - They also will carry a "parent" param which is a recursive structure that allows you to traverse the graph in time.

```

class ExecutorConfig:
 8class ExecutorConfig:
 9    def __init__(
10        self,
11        *,
12        timeout: float | None = None,
13        end_on_error: bool = False,
14        broadcast_callback: (
15            Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
16        ) = None,
17        prompt_injection: bool = True,
18        save_state: bool = True,
19        payload_callback: Callable[[dict[str, Any]], None] | None = None,
20    ):
21        """
22        ExecutorConfig is special configuration object designed to allow customization of the executor in the RT system.
23
24        Args:
25            timeout (float | None): The maximum number of seconds to wait for a response to your top level request. Pass None (or omit) to disable the timeout entirely.
26            end_on_error (bool): If true, the executor will stop execution when an exception is encountered.
27            broadcast_callback (Callable or Coroutine): A function or coroutine that receives items published with `rt.broadcast`.
28            prompt_injection (bool): If true, prompts can be injected with global context
29            save_state (bool): If true, the state of the executor will be saved to disk.
30        """
31        self.timeout = timeout
32        self.end_on_error = end_on_error
33        self.subscriber = broadcast_callback
34        self.prompt_injection = prompt_injection
35        # During test runs, disable save_state by default unless RAILTRACKS_ALLOW_PERSISTENCE is set
36        self._user_save_state = save_state
37
38        self.payload_callback = payload_callback
39
40    # this is done because if we try to lock the save_state in init
41    # later when we want to allow a few tests to actually run persistance, they wont be able to do so
42    @property
43    def save_state(self) -> bool:
44        if os.getenv("RAILTRACKS_TEST_MODE") and not os.getenv(
45            "RAILTRACKS_ALLOW_PERSISTENCE"
46        ):
47            return False
48        return self._user_save_state
49
50    def precedence_overwritten(
51        self,
52        *,
53        timeout: float | None = None,
54        end_on_error: bool | None = None,
55        subscriber: (
56            Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
57        ) = None,
58        prompt_injection: bool | None = None,
59        save_state: bool | None = None,
60        payload_callback: Callable[[dict[str, Any]], None] | None = None,
61    ):
62        """
63        If any of the parameters are provided (not None), it will create a new update the current instance with the new values and return a deep copied reference to it.
64        """
65        return ExecutorConfig(
66            timeout=timeout,
67            end_on_error=end_on_error
68            if end_on_error is not None
69            else self.end_on_error,
70            broadcast_callback=subscriber
71            if subscriber is not None
72            else self.subscriber,
73            prompt_injection=prompt_injection
74            if prompt_injection is not None
75            else self.prompt_injection,
76            save_state=save_state if save_state is not None else self.save_state,
77            payload_callback=payload_callback
78            if payload_callback is not None
79            else self.payload_callback,
80        )
81
82    def __repr__(self):
83        return (
84            f"ExecutorConfig(timeout={self.timeout}, end_on_error={self.end_on_error}, "
85            f"prompt_injection={self.prompt_injection}, "
86            f"save_state={self.save_state}, payload_callback={self.payload_callback})"
87        )
ExecutorConfig( *, timeout: float | None = None, end_on_error: bool = False, broadcast_callback: Union[Callable[[str], NoneType], Callable[[str], Coroutine[NoneType, NoneType, NoneType]], NoneType] = None, prompt_injection: bool = True, save_state: bool = True, payload_callback: Optional[Callable[[dict[str, Any]], NoneType]] = None)
 9    def __init__(
10        self,
11        *,
12        timeout: float | None = None,
13        end_on_error: bool = False,
14        broadcast_callback: (
15            Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
16        ) = None,
17        prompt_injection: bool = True,
18        save_state: bool = True,
19        payload_callback: Callable[[dict[str, Any]], None] | None = None,
20    ):
21        """
22        ExecutorConfig is special configuration object designed to allow customization of the executor in the RT system.
23
24        Args:
25            timeout (float | None): The maximum number of seconds to wait for a response to your top level request. Pass None (or omit) to disable the timeout entirely.
26            end_on_error (bool): If true, the executor will stop execution when an exception is encountered.
27            broadcast_callback (Callable or Coroutine): A function or coroutine that receives items published with `rt.broadcast`.
28            prompt_injection (bool): If true, prompts can be injected with global context
29            save_state (bool): If true, the state of the executor will be saved to disk.
30        """
31        self.timeout = timeout
32        self.end_on_error = end_on_error
33        self.subscriber = broadcast_callback
34        self.prompt_injection = prompt_injection
35        # During test runs, disable save_state by default unless RAILTRACKS_ALLOW_PERSISTENCE is set
36        self._user_save_state = save_state
37
38        self.payload_callback = payload_callback

ExecutorConfig is special configuration object designed to allow customization of the executor in the RT system.

Arguments:
  • timeout (float | None): The maximum number of seconds to wait for a response to your top level request. Pass None (or omit) to disable the timeout entirely.
  • end_on_error (bool): If true, the executor will stop execution when an exception is encountered.
  • broadcast_callback (Callable or Coroutine): A function or coroutine that receives items published with rt.broadcast.
  • prompt_injection (bool): If true, prompts can be injected with global context
  • save_state (bool): If true, the state of the executor will be saved to disk.
timeout
end_on_error
subscriber
prompt_injection
payload_callback
save_state: bool
42    @property
43    def save_state(self) -> bool:
44        if os.getenv("RAILTRACKS_TEST_MODE") and not os.getenv(
45            "RAILTRACKS_ALLOW_PERSISTENCE"
46        ):
47            return False
48        return self._user_save_state
def precedence_overwritten( self, *, timeout: float | None = None, end_on_error: bool | None = None, subscriber: Union[Callable[[str], NoneType], Callable[[str], Coroutine[NoneType, NoneType, NoneType]], NoneType] = None, prompt_injection: bool | None = None, save_state: bool | None = None, payload_callback: Optional[Callable[[dict[str, Any]], NoneType]] = None):
50    def precedence_overwritten(
51        self,
52        *,
53        timeout: float | None = None,
54        end_on_error: bool | None = None,
55        subscriber: (
56            Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
57        ) = None,
58        prompt_injection: bool | None = None,
59        save_state: bool | None = None,
60        payload_callback: Callable[[dict[str, Any]], None] | None = None,
61    ):
62        """
63        If any of the parameters are provided (not None), it will create a new update the current instance with the new values and return a deep copied reference to it.
64        """
65        return ExecutorConfig(
66            timeout=timeout,
67            end_on_error=end_on_error
68            if end_on_error is not None
69            else self.end_on_error,
70            broadcast_callback=subscriber
71            if subscriber is not None
72            else self.subscriber,
73            prompt_injection=prompt_injection
74            if prompt_injection is not None
75            else self.prompt_injection,
76            save_state=save_state if save_state is not None else self.save_state,
77            payload_callback=payload_callback
78            if payload_callback is not None
79            else self.payload_callback,
80        )

If any of the parameters are provided (not None), it will create a new update the current instance with the new values and return a deep copied reference to it.

def set_config( *, timeout: float | None = None, end_on_error: bool | None = None, broadcast_callback: Union[Callable[[str], NoneType], Callable[[str], Coroutine[NoneType, NoneType, NoneType]], NoneType] = None, prompt_injection: bool | None = None, save_state: bool | None = None) -> None:
365def set_config(
366    *,
367    timeout: float | None = None,
368    end_on_error: bool | None = None,
369    broadcast_callback: (
370        Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
371    ) = None,
372    prompt_injection: bool | None = None,
373    save_state: bool | None = None,
374) -> None:
375    """
376    Sets the global configuration for the executor. This will be propagated to all new runners created after this call.
377
378    - If you call this function after the runner has been created, it will not affect the current runner.
379    - This function will only overwrite the values that are provided, leaving the rest unchanged.
380
381
382    """
383
384    if is_context_active():
385        warnings.warn(
386            "The executor config is being set after the runner has been created, this is not recommended"
387        )
388
389    config = global_executor_config.get()
390
391    new_config = config.precedence_overwritten(
392        timeout=timeout,
393        end_on_error=end_on_error,
394        subscriber=broadcast_callback,
395        prompt_injection=prompt_injection,
396        save_state=save_state,
397    )
398
399    global_executor_config.set(new_config)

Sets the global configuration for the executor. This will be propagated to all new runners created after this call.

  • If you call this function after the runner has been created, it will not affect the current runner.
  • This function will only overwrite the values that are provided, leaving the rest unchanged.
def function_node( func: Union[Callable[~_P, Union[Coroutine[NoneType, NoneType, ~_TOutput], ~_TOutput]], List[Callable[~_P, Union[Coroutine[NoneType, NoneType, ~_TOutput], ~_TOutput]]]], /, *, name: str | None = None, manifest: ToolManifest | None = None) -> Union[Callable[~_P, Union[Coroutine[NoneType, NoneType, ~_TOutput], ~_TOutput]], List[Callable[~_P, Union[Coroutine[NoneType, NoneType, ~_TOutput], ~_TOutput]]], NoneType]:
173def function_node(
174    func: Callable[_P, Coroutine[None, None, _TOutput] | _TOutput]
175    | List[Callable[_P, Coroutine[None, None, _TOutput] | _TOutput]],
176    /,
177    *,
178    name: str | None = None,
179    manifest: ToolManifest | None = None,
180) -> (
181    Callable[_P, Coroutine[None, None, _TOutput] | _TOutput]
182    | List[Callable[_P, Coroutine[None, None, _TOutput] | _TOutput]]
183    | None
184):
185    """
186    Creates a new Node type from a function that can be used in `rt.call()`.
187
188    By default, it will parse the function's docstring and turn them into tool details and parameters. However, if
189    you provide custom ToolManifest it will override that logic.
190
191    WARNING: If you overriding tool parameters. It is on you to make sure they will work with your function.
192
193    NOTE: If you have already converted this function to a node this function will do nothing
194
195    Args:
196        func (Callable): The function to convert into a Node.
197        name (str, optional): Human-readable name for the node/tool.
198        manifest (ToolManifest, optional): The details you would like to override the tool with.
199    """
200
201    # handle the case where a list of functions is provided
202    if isinstance(func, list):
203        return [function_node(f, name=name, manifest=manifest) for f in func]
204
205    # check if the function has already been converted to a node
206    if hasattr(func, "node_type"):
207        warnings.warn(
208            "The provided function has already been converted to a node.",
209            UserWarning,
210        )
211        return func
212
213    # validate_function_parameters is separated out to allow for easier testing.
214    validate_function_parameters(func, manifest)
215
216    # assign the correct node class based on whether the function is async or sync
217    if asyncio.iscoroutinefunction(func):
218        if inspect.ismethod(func):
219            func = _function_preserving_metadata(func)
220        node_class = AsyncDynamicFunctionNode
221    elif inspect.isfunction(func):
222        node_class = SyncDynamicFunctionNode
223    elif inspect.ismethod(func):
224        # Bound methods can't hold attributes, so wrap in a plain function first.
225        # functools.wraps preserves __name__, __doc__, and __wrapped__ for the NodeBuilder.
226        func = _function_preserving_metadata(func)
227        node_class = SyncDynamicFunctionNode
228    elif inspect.isbuiltin(func):
229        # builtin functions are written in C and do not have space for the addition of metadata like our node type.
230        # so instead we wrap them in a function that allows for the addition of the node type.
231        # this logic preserved details like the function name, docstring, and signature, but allows us to add the node type.
232        func = _function_preserving_metadata(func)
233        node_class = SyncDynamicFunctionNode
234    else:
235        raise NodeCreationError(
236            message=f"The provided function is not a valid coroutine or sync function it is {type(func)}.",
237            notes=[
238                "You must provide a valid function or coroutine function to make a node.",
239            ],
240        )
241
242    # build the node using the NodeBuilder
243    builder = NodeBuilder(
244        node_class,
245        name=name if name is not None else f"{func.__name__}",
246    )
247
248    builder.setup_function_node(
249        func,
250        tool_details=manifest.description if manifest is not None else None,
251        tool_params=manifest.parameters if manifest is not None else None,
252    )
253
254    completed_node_type = builder.build()
255
256    # there is some pretty scary logic here.
257    if issubclass(completed_node_type, AsyncDynamicFunctionNode):
258        setattr(func, "node_type", completed_node_type)
259        return func
260    elif issubclass(completed_node_type, SyncDynamicFunctionNode):
261        setattr(func, "node_type", completed_node_type)
262        return func
263    else:
264        raise NodeCreationError(
265            message="The provided function did not create a valid node type.",
266            notes=[
267                "Please make a github issue with the details of what went wrong.",
268            ],
269        )

Creates a new Node type from a function that can be used in rt.call().

By default, it will parse the function's docstring and turn them into tool details and parameters. However, if you provide custom ToolManifest it will override that logic.

WARNING: If you overriding tool parameters. It is on you to make sure they will work with your function.

NOTE: If you have already converted this function to a node this function will do nothing

Arguments:
  • func (Callable): The function to convert into a Node.
  • name (str, optional): Human-readable name for the node/tool.
  • manifest (ToolManifest, optional): The details you would like to override the tool with.
def agent_node( name: str | None = None, *, tool_nodes: Optional[Iterable[Union[Type[railtracks.nodes.nodes.Node], Callable, railtracks.built_nodes.concrete.function_base.RTFunction]]] = None, output_schema: Optional[Type[~_TBaseModel]] = None, llm: Optional[railtracks.llm.ModelBase[~_TStream]] = None, system_message: railtracks.llm.SystemMessage | str | None = None, manifest: ToolManifest | None = None, guardrails: railtracks.guardrails.core.config.Guard | None = None):
305def agent_node(
306    name: str | None = None,
307    *,
308    tool_nodes: Iterable[Type[Node] | Callable | RTFunction] | None = None,
309    output_schema: Type[_TBaseModel] | None = None,
310    llm: ModelBase[_TStream] | None = None,
311    system_message: SystemMessage | str | None = None,
312    manifest: ToolManifest | None = None,
313    guardrails: Guard | None = None,
314):
315    """
316    Dynamically creates an agent based on the provided parameters.
317
318    Args:
319        name (str | None): The name of the agent. If none the default will be used.
320        tool_nodes (set[Type[Node] | Callable | RTFunction] | None): If your agent is a LLM with access to tools, what does it have access to?
321        output_schema (Type[_TBaseModel] | None): If your agent should return a structured output, what is the output_schema?
322        llm (ModelBase): The LLM model to use. If None it will need to be passed in at instance time.
323            Deferring the model this way is going away: `llm` becomes a required keyword in
324            railtracks 1.5.0.
325        system_message (SystemMessage | str | None): System message for the agent.
326        manifest (ToolManifest | None): If you want to use this as a tool in other agents you can pass in a ToolManifest.
327        guardrails (Guard | None): Guardrail config. When provided, the agent runs input/output guardrails.
328            Removed in railtracks 1.5.0, where guards attach as model middleware instead.
329    """
330    if guardrails is not None:
331        warn_pending_change(
332            "The `guardrails=` argument of agent_node",
333            change="is removed",
334            detail=(
335                "Guards attach as model middleware in 1.5.0. Writing a guard is "
336                "unchanged; only the way it is attached to the agent changes."
337            ),
338        )
339
340    if llm is None:
341        warn_pending_change(
342            "Creating an agent without an `llm`",
343            change="stops working",
344            detail=(
345                "Pass `llm=` when building the agent; it becomes a required keyword. "
346                "If you rely on supplying the model at instance time, 1.5.0 replaces "
347                "that with a zero-argument model factory."
348            ),
349        )
350
351    unpacked_tool_nodes = _unpack_tool_nodes(tool_nodes)
352
353    # See issue (___) this logic should be migrated soon.
354    if manifest is not None:
355        tool_details = manifest.description
356        tool_params = manifest.parameters
357    else:
358        tool_details = None
359        tool_params = None
360
361    return _build_dynamic_agent(
362        unpacked_tool_nodes=unpacked_tool_nodes,
363        output_schema=output_schema,
364        name=name,
365        llm=llm,
366        system_message=system_message,
367        tool_details=tool_details,
368        tool_params=tool_params,
369        guardrails=guardrails,
370    )

Dynamically creates an agent based on the provided parameters.

Arguments:
  • name (str | None): The name of the agent. If none the default will be used.
  • tool_nodes (set[Type[Node] | Callable | RTFunction] | None): If your agent is a LLM with access to tools, what does it have access to?
  • output_schema (Type[_TBaseModel] | None): If your agent should return a structured output, what is the output_schema?
  • llm (ModelBase): The LLM model to use. If None it will need to be passed in at instance time. Deferring the model this way is going away: llm becomes a required keyword in railtracks 1.5.0.
  • system_message (SystemMessage | str | None): System message for the agent.
  • manifest (ToolManifest | None): If you want to use this as a tool in other agents you can pass in a ToolManifest.
  • guardrails (Guard | None): Guardrail config. When provided, the agent runs input/output guardrails. Removed in railtracks 1.5.0, where guards attach as model middleware instead.
class MCPStdioParams(mcp.client.stdio.StdioServerParameters):
19class MCPStdioParams(StdioServerParameters):
20    """
21    Configuration parameters for STDIO-based MCP server connections.
22
23    Extends the standard StdioServerParameters with a timeout field.
24
25    Attributes:
26        timeout: Maximum time to wait for operations (default: 30 seconds)
27    """
28
29    timeout: timedelta = timedelta(seconds=30)
30
31    def as_stdio_params(self) -> StdioServerParameters:
32        """
33        Convert to standard StdioServerParameters, excluding the timeout field.
34
35        Returns:
36            StdioServerParameters without the timeout attribute
37        """
38        stdio_kwargs = self.dict(exclude={"timeout"})
39        return StdioServerParameters(**stdio_kwargs)

Configuration parameters for STDIO-based MCP server connections.

Extends the standard StdioServerParameters with a timeout field.

Attributes:
  • timeout: Maximum time to wait for operations (default: 30 seconds)
timeout: datetime.timedelta
def as_stdio_params(self) -> mcp.client.stdio.StdioServerParameters:
31    def as_stdio_params(self) -> StdioServerParameters:
32        """
33        Convert to standard StdioServerParameters, excluding the timeout field.
34
35        Returns:
36            StdioServerParameters without the timeout attribute
37        """
38        stdio_kwargs = self.dict(exclude={"timeout"})
39        return StdioServerParameters(**stdio_kwargs)

Convert to standard StdioServerParameters, excluding the timeout field.

Returns:

StdioServerParameters without the timeout attribute

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class MCPHttpParams(pydantic.main.BaseModel):
42class MCPHttpParams(BaseModel):
43    """
44    Configuration parameters for HTTP-based MCP server connections.
45
46    Supports both SSE (Server-Sent Events) and streamable HTTP transports.
47    The transport type is automatically determined based on the URL.
48
49    Attributes:
50        url: The MCP server URL (use /sse suffix for SSE transport)
51        headers: Optional HTTP headers for authentication
52        timeout: Connection timeout (default: 30 seconds)
53        sse_read_timeout: SSE read timeout (default: 5 minutes)
54        terminate_on_close: Whether to terminate connection on close (default: True)
55    """
56
57    url: str
58    headers: dict[str, Any] | None = None
59    timeout: timedelta = timedelta(seconds=30)
60    sse_read_timeout: timedelta = timedelta(seconds=60 * 5)
61    terminate_on_close: bool = True

Configuration parameters for HTTP-based MCP server connections.

Supports both SSE (Server-Sent Events) and streamable HTTP transports. The transport type is automatically determined based on the URL.

Attributes:
  • url: The MCP server URL (use /sse suffix for SSE transport)
  • headers: Optional HTTP headers for authentication
  • timeout: Connection timeout (default: 30 seconds)
  • sse_read_timeout: SSE read timeout (default: 5 minutes)
  • terminate_on_close: Whether to terminate connection on close (default: True)
url: str
headers: dict[str, typing.Any] | None
timeout: datetime.timedelta
sse_read_timeout: datetime.timedelta
terminate_on_close: bool
model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

def connect_mcp( config: MCPStdioParams | MCPHttpParams, client_session: mcp.client.session.ClientSession | None = None, setup_timeout: float = 30) -> railtracks.rt_mcp.main.MCPServer:
 8def connect_mcp(
 9    config: MCPStdioParams | MCPHttpParams,
10    client_session: ClientSession | None = None,
11    setup_timeout: float = 30,
12) -> MCPServer:
13    """
14    Connect to an MCP server and return a server instance with available tools.
15
16    This is the primary entry point for using MCP servers in Railtracks.
17    The server will connect in the background, discover available tools,
18    and convert them to Railtracks Node classes.
19
20    The connection remains active until explicitly closed or the context exits.
21
22    Usage Examples:
23        # STDIO connection (local MCP server)
24        config = rt.MCPStdioParams(
25            command="uvx",
26            args=["mcp-server-time"]
27        )
28        server = rt.connect_mcp(config)
29
30        # HTTP connection (remote MCP server)
31        config = rt.MCPHttpParams(
32            url="https://mcp.example.com/sse",
33            headers={"Authorization": "Bearer token"}
34        )
35        server = rt.connect_mcp(config)
36
37        # Context manager (recommended)
38        with rt.connect_mcp(config) as server:
39            tools = server.tools
40            # Use tools...
41        # Automatically closed
42
43        # Access tools
44        for tool in server.tools:
45            print(f"Tool: {tool.name()}")
46            print(f"Description: {tool.tool_info().description}")
47
48    Args:
49        config: Server configuration:
50            - MCPStdioParams: For local servers via stdin/stdout
51            - MCPHttpParams: For remote servers via HTTP/SSE
52        client_session: Optional pre-configured ClientSession for advanced use cases.
53                       If not provided, a new session will be created automatically.
54        setup_timeout: Maximum seconds to wait for connection (default: 30).
55                      Increase for slow servers or complex authentication flows.
56
57    Returns:
58        MCPServer: Connected server instance with:
59            - tools: List of Node classes representing MCP tools
60            - close(): Method to explicitly close the connection
61            - Context manager support for automatic cleanup
62
63    Raises:
64        FileNotFoundError: If STDIO command not found. Verify the command is:
65                          - Installed and in your PATH
66                          - Spelled correctly (check for typos)
67                          - Executable (check permissions on Unix)
68        ConnectionError: If connection to server fails. Check:
69                        - Server URL is correct and accessible
70                        - Network connectivity and firewall settings
71                        - Authentication credentials are valid
72                        - Server is running and accepting connections
73        TimeoutError: If connection exceeds setup_timeout. Try:
74                     - Increasing setup_timeout parameter
75                     - Checking server performance/load
76                     - Verifying server is responding
77        RuntimeError: For other setup failures (e.g., protocol errors, config issues)
78
79    Note:
80        - The connection runs in a background thread for sync/async bridging
81        - Tools are cached after first retrieval for performance
82        - Always close() the server when done or use context manager
83        - Jupyter compatibility patches are applied automatically
84    """
85    # Apply Jupyter compatibility patches if needed
86    apply_patches()
87
88    return MCPServer(
89        config=config, client_session=client_session, setup_timeout=setup_timeout
90    )

Connect to an MCP server and return a server instance with available tools.

This is the primary entry point for using MCP servers in Railtracks. The server will connect in the background, discover available tools, and convert them to Railtracks Node classes.

The connection remains active until explicitly closed or the context exits.

Usage Examples:

STDIO connection (local MCP server)

config = rt.MCPStdioParams( command="uvx", args=["mcp-server-time"] ) server = rt.connect_mcp(config)

HTTP connection (remote MCP server)

config = rt.MCPHttpParams( url="https://mcp.example.com/sse", headers={"Authorization": "Bearer token"} ) server = rt.connect_mcp(config)

Context manager (recommended)

with rt.connect_mcp(config) as server: tools = server.tools # Use tools...

Automatically closed

Access tools

for tool in server.tools: print(f"Tool: {tool.name()}") print(f"Description: {tool.tool_info().description}")

Arguments:
  • config: Server configuration:
    • MCPStdioParams: For local servers via stdin/stdout
    • MCPHttpParams: For remote servers via HTTP/SSE
  • client_session: Optional pre-configured ClientSession for advanced use cases. If not provided, a new session will be created automatically.
  • setup_timeout: Maximum seconds to wait for connection (default: 30). Increase for slow servers or complex authentication flows.
Returns:

MCPServer: Connected server instance with: - tools: List of Node classes representing MCP tools - close(): Method to explicitly close the connection - Context manager support for automatic cleanup

Raises:
  • FileNotFoundError: If STDIO command not found. Verify the command is:
    • Installed and in your PATH
    • Spelled correctly (check for typos)
    • Executable (check permissions on Unix)
  • ConnectionError: If connection to server fails. Check:
    • Server URL is correct and accessible
    • Network connectivity and firewall settings
    • Authentication credentials are valid
    • Server is running and accepting connections
  • TimeoutError: If connection exceeds setup_timeout. Try:
    • Increasing setup_timeout parameter
    • Checking server performance/load
    • Verifying server is responding
  • RuntimeError: For other setup failures (e.g., protocol errors, config issues)
Note:
  • The connection runs in a background thread for sync/async bridging
  • Tools are cached after first retrieval for performance
  • Always close() the server when done or use context manager
  • Jupyter compatibility patches are applied automatically
def create_mcp_server( nodes: List[railtracks.nodes.nodes.Node | railtracks.built_nodes.concrete.function_base.RTFunction], server_name: str = 'MCP Server', fastmcp: mcp.server.fastmcp.server.FastMCP | None = None):
 86def create_mcp_server(
 87    nodes: List[Node | RTFunction],
 88    server_name: str = "MCP Server",
 89    fastmcp: FastMCP | None = None,
 90):
 91    """
 92    Create a FastMCP server that can be used to run nodes as MCP tools.
 93
 94    Args:
 95        nodes: List of Node classes to be registered as tools with the MCP server.
 96        server_name: Name of the MCP server instance.
 97        fastmcp: Optional FastMCP instance to use instead of creating a new one.
 98
 99    Returns:
100        A FastMCP server instance.
101    """
102    if fastmcp is not None:
103        if not isinstance(fastmcp, FastMCP):
104            raise ValueError("Provided fastmcp must be an instance of FastMCP.")
105        mcp = fastmcp
106    else:
107        mcp = FastMCP(server_name)
108
109    for node in [n if not hasattr(n, "node_type") else n.node_type for n in nodes]:
110        node_info = node.tool_info()
111        func = _create_tool_function(node, node_info)
112
113        mcp._tool_manager._tools[node_info.name] = MCPTool(
114            fn=func,
115            name=node_info.name,
116            description=node_info.detail,
117            parameters=(
118                _parameters_to_json_schema(node_info.parameters)
119                if node_info.parameters is not None
120                else {}
121            ),
122            fn_metadata=func_metadata(func, []),
123            is_async=True,
124            context_kwarg=None,
125            annotations=None,
126        )  # Register the node as a tool
127
128    return mcp

Create a FastMCP server that can be used to run nodes as MCP tools.

Arguments:
  • nodes: List of Node classes to be registered as tools with the MCP server.
  • server_name: Name of the MCP server instance.
  • fastmcp: Optional FastMCP instance to use instead of creating a new one.
Returns:

A FastMCP server instance.

class ToolManifest:
 7class ToolManifest:
 8    """
 9    Creates a manifest for a tool, which includes its description and parameters.
10
11    Args:
12        description (str): A description of the tool.
13        parameters (Iterable[Parameter] | None): An iterable of parameters for the tool. If None, there are no paramerters.
14    """
15
16    def __init__(
17        self,
18        description: str,
19        parameters: Iterable[Parameter] | None = None,
20    ):
21        self.description = description
22        self.parameters: List[Parameter] = (
23            list(parameters) if parameters is not None else []
24        )

Creates a manifest for a tool, which includes its description and parameters.

Arguments:
  • description (str): A description of the tool.
  • parameters (Iterable[Parameter] | None): An iterable of parameters for the tool. If None, there are no paramerters.
ToolManifest( description: str, parameters: Optional[Iterable[railtracks.llm.Parameter]] = None)
16    def __init__(
17        self,
18        description: str,
19        parameters: Iterable[Parameter] | None = None,
20    ):
21        self.description = description
22        self.parameters: List[Parameter] = (
23            list(parameters) if parameters is not None else []
24        )
description
parameters: List[railtracks.llm.Parameter]
def session_id() -> str | None:
427def session_id() -> str | None:
428    """
429    Gets the current session ID if it exists, otherwise returns None.
430    """
431    try:
432        return get_session_id()
433    except ContextError:
434        return None

Gets the current session ID if it exists, otherwise returns None.

class Flow(typing.Generic[~_P, ~_TOutput]):
 25class Flow(Generic[_P, _TOutput]):
 26    """A reusable, configured entry point for running an agent graph.
 27
 28    Binds an entry-point node to a fixed set of runtime options so the same
 29    configuration can be invoked repeatedly.  Each invocation is fully isolated.
 30
 31    Typical usage::
 32
 33        flow = Flow("my-agent", entry_point=my_node, context={"user": "alice"})
 34        result = await flow.ainvoke(query)  # async (preferred)
 35        result = flow.invoke(query)  # sync
 36
 37    Args:
 38        name: Unique human-readable name used in logging and state filenames.
 39        entry_point: The node (or decorated function) that starts the graph.
 40        context: Key/value pairs available to every node via ``rt.context``.
 41            Deep-copied at invocation time so mutations never affect later runs.
 42        timeout: Maximum seconds to wait for the run. ``None`` means no limit.
 43        end_on_error: When ``True``, the first unhandled exception aborts the run.
 44        broadcast_callback: Called with each string emitted by ``rt.broadcast()``.
 45            May be sync or async.
 46        prompt_injection: When ``True``, prompt text is injected from context
 47            variables before the run starts.
 48        save_state: When ``True``, session state is persisted to
 49            ``.railtracks/data/sessions/`` after the run.
 50        payload_callback: Called with the final result payload on success.
 51    """
 52
 53    def __init__(
 54        self,
 55        name: str,
 56        entry_point: (
 57            Callable[_P, Node[_TOutput]]
 58            | RTSyncFunction[_P, _TOutput]
 59            | RTAsyncFunction[_P, _TOutput]
 60        ),
 61        *,
 62        context: dict[str, Any] | None = None,
 63        timeout: float | None = None,
 64        end_on_error: bool | None = None,
 65        broadcast_callback: (
 66            Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
 67        ) = None,
 68        prompt_injection: bool | None = None,
 69        save_state: bool | None = None,
 70        payload_callback: Callable[[dict[str, Any]], Any] | None = None,
 71    ) -> None:
 72        self.entry_point: Callable[_P, Node[_TOutput]]
 73
 74        if hasattr(entry_point, "node_type"):
 75            self.entry_point = entry_point.node_type
 76        else:
 77            self.entry_point = entry_point
 78
 79        self.name = name
 80        self._context: dict[str, Any] = context or {}
 81        self._timeout = timeout
 82        self._end_on_error = end_on_error
 83        self._broadcast_callback = broadcast_callback
 84        self._prompt_injection = prompt_injection
 85        self._save_state = save_state
 86        self._payload_callback = payload_callback
 87
 88    def update_context(self, context: dict[str, Any]) -> Flow[_P, _TOutput]:
 89        """Return a new Flow with additional context values merged in.
 90
 91        The original flow is not modified.  Values in ``context`` override
 92        any existing keys; keys not present in ``context`` are preserved.
 93
 94        Args:
 95            context: Entries to add or override in the flow's context.
 96
 97        Returns:
 98            A new :class:`Flow` instance with the merged context.
 99        """
100        new_obj = deepcopy(self)
101        new_obj._context.update(context)
102        return new_obj
103
104    async def ainvoke(self, *args: _P.args, **kwargs: _P.kwargs) -> _TOutput:
105        """Run the flow asynchronously and return the entry-point result.
106
107        Args:
108            *args: Positional arguments forwarded to the entry-point node.
109            **kwargs: Keyword arguments forwarded to the entry-point node.
110
111        Returns:
112            The value returned by the entry-point node.
113        """
114        with Session(
115            context=deepcopy(self._context),
116            flow_name=self.name,
117            flow_id=self.equality_hash(),
118            name=None,
119            timeout=self._timeout,
120            end_on_error=self._end_on_error,
121            broadcast_callback=self._broadcast_callback,
122            prompt_injection=self._prompt_injection,
123            save_state=self._save_state,
124            payload_callback=self._payload_callback,
125        ):
126            result = await call(self.entry_point, *args, **kwargs)
127
128        return result
129
130    def invoke(self, *args: _P.args, **kwargs: _P.kwargs) -> _TOutput:
131        """Run the flow synchronously and return the entry-point result.
132
133        Prefer ``await flow.ainvoke()`` in async contexts.
134
135        Args:
136            *args: Positional arguments forwarded to the entry-point node.
137            **kwargs: Keyword arguments forwarded to the entry-point node.
138
139        Returns:
140            The value returned by the entry-point node.
141
142        Note:
143            When no event loop is running, delegates to ``asyncio.run()``.
144            When a loop is already running (e.g. Jupyter, FastAPI), submits
145            the coroutine to a ``ThreadPoolExecutor`` worker thread.
146        """
147        try:
148            asyncio.get_running_loop()
149        except RuntimeError:
150            return asyncio.run(self.ainvoke(*args, **kwargs))
151
152        ctx = contextvars.copy_context()
153        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
154            future = pool.submit(ctx.run, asyncio.run, self.ainvoke(*args, **kwargs))
155            return future.result()
156
157    def equality_hash(self) -> str:
158        """Return a stable hash that identifies this flow's configuration.
159
160        Two flows with the same name produce the same hash regardless of
161        other parameters (timeout, context, etc.).
162        """
163        config_string = json.dumps(self._get_hash_content(), sort_keys=True)
164        return hashlib.sha256(config_string.encode()).hexdigest()
165
166    def _get_hash_content(self) -> dict:
167        return {
168            "name": self.name,
169        }

A reusable, configured entry point for running an agent graph.

Binds an entry-point node to a fixed set of runtime options so the same configuration can be invoked repeatedly. Each invocation is fully isolated.

Typical usage::

flow = Flow("my-agent", entry_point=my_node, context={"user": "alice"})
result = await flow.ainvoke(query)  # async (preferred)
result = flow.invoke(query)  # sync
Arguments:
  • name: Unique human-readable name used in logging and state filenames.
  • entry_point: The node (or decorated function) that starts the graph.
  • context: Key/value pairs available to every node via rt.context. Deep-copied at invocation time so mutations never affect later runs.
  • timeout: Maximum seconds to wait for the run. None means no limit.
  • end_on_error: When True, the first unhandled exception aborts the run.
  • broadcast_callback: Called with each string emitted by rt.broadcast(). May be sync or async.
  • prompt_injection: When True, prompt text is injected from context variables before the run starts.
  • save_state: When True, session state is persisted to .railtracks/data/sessions/ after the run.
  • payload_callback: Called with the final result payload on success.
Flow( name: str, entry_point: Union[Callable[~_P, railtracks.nodes.nodes.Node[~_TOutput]], railtracks.built_nodes.concrete.function_base.RTSyncFunction[~_P, ~_TOutput], railtracks.built_nodes.concrete.function_base.RTAsyncFunction[~_P, ~_TOutput]], *, context: dict[str, typing.Any] | None = None, timeout: float | None = None, end_on_error: bool | None = None, broadcast_callback: Union[Callable[[str], NoneType], Callable[[str], Coroutine[NoneType, NoneType, NoneType]], NoneType] = None, prompt_injection: bool | None = None, save_state: bool | None = None, payload_callback: Optional[Callable[[dict[str, Any]], Any]] = None)
53    def __init__(
54        self,
55        name: str,
56        entry_point: (
57            Callable[_P, Node[_TOutput]]
58            | RTSyncFunction[_P, _TOutput]
59            | RTAsyncFunction[_P, _TOutput]
60        ),
61        *,
62        context: dict[str, Any] | None = None,
63        timeout: float | None = None,
64        end_on_error: bool | None = None,
65        broadcast_callback: (
66            Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None
67        ) = None,
68        prompt_injection: bool | None = None,
69        save_state: bool | None = None,
70        payload_callback: Callable[[dict[str, Any]], Any] | None = None,
71    ) -> None:
72        self.entry_point: Callable[_P, Node[_TOutput]]
73
74        if hasattr(entry_point, "node_type"):
75            self.entry_point = entry_point.node_type
76        else:
77            self.entry_point = entry_point
78
79        self.name = name
80        self._context: dict[str, Any] = context or {}
81        self._timeout = timeout
82        self._end_on_error = end_on_error
83        self._broadcast_callback = broadcast_callback
84        self._prompt_injection = prompt_injection
85        self._save_state = save_state
86        self._payload_callback = payload_callback
entry_point: Callable[~_P, railtracks.nodes.nodes.Node[~_TOutput]]
name
def update_context( self, context: dict[str, typing.Any]) -> Flow[~_P, ~_TOutput]:
 88    def update_context(self, context: dict[str, Any]) -> Flow[_P, _TOutput]:
 89        """Return a new Flow with additional context values merged in.
 90
 91        The original flow is not modified.  Values in ``context`` override
 92        any existing keys; keys not present in ``context`` are preserved.
 93
 94        Args:
 95            context: Entries to add or override in the flow's context.
 96
 97        Returns:
 98            A new :class:`Flow` instance with the merged context.
 99        """
100        new_obj = deepcopy(self)
101        new_obj._context.update(context)
102        return new_obj

Return a new Flow with additional context values merged in.

The original flow is not modified. Values in context override any existing keys; keys not present in context are preserved.

Arguments:
  • context: Entries to add or override in the flow's context.
Returns:

A new Flow instance with the merged context.

async def ainvoke(self, *args: _P.args, **kwargs: _P.kwargs) -> ~_TOutput:
104    async def ainvoke(self, *args: _P.args, **kwargs: _P.kwargs) -> _TOutput:
105        """Run the flow asynchronously and return the entry-point result.
106
107        Args:
108            *args: Positional arguments forwarded to the entry-point node.
109            **kwargs: Keyword arguments forwarded to the entry-point node.
110
111        Returns:
112            The value returned by the entry-point node.
113        """
114        with Session(
115            context=deepcopy(self._context),
116            flow_name=self.name,
117            flow_id=self.equality_hash(),
118            name=None,
119            timeout=self._timeout,
120            end_on_error=self._end_on_error,
121            broadcast_callback=self._broadcast_callback,
122            prompt_injection=self._prompt_injection,
123            save_state=self._save_state,
124            payload_callback=self._payload_callback,
125        ):
126            result = await call(self.entry_point, *args, **kwargs)
127
128        return result

Run the flow asynchronously and return the entry-point result.

Arguments:
  • *args: Positional arguments forwarded to the entry-point node.
  • **kwargs: Keyword arguments forwarded to the entry-point node.
Returns:

The value returned by the entry-point node.

def invoke(self, *args: _P.args, **kwargs: _P.kwargs) -> ~_TOutput:
130    def invoke(self, *args: _P.args, **kwargs: _P.kwargs) -> _TOutput:
131        """Run the flow synchronously and return the entry-point result.
132
133        Prefer ``await flow.ainvoke()`` in async contexts.
134
135        Args:
136            *args: Positional arguments forwarded to the entry-point node.
137            **kwargs: Keyword arguments forwarded to the entry-point node.
138
139        Returns:
140            The value returned by the entry-point node.
141
142        Note:
143            When no event loop is running, delegates to ``asyncio.run()``.
144            When a loop is already running (e.g. Jupyter, FastAPI), submits
145            the coroutine to a ``ThreadPoolExecutor`` worker thread.
146        """
147        try:
148            asyncio.get_running_loop()
149        except RuntimeError:
150            return asyncio.run(self.ainvoke(*args, **kwargs))
151
152        ctx = contextvars.copy_context()
153        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
154            future = pool.submit(ctx.run, asyncio.run, self.ainvoke(*args, **kwargs))
155            return future.result()

Run the flow synchronously and return the entry-point result.

Prefer await flow.ainvoke() in async contexts.

Arguments:
  • *args: Positional arguments forwarded to the entry-point node.
  • **kwargs: Keyword arguments forwarded to the entry-point node.
Returns:

The value returned by the entry-point node.

Note:

When no event loop is running, delegates to asyncio.run(). When a loop is already running (e.g. Jupyter, FastAPI), submits the coroutine to a ThreadPoolExecutor worker thread.

def equality_hash(self) -> str:
157    def equality_hash(self) -> str:
158        """Return a stable hash that identifies this flow's configuration.
159
160        Two flows with the same name produce the same hash regardless of
161        other parameters (timeout, context, etc.).
162        """
163        config_string = json.dumps(self._get_hash_content(), sort_keys=True)
164        return hashlib.sha256(config_string.encode()).hexdigest()

Return a stable hash that identifies this flow's configuration.

Two flows with the same name produce the same hash regardless of other parameters (timeout, context, etc.).

def enable_logging( level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'NONE'] = 'INFO', log_file: str | os.PathLike | None = None, *, name_style: Literal['full', 'short'] = 'short') -> None:
324def enable_logging(
325    level: AllowableLogLevels = "INFO",
326    log_file: str | os.PathLike | None = None,
327    *,
328    name_style: LoggerNameDisplay = "short",
329) -> None:
330    """
331    Opt-in helper to enable Railtracks logging. Call this explicitly from your
332    application entry point (CLI, main.py, server startup); the library never
333    calls it automatically.
334
335    Uses the given level and log_file; when None, reads RT_LOG_LEVEL and
336    RT_LOG_FILE from the environment. Sets up console output (and optional file)
337    with a ThreadAwareFilter for per-thread level control.
338
339    Args:
340        level: Logging level (default "INFO"). Overridden by RT_LOG_LEVEL when None.
341        log_file: Optional path for a log file. Overridden by RT_LOG_FILE when None.
342        name_style: Console column for logger name: ``full`` (dotted name) or
343            ``short`` (``RT.<Label>``: last segment with leading non-letters stripped,
344            then capitalized). Default ``short``.
345    """
346    initialize_module_logging(level=level, log_file=log_file, name_style=name_style)

Opt-in helper to enable Railtracks logging. Call this explicitly from your application entry point (CLI, main.py, server startup); the library never calls it automatically.

Uses the given level and log_file; when None, reads RT_LOG_LEVEL and RT_LOG_FILE from the environment. Sets up console output (and optional file) with a ThreadAwareFilter for per-thread level control.

Arguments:
  • level: Logging level (default "INFO"). Overridden by RT_LOG_LEVEL when None.
  • log_file: Optional path for a log file. Overridden by RT_LOG_FILE when None.
  • name_style: Console column for logger name: full (dotted name) or short (RT.<Label>: last segment with leading non-letters stripped, then capitalized). Default short.
def escape_braces(text: str) -> str:
10def escape_braces(text: str) -> str:
11    """
12    Escape the braces in `text` so that prompt injection treats it as data.
13
14    Apply this to untrusted or arbitrary strings before embedding them in a message that
15    will have context values injected into it. Injecting the returned string yields
16    `text` back unchanged.
17
18    Args:
19        text: The string to escape.
20
21    Returns:
22        `text` with every `{` and `}` doubled.
23    """
24    return text.replace("{", "{{").replace("}", "}}")

Escape the braces in text so that prompt injection treats it as data.

Apply this to untrusted or arbitrary strings before embedding them in a message that will have context values injected into it. Injecting the returned string yields text back unchanged.

Arguments:
  • text: The string to escape.
Returns:

text with every { and } doubled.