Use Canned Responses and Guardrails¶
Short-circuit jailbreak attempts, off-topic prompts, and other unwanted inputs to a scripted reply — without ever running the model.
Prerequisites
- A connected session with
CreateSessionalready called — see Connect and Manage a Session.
The pattern¶
flowchart LR
guard["RegexGuardrail<br>guard"]
refuse["CannedResponse<br>refuse"]
gen["Generate<br>answer"]
guard -- "triggered" --> refuse
guard -- "not_triggered" --> gen
refuse -- "default" --> END
gen -- "default" --> END
A RegexGuardrail
matches the current user message against a list of case-insensitive
regexes. On a match, the graph takes the triggered route to a
CannedResponse that emits
one scripted reply (RoundRobin selection by default — it cycles through
the lines; set selection_strategy: First for a deterministic pick or
Random to sample). On a miss, it falls through to Generate as normal.
Steps¶
// 1. Create storages.
await TryllClient.Instance.RequestCreateStringStorageAsync(
"jailbreak_patterns",
new List<string> {
@"ignore\s+previous\s+instructions?",
@"you\s+are\s+DAN",
@"pretend\s+to\s+be",
});
await TryllClient.Instance.RequestCreateStringStorageAsync(
"refusal_lines",
new List<string> {
"I can't help with that request.",
"That's outside what I can assist with.",
});
// 2. Build the graph.
// Exits pointing to END are the default (empty string) — no need to set them.
var graph = new TryllGraphBuilder()
.AddRegexGuardrail("guard", new TryllRegexGuardrailParams
{
StringStorage = "jailbreak_patterns",
TriggeredExit = "refuse",
NotTriggeredExit = "answer",
})
.AddCannedResponse("refuse", new TryllCannedResponseParams
{
StringStorage = "refusal_lines",
// DefaultExit is "" (END) by default.
})
.AddGenerate("answer", new TryllGenerateParams())
.SetStartNode("guard")
.SetDefaultModelName("My Local Model")
.Build();
var (agent, error) = await TryllClient.Instance.RequestCreateAgentAsync(graph);
agent.SetOnAnswerText((nodeName, text, isDelta, isFinal) => { /* stream text */ });
agent.SetOnTurnComplete((status, debugInfo, tokens) => { /* turn ended */ });
- Call
UTryllSubsystem::RequestCreateStringStoragetwice — once for patterns, once for refusal lines. Bind On Create String Storage Complete for each and wait forbSuccess. - In a
UTryllWorkflowAsset, build the three-node graph with thestring_storageparameter set on the guardrail and the canned-response nodes.
The same in C++:
#include "Generated/TryllGraphBuilder.Nodes.h"
#include "Generated/TryllNodeParamsFactory.h"
// 1. Create storages.
UTryllSubsystem* Subsystem = GetGameInstance()->GetSubsystem<UTryllSubsystem>();
Subsystem->RequestCreateStringStorage(TEXT("jailbreak_patterns"), {
TEXT("ignore\\s+previous\\s+instructions?"),
TEXT("you\\s+are\\s+DAN"),
TEXT("pretend\\s+to\\s+be"),
});
Subsystem->RequestCreateStringStorage(TEXT("refusal_lines"), {
TEXT("I can't help with that request."),
TEXT("That's outside what I can assist with."),
});
// 2. Build the graph — exits pointing to END are the default (empty string).
UTryllRegexGuardrailParams* GuardP = UTryllNodeParamsFactory::MakeRegexGuardrailParams(this);
GuardP->StringStorage.Path = TEXT("jailbreak_patterns");
GuardP->TriggeredExit = TEXT("refuse");
GuardP->NotTriggeredExit = TEXT("answer");
UTryllCannedResponseParams* RefuseP = UTryllNodeParamsFactory::MakeCannedResponseParams(this);
RefuseP->StringStorage.Path = TEXT("refusal_lines");
// RefuseP->DefaultExit is TEXT("") (END) by default.
FTryllGraphDescription Graph = FTryllGraphBuilder()
.AddNode(TEXT("guard"), GuardP)
.AddNode(TEXT("refuse"), RefuseP)
.AddNode(TEXT("answer"), UTryllNodeParamsFactory::MakeGenerateParams(this))
.SetStartNode(TEXT("guard"))
.SetDefaultModelName(TEXT("My Local Model"))
.Build();
// 3. Once both storages report success (OnComplete or
// OnCreateStringStorageComplete), create the agent.
Subsystem->RequestCreateAgent(Graph,
[](TSharedPtr<FTryllAgent> Agent, FTryllError Error) { /* keep Agent */ });
namespace TC = Tryll::Client;
namespace NP = Tryll::NodeParams;
// 1. Storages
client.CreateStringStorage("jailbreak_patterns", {
R"(ignore\s+previous\s+instructions?)",
R"(you\s+are\s+DAN)",
});
client.CreateStringStorage("refusal_lines", {
"I can't help with that request.",
"That's outside what I can assist with.",
});
// 2. Graph — exits pointing to END are the default (empty string).
NP::RegexGuardrailParamsT guardParams;
guardParams.string_storage = "jailbreak_patterns";
guardParams.triggered_exit = "refuse";
guardParams.not_triggered_exit = "answer";
NP::CannedResponseParamsT refuseParams;
refuseParams.string_storage = "refusal_lines";
// refuseParams.default_exit defaults to "" (END)
TC::GraphDescription graph;
graph.AddRegexGuardrail("guard", guardParams)
.AddCannedResponse("refuse", refuseParams)
.AddGenerate("answer", NP::GenerateParamsT{})
.SetStartNode("guard")
.SetDefaultModelName("My Local Model");
auto agent = client.CreateAgent(graph);
from tryll_client.graph import (
GraphDescription, RegexGuardrailParams,
CannedResponseParams, GenerateParams,
)
# 1. Define the patterns and the refusal lines as string storages.
# These die with the session; recreate on reconnect.
client.create_string_storage(
name="jailbreak_patterns",
strings=[
r"ignore\s+previous\s+instructions?",
r"you\s+are\s+DAN",
r"pretend\s+to\s+be",
],
)
client.create_string_storage(
name="refusal_lines",
strings=[
"I can't help with that request.",
"That's outside what I can assist with.",
],
)
# 2. Build the graph — exits pointing to END are the default (empty string).
graph = (
GraphDescription()
.add_node("guard", RegexGuardrailParams(
string_storage="jailbreak_patterns",
triggered_exit="refuse",
not_triggered_exit="answer",
))
.add_node("refuse", CannedResponseParams(
string_storage="refusal_lines",
# default_exit is "" (END) by default.
))
.add_node("answer", GenerateParams())
.set_start_node("guard")
.set_default_model_name("My Local Model")
)
agent = client.create_agent(graph)
Use file-based patterns instead of inline¶
If your patterns or refusal lines live in a text file on the server
machine (say you maintain them out-of-band), pass the path to
CreateStringStorage instead of listing strings inline. The server
reads the file at storage-creation time; the resulting storage is
used exactly like an inline one.
File format: UTF-8, one entry per line, # starts a comment, blank
lines are ignored. See
String Storage → File format
for the full spec.
Paths are resolved on the server, relative to the process storage root
(--storage-root / storage_root / mode default). An absolute path or a ..
that escapes the root is rejected (this request reports it as
3010 StorageFileNotFound) — keep these paths relative and inside the
storage folder.
A storage created from a file is kept under its path, not under the
name you pass. Wire the graph as in the inline example, but set each
node's string_storage to the same relative path — patterns/jailbreak.txt
on the guardrail and patterns/refusals.txt on the canned response. You can
also skip the create call: a node whose string_storage names a file loads it
at agent creation. Creating it first only keeps it loaded for the session.
Server-default fallback¶
If a node has neither string_storage nor an inline list (lines on a
canned response, inline_strings on a regex guardrail), the
server falls back to a
file the operator configured in
server-config.json
(default_canned_responses_path, default_guardrail_patterns_path).
This is a deployment-wide fallback for the server admin. Client workflows should create a storage explicitly; do not rely on the operator's defaults unless you control both.
Priority order: explicit string_storage > the inline list (lines or
inline_strings) >
operator-configured default file. See the
node reference.
Verify it worked¶
Send a jailbreak-style prompt:
Call UTryllAgentComponent::SendMessage with the prompt; the
refusal line arrives as a single OnAnswerText chunk with
bIsFinal = true.
You should get one of the refusal lines back, with one
AnswerText chunk carrying the full string, is_delta = false, and
is_final = true. The server log shows the match at info; with
log_level set to "trace", the node end lines name the route taken:
[info] [RegexGuardrailNode] Pattern triggered
[trace] [NODE] end node=guard exit=triggered duration_ms=0.1
[trace] [NODE] end node=refuse exit=default duration_ms=0.0
Now a normal prompt:
UTryllAgentComponent::SendMessage("What's the capital of France?")
— the streamed reply arrives through On Answer Text.
goes through not_triggered → answer and runs the model.
Common pitfalls¶
- Regex anchors. Patterns are matched with
std::regex_search, so they hit anywhere in the message. If you want whole-message match, anchor with^…$yourself. - Invalid patterns are dropped, not reported. At agent creation the
server skips a pattern that does not compile and logs a warning
(
[RegexGuardrailNode] Invalid regex '…': …); the agent is created with the remaining patterns. Rebindingstring_storageat runtime to a list with a bad pattern is rejected with error 3007 and keeps the previous set. Test patterns in a regex tester first. - False positives. A pattern like
pretendwill trigger on "the actor is pretending to be angry". Make patterns tighter:pretend\s+to\s+be\s+(DAN|an? (unrestricted|uncensored)). - Node with nothing to resolve. If you leave both
string_storageand the inline list unset and the operator has not configured a default file,CreateAgentfails: the node reports that it requiresstring_storage, inline text, or a server-configured default. Create a storage explicitly.