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.