Skip to content

String Storage

A string storage is a session-owned, named container of strings. It exists to back workflow nodes with fixed text content without baking that content into the graph definition at authoring time.

String storages are not embedded and not indexed. For vector retrieval, see embedded string storage.

Kinds

Every string storage has a kind set at creation time; the kind determines what accessors the server exposes to nodes and what validation rules apply.

Kind Description Key uniqueness
List Ordered sequence of plain strings. No keys.
Map Ordered sequence of key → value pairs. Each key must be unique.
Multimap Ordered sequence of key → value pairs. Duplicate keys allowed.

All three kinds share a list view — the server can iterate every value by position regardless of kind. Map and Multimap additionally expose a keyed view (FindFirst, FindAll) that is used by nodes which need to look up values by a string key.

Internal storage order for Map and Multimap

The server stores Map and Multimap entries sorted by key (lexicographic, stable within each key group so that duplicate-key values appear in their original insertion order). This makes FindFirst and FindAll O(log n) binary-search lookups, and guarantees that FindAll returns every matching value even when the keys were not grouped at creation time. The list view (GetByIndex) reflects this sorted order rather than the original construction order.

Current node support

CannedResponseNode and HumanMessageGuardrailNode use only the list view — they iterate all values in order and are not key-aware. Both nodes accept any kind.

IntentToInstructionNode uses the keyed view (FindFirst) and requires Map kind. It is the primary consumer of Map-kind storages today.

Key-aware CannedResponse selection (keyed by a runtime intent label) is planned as future work.

Lifecycle

Step Frame Notes
Create CreateStringStorageRequest Sent before CreateAgentRequest. Server responds with CreateStringStorageResponse.
Use (referenced by node string_storage param) Scoped to the session.
Destroy DestroyStringStorageRequest Server responds with Ack. Agents that already hold the storage keep it alive — the call only removes the session's named reference.

The session drops every storage it owns on disconnect. See Lifetime and Ownership → StringStorage for the full ownership model.

Creation

CreateStringStorageRequest carries these fields:

Field Type Description
name string Unique name within the session. Required.
kind StringStorageKind enum List (0, default), Map (1), or Multimap (2).
strings [string] Inline values. For List: the strings themselves. For Map/Multimap: the values, parallel to keys.
keys [string] Inline keys (Map/Multimap only). Must have the same length as strings.
file_path string Server-side file path. .txt for List; .json for Map/Multimap.

Exactly one content source must be provided:

  • strings alone → List inline.
  • strings + keysMap or Multimap inline.
  • file_path → file-based (kind determines the expected format).

Old clients that do not set kind default to List and keep working without any changes.

File format — List (.txt)

UTF-8 text, one value per line. Lines starting with # and blank lines are ignored:

# Lines starting with # are comments.
# Blank lines are also ignored.

I can't help with that.
I can't assist with that request.

File format — Map / Multimap (.json)

A JSON array of objects with id (key) and text (value) fields. This is the same on-disk shape used by knowledge-base records; a metadata field is accepted but ignored in this context.

[
  {"id": "greet_friendly", "text": "Hello there, traveller!"},
  {"id": "greet_friendly", "text": "Well met!"},
  {"id": "greet_hostile",  "text": "What do you want?"}
]

Duplicate id values are rejected for Map kind and accepted for Multimap kind.

Note

Relative paths in file_path are resolved against the server's current working directory, not the client's. Use absolute paths for portable deployments or rely on server defaults (see below).

Server defaults

If a canned-response or guardrail node is created with no string_storage param and no inline response_N / pattern_N params, the server falls back to a file configured in server-config.json:

  • default_canned_responses_path — default responses (List kind).
  • default_guardrail_patterns_path — default patterns (List kind, regex).

Both files are plain .txt (List kind). There are no JSON defaults.

Resolution order

A canned-response or guardrail node resolves its string source in this order:

  1. Named storagestring_storage param set → look up in the session's storage manager.
  2. Inline paramsresponse_0 / pattern_0 (etc.) set → use as-is.
  3. Server default — load from the configured default file.

Minimum working examples

client.create_string_storage(
    name="refusal_lines",
    strings=[
        "I can't help with that.",
        "That's outside what I can do.",
    ],
)
# Reference "refusal_lines" from a CannedResponse node
# via the `string_storage` param.
client.CreateStringStorage("refusal_lines", {
    "I can't help with that.",
    "That's outside what I can do.",
});
auto* Subsystem = GetGameInstance()->GetSubsystem<UTryllSubsystem>();
Subsystem->RequestCreateStringStorage(
    TEXT("refusal_lines"),
    { TEXT("I can't help with that."),
      TEXT("That's outside what I can do.") });
client.create_keyed_string_storage(
    name="intent_instructions",
    keys=["greet", "farewell"],
    values=["Say hello warmly.", "Say goodbye politely."],
    kind=StringStorageKind.Map,
)
client.CreateStringStorageKeyed(
    "intent_instructions",
    {"greet", "farewell"},
    {"Say hello warmly.", "Say goodbye politely."},
    ::Tryll::StringStorageKind_Map);
auto* Subsystem = GetGameInstance()->GetSubsystem<UTryllSubsystem>();
Subsystem->RequestCreateStringStorageKeyed(
    TEXT("intent_instructions"),
    { TEXT("greet"), TEXT("farewell") },
    { TEXT("Say hello warmly."), TEXT("Say goodbye politely.") },
    1 /* Map */);
await client.RequestCreateKeyedStringStorageAsync(
    "intent_instructions",
    new List<string> { "greet", "farewell" },
    new List<string> { "Say hello warmly.", "Say goodbye politely." },
    Tryll.StringStorageKind.Map);

Errors

Code Cause
7001 Name empty, malformed, or reserved.
7002 A storage with this name already exists in the session.
7003 Content rejected: unreadable file, empty array, bad format, duplicate key in Map, size mismatch between keys and strings.

Client bindings

List (inline) List (file) Map / Multimap (inline) Map / Multimap (file)
C++ CreateStringStorage(name, strings) CreateStringStorageFromFile(name, path) CreateStringStorageKeyed(name, keys, values, kind) CreateStringStorageFromFile(name, path, kind)
Python create_string_storage(name, strings=…) create_string_storage(name, file_path=…) create_keyed_string_storage(name, keys, values, kind) create_string_storage(name, file_path=…, kind=…)
Unreal RequestCreateStringStorage(Name, Strings) RequestCreateStringStorageFromFile(Name, Path) RequestCreateStringStorageKeyed(Name, Keys, Values, Kind) RequestCreateStringStorageFromFile(Name, Path, Kind)
Unity RequestCreateStringStorageAsync(name, strings) RequestCreateStringStorageFromFileAsync(name, path) RequestCreateKeyedStringStorageAsync(name, keys, values, kind) RequestCreateStringStorageFromFileAsync(name, path, kind)

All methods complete asynchronously (C++: blocking with timeout; Python: blocking; Unreal/Unity: Task/callback).
The kind parameter for Unreal uses uint8 ordinals: 0 = List, 1 = Map, 2 = Multimap.
For C++ and Python, use the typed ::Tryll::StringStorageKind_Map / StringStorageKind.Map constants.