Skip to content

Build a Chat Agent with a Graph

Create an agent whose graph is a simple HumanMessageGuardrail → Generate chain, send a message, and get a streamed reply.

Prerequisites

Steps

// 1. Attach TryllAgentComponent to a GameObject.
//    Assign a TryllWorkflowAsset in the Inspector, or build the graph at runtime:
var graph = new TryllGraphBuilder()
    .AddHumanMessageGuardrail("guard", new TryllHumanMessageGuardrailParams
    {
        // TriggeredExit is "" (END) by default — drop jailbreaks silently.
        NotTriggeredExit = "answer",
    })
    .AddGenerate("answer", new TryllGenerateParams())
    .SetStartNode("guard")
    .SetDefaultModelName("My Local Model")
    .Build();

var agentComp = GetComponent<TryllAgentComponent>();
agentComp.InlineGraphDescription = graph;

// 2. Wire events.
agentComp.OnAnswerText.AddListener(
    (text, isDelta, isFinal) => chatText.text += text);
agentComp.OnTurnComplete.AddListener(
    (status, _, _) => typingIndicator.SetActive(false));

// 3. When the agent is created, send the first message.
agentComp.OnAgentCreated.AddListener(() => agentComp.SendMessage("Hello!"));
  1. Add a UTryllAgentComponent to your actor.
  2. In the Details panel, edit Graph on the component, or reference a UTryllWorkflowAsset you authored in the Content Browser. On the guard node, set Triggered Exit to "" (END) and Not Triggered Exit to "answer". The answer node's Default Exit can be left empty (END).
  3. In Blueprint:
    • Bind On Agent Ready to a handler that calls Send Message.
    • Bind On Answer Text to append each chunk to a UI text block.
    • Bind On Turn Complete to flip the "typing" indicator off.
#include <tryll/TryllClient.h>

namespace TC = Tryll::Client;

TC::HumanMessageGuardrailParams guardParams;
// guardParams.triggered_exit defaults to "" (END) — drop jailbreaks silently.
guardParams.not_triggered_exit = "answer";

TC::GraphDescription graph;
graph.AddNode("guard",  guardParams)
     .AddNode("answer", TC::GenerateParams{})
     .SetStartNode("guard")
     .SetDefaultModelName("My Local Model");

auto agent = client.CreateAgent(graph);

// Register persistent callbacks, then send — fire-and-forget.
agent.SetOnAnswerText([](std::string_view text, bool, bool)
    { std::cout << text << std::flush; });
agent.SetOnTurnComplete([](TC::TurnStatus, std::string_view, std::int32_t)
    { std::cout << "\n"; });
agent.SendText("Hello!");
from tryll_client.graph import GraphDescription, HumanMessageGuardrailParams, GenerateParams

# 1. Build the graph.
guard_params = HumanMessageGuardrailParams(
    # triggered_exit defaults to "" (END) — drop jailbreaks silently.
    not_triggered_exit = "answer",
)

graph = (
    GraphDescription()
    .add_node("guard",  guard_params)
    .add_node("answer", GenerateParams())
    .set_start_node("guard")
    .set_default_model_name("My Local Model")
)

# 2. Create the agent.
agent = client.create_agent(graph)

# 3. Send a message. send_message blocks and returns the full reply.
reply = agent.send_message("Hello!")
print(reply)

Verify it worked

Server-side, at info log level, one turn produces:

[info] Agent 7: turn starting
[info] Node guard: not_triggered
[info] Node answer: default
[info] Agent 7: turn complete (Success, tokens=128)

Client-side, Python returns the full reply string from send_message, while C++ and Unreal see each streamed chunk arrive in the SendText callback / OnAnswerText delegate, terminated by a TurnComplete with status Success.

Common pitfalls

  • "Model not found" (error 4002) — either the name is wrong, or the model is not in models.json. Double-check with client.list_models().
  • "Graph validation failed" (error 3003) — check for a bad start node name or duplicate node names in the graph. If the message says InvalidExitTarget (3008), an exit field on one of the nodes names a node that doesn't exist — fix the string or leave it empty for END.
  • Silent nothing after SendMessage. Check that the start node is not an always-END branch (e.g., guardrail that matches everything). The server log will show which route was taken.