Skip to content

Upgrading to 1.5.0

Railtracks 1.5.0 reworks how cross-cutting behaviour attaches to nodes, and removes or relocates a number of public APIs as a result.

1.5.0 is a minor release with breaking changes

Version pins such as railtracks>=1.4 or railtracks~=1.4 will pick up 1.5.0 automatically. If you are not ready to migrate, pin railtracks==<YOUR VERSION>.

Release 1.4.7 emits a FutureWarning at each affected call site. FutureWarning is shown by default, so you should see these without configuring anything.


Changes you can make today

Everything in this section has a forward path that works in both 1.4.7 and 1.5.0 — migrate now and the upgrade is a no-op.

agent_node requires llm

llm becomes a required keyword argument. Omitting it currently defers model selection to instance time; in 1.5.0 it raises TypeError when the agent is built.

# before
Agent = rt.agent_node(name="my-agent")

# after
Agent = rt.agent_node(name="my-agent", llm=rt.llm.OpenAILLM("gpt-4o"))

Prebuilt guards move to rt.prebuilt.guardrails

The built-in guards and the PII configuration classes move out of rt.guardrails.llm. The classes themselves are unchanged; only the import path differs, and the new path already works in 1.4.7.

# before
from railtracks.guardrails.llm import BlockTextInputGuard, PIIRedactConfig

# after
from railtracks.prebuilt.guardrails import BlockTextInputGuard, PIIRedactConfig

This covers all nine names: BlockTextInputGuard, BlockTextOutputGuard, InputLengthGuard, OutputLengthGuard, PIIRedactInputGuard, PIIRedactOutputGuard, PIICustomPattern, PIIEntity, PIIRedactConfig. The rt.guardrails.llm.input and rt.guardrails.llm.output submodules go away with them; everything is re-exported flat from rt.prebuilt.guardrails.

Custom guards are unaffected: keep subclassing rt.guardrails.InputGuard / rt.guardrails.OutputGuard.

rt.interactive and local_chat are removed

The local chat UI is going away. If you depend on it, pin railtracks==<YOUR VERSION> and open an issue describing your use case.


Changes that need the 1.5.0 upgrade

These have no forward path in 1.4.7; the replacements ship with 1.5.0. Warned call sites point here rather than naming an API you cannot import yet.

Guards attach as middleware instead of guardrails=

agent_node(guardrails=...) is removed, along with the Guard container that fed it. Guards become model middleware: write one with the @rt.input_guard / @rt.output_guard decorators and attach it with model_middleware=[...].

# before -- 1.4.7
from railtracks.guardrails import Guard

Agent = rt.agent_node(
    llm=rt.llm.GeminiLLM("gemini-2.5-flash"),
    guardrails=Guard(input=[my_guard]),
)

# after -- 1.5.0
from railtracks.guardrails import GuardrailDecision, LLMGuardrailEvent

@rt.input_guard
def my_guard(event: LLMGuardrailEvent) -> GuardrailDecision:
    if "password" in str(event.messages[-1].content).lower():
        return GuardrailDecision.block(reason="Not allowed.")
    return GuardrailDecision.allow()

Agent = rt.agent_node(
    llm=rt.llm.GeminiLLM("gemini-2.5-flash"),
    model_middleware=[my_guard],
)

Ordering and composition are handled by the middleware chain, so Guard has no direct equivalent.

Writing a guard does not change. InputGuard, OutputGuard, GuardrailDecision, GuardrailAction, GuardrailTrace, GuardrailBlockedError, LLMGuardrailEvent and LLMGuardrailPhase all stay exactly as they are; only the attachment point moves.

Streaming becomes async

stream=True is removed from every model constructor, along with the synchronous generator it returned. Streaming an agent run is now rt.astream.

# before -- 1.4.7
model = rt.llm.OpenAILLM("gpt-4o", stream=True)
for chunk in model.chat(history):
    print(chunk)

# after -- 1.5.0
agent = rt.agent_node(name="Poet", llm=rt.llm.OpenAILLM("gpt-4o"))

stream = rt.astream(agent, user_input="Write a short poem about rain.")
async for chunk in stream:
    print(chunk, end="", flush=True)
final = stream.result

await rt.astream(...) returns just the final result if you do not need the chunks.