Change Agent Parameters at Runtime¶
Adjust a node's configuration while the agent is idle — for example to
switch personalities by updating system_prompt, or to tune retrieval
precision by changing top_k — without recreating the agent.
Prerequisites
- An agent created and ready to receive messages — see Build a Chat Agent with a Graph.
- The agent must be idle — or paused — when you call
ChangeParams/change_params. The server rejects the request with error3004 AgentBusyif a turn is actively running. See How to pause and resume a turn for the one exception: a turn suspended at aPausenode (or a pausedToolCall) allowsChangeAgentParamwhile it's parked.
How it works¶
Parameters are now typed — each node type has a dedicated params object
(e.g. GenerateParams, RetrieveParams) instead of a string key/value map.
The canonical mutation pattern is clone-set-send:
- Clone the baseline params object (preserves structural/immutable fields automatically).
- Set only the fields you want to change.
- Send the modified object via
ChangeParams.
The server diffs the supplied params against the node's current params,
vetoes any structural fields that have changed (3006 ParamNotMutable),
validates values (3007 InvalidParamValue), and applies the delta.
Which fields can be changed?¶
Fields marked (structural) in the schema cannot be changed at runtime.
All other fields are mutable unless a node-specific validator rejects the value.
Structural on every node
On every node that has them, the exit fields (default_exit,
found_exit, not_found_exit, tool_called_exit,
no_tool_called_exit), plus context_size, input, and output_name,
are structural — fixed at CreateAgent. The table lists only the
node-specific structural fields in addition to those.
| Node | Node-specific structural fields | Mutable fields |
|---|---|---|
Generate |
model_name |
system_prompt, template, placement, send, history_role, grammar, substitute_agent_variables, all sampling.* |
GenerateAndSpeak |
model_name, tts_model_name |
system_prompt, template, placement, history_role, grammar, substitute_agent_variables, speaker_id, speed, tts_lang, tts_voice, min_sentence_chars, all sampling.* |
Speak |
tts_model_name |
speaker_id, speed, tts_lang, tts_voice, min_sentence_chars |
ToolCall |
model_name, tools |
system_prompt, mode, parallel_tool_calls, disposition, acknowledge_text, all sampling.* |
Retrieve |
embedded_string_storage |
top_k, threshold, source, filter |
CannedResponse |
inline_strings |
string_storage (rebind), selection_strategy, send, history_role |
Transform |
— | template |
Pause |
— | (none — Pause has no mutable params) |
RegexGuardrail |
inline_strings |
string_storage (rebind) |
Instruction |
— | instruction |
ClassifyIntent |
embedded_string_storage |
metadata_field, threshold, filter, notify_client, diagnostic_topk |
ClassifyIntentLLM |
model_name, intents_ids, intents_prompt |
system_prompt, history_turns, threshold, margin, notify_client, not_found_intent |
IntentToInstruction |
inline_keys, inline_strings |
string_storage (rebind) |
tool_call_format on ToolCall is deprecated and ignored by the server
(tool prompting is chat-template driven), so it is not shown; do not set it.
Examples¶
using Tryll.Client;
var agentComp = GetComponent<TryllAgentComponent>();
// Clone the baseline, change one field, send.
// GetNodeParamsBaseline already returns a deep copy — mutate in place.
var p = (TryllGenerateParams)agentComp.GetNodeParamsBaseline("answer");
p.SystemPrompt = "You are a pirate.";
agentComp.ChangeParams("answer", p,
err => {
if (err.IsOk)
Debug.Log("Persona updated");
else
Debug.LogError($"Error {err.Code}: {err.Message}");
});
// Or bind OnParamChanged in the Inspector / AddListener.
// Relax the retrieval threshold.
var rp = (TryllRetrieveParams)agentComp.GetNodeParamsBaseline("knowledge");
rp.Threshold = 0.9f;
agentComp.ChangeParams("knowledge", rp);
// Switch to a single whole-text answer instead of streamed deltas.
var gp = (TryllGenerateParams)agentComp.GetNodeParamsBaseline("answer");
gp.Send = TryllSendAnswer.Whole;
agentComp.ChangeParams("answer", gp);
#include "Generated/Nodes/TryllGenerateParams.h"
#include "Generated/Nodes/TryllNodeParamsFactory.h"
// Clone the baseline, change one field, send.
UTryllNodeParamsBase* Base = AgentComponent->GetNodeParamsBaseline(TEXT("answer"));
UTryllGenerateParams* P = Cast<UTryllGenerateParams>(
UTryllNodeParamsFactory::CloneParams(Base, this));
P->SystemPrompt = TEXT("You are a pirate.");
AgentComponent->ChangeParams(
TEXT("answer"), P,
[](const FTryllError& Err)
{
if (Err.Code == 0)
UE_LOG(LogTemp, Log, TEXT("Persona updated"));
else
UE_LOG(LogTemp, Warning, TEXT("Error %d: %s"), Err.Code, *Err.Message);
});
// Relax the retrieval threshold.
UTryllRetrieveParams* RP = Cast<UTryllRetrieveParams>(
UTryllNodeParamsFactory::CloneParams(
AgentComponent->GetNodeParamsBaseline(TEXT("knowledge")), this));
RP->Threshold = 0.9f;
AgentComponent->ChangeParams(TEXT("knowledge"), RP);
- Call Get Node Params Baseline on the
TryllAgentComponentto get the current params object for a node. - Call Clone Params (from
TryllNodeParamsFactory) on the result. - Set any fields on the cloned object.
- Call Change Params on the
TryllAgentComponentand bind On Param Changed to handle success or error.
The C++ client does not cache a baseline copy of authored params. Keep your
own copy of the params you last sent (or authored at CreateAgent) and
start each mutation from that local copy. Structural fields must match the
create-time values or the server returns ParamNotMutable.
#include <tryll/AgentProxy.h>
// XxxT PODs come from the flatc-generated messages_generated.h (--gen-object-api)
#include <NodeParams_generated.h>
using namespace Tryll::NodeParams;
using namespace Tryll::Client;
// Clone the caller-owned baseline, change system prompt, send asynchronously.
GenerateParamsT p = answerBaseline; // caller-owned copy
p.system_prompt = "You are a pirate.";
agent.ChangeParamsAsync("answer", std::move(p)).get(); // or .then(...)
// Relax the retrieval threshold (synchronous).
RetrieveParamsT rp = knowledgeBaseline; // caller-owned copy
rp.threshold = 0.9f;
agent.ChangeParams("knowledge", std::move(rp));
The Python client does not cache a baseline copy of authored params. Keep
your own copy of the params you last sent (or authored at create_agent)
and start each mutation from a deepcopy of that local copy.
from copy import deepcopy
from tryll_client.graph import GenerateParams, RetrieveParams, SendAnswer
# Clone the caller-owned baseline, change system_prompt, send.
p = deepcopy(answer_baseline) # caller-owned copy
p.system_prompt = "You are a pirate."
agent.change_params("answer", p)
# Relax the retrieval threshold.
rp = deepcopy(knowledge_baseline)
rp.threshold = 0.9
agent.change_params("knowledge", rp)
# Switch to a single whole-text answer instead of streamed deltas.
gp = deepcopy(answer_baseline)
gp.send = SendAnswer.Whole
agent.change_params("answer", gp)
# Flip a node between a strict "command turn" and free chat by setting or
# clearing its GBNF grammar (see the Constrained Output concept). The
# sampler rebuilds the grammar each turn, so the change takes effect next send.
gp = deepcopy(answer_baseline)
gp.grammar = 'root ::= "yes" | "no"' # constrain; "" clears back to free chat
agent.change_params("answer", gp)
change_params raises a TryllError on failure (unknown node, structural
field changed, or invalid value). A malformed GBNF grammar is rejected with
InvalidParamValue (3007) and the previous grammar is kept.
system_prompt and KV-cache rewind¶
Changing system_prompt does not immediately flush the KV cache.
The change is stored and picked up on the next send_message call.
At that point the projection pipeline detects the new token sequence,
trims the stale tail of the cache, and re-decodes only the changed
prefix. This is the most efficient path: back-to-back change_params /
send_message calls impose only one re-decode, not two.
Error codes¶
| Code | Name | Cause |
|---|---|---|
3004 |
AgentBusy |
A turn is actively running (not idle and not paused). Wait for TurnComplete, or call ChangeParams while parked at a pause. |
3005 |
UnknownNode |
node_name does not match any node instance name in the graph. |
3006 |
ParamNotMutable |
A structural field was included in the mutation request. |
3007 |
InvalidParamValue |
A mutable field failed schema-range or node-specific validation. |