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);
- Call
UTryllSubsystem::CreateStringStoragetwice — once for patterns, once for refusal lines. Bind On Create String Storage Complete for each and wait forbSuccess. - In a
UTryllWorkflowAsset(or directly on the component's Graph details panel), build the three-node graph with thestring_storageparameter set on the guardrail and the canned-response nodes.
namespace TC = Tryll::Client;
// 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).
TC::RegexGuardrailParams guardParams;
guardParams.string_storage = "jailbreak_patterns";
guardParams.triggered_exit = "refuse";
guardParams.not_triggered_exit = "answer";
TC::CannedResponseParams refuseParams;
refuseParams.string_storage = "refusal_lines";
// refuseParams.default_exit defaults to "" (END)
TC::GraphDescription graph;
graph.AddNode("guard", guardParams)
.AddNode("refuse", refuseParams)
.AddNode("answer", TC::GenerateParams{})
.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 session's storage
root (CreateSessionRequest.storage_data_folder, else the server's
storage_root, else the exe directory). An absolute path or a ..
that escapes the root is rejected with 3009 StorageOutsideRoot — keep
these paths relative and inside the storage folder.
Wire the graph the same way as in the inline example — the node
string_storage param refers to the storage by name and does not
care whether it was created from inline strings or from a file.
Server-default fallback¶
If a node has neither string_storage nor inline
pattern_0 / response_0 / … params, 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 > inline pattern_0 /
response_0 / … params > 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 and
is_final = true. Server log:
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. - Compilation errors in a pattern fail the whole agent creation with error 3003. 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 inline params unset and the operator has not configured a default file, the node is effectively disabled — a guardrail always exits vianot_triggered, a canned-response has no lines to emit. Create a storage explicitly.