Skip to content

Latest commit

 

History

History
408 lines (306 loc) · 11.9 KB

File metadata and controls

408 lines (306 loc) · 11.9 KB

The Trellis Guide: Node Syntax Reference

This document is the canonical reference for defining Nodes in Trellis. It reflects the v0.7+ Universal Action Semantics ("Duck Typing").

1. Philosophies

1.1. Everything is a Node

The Node is the atomic unit of the conversation/flow.

  • It can Speak (Text).
  • It can Listen (Input).
  • It can Act (Tool).
  • It can Decide (Transition).

1.2. Behavioral Typing (Duck Typing)

We do not rely on rigid type fields (like type: tool). Instead, the properties you define determine the node's behavior.

  • Has do? -> It is an Action Node. The Engine executes the tool.
  • Has wait or input_type? -> It is an Input Node. The Engine pauses for user input.
  • Has content? -> It renders text (Markdown).

Note: You can mix behaviors, with constraints.

  • Content + Do = "Talk & Act" (e.g. "Loading..." + Init).
  • Content + Wait = "Talk & Listen" (e.g. "Question?" + Input).
  • Forbidden: Do + Wait (in the same node). You cannot act and listen simultaneously (state collision).

2. Anatomy of a Node (YAML/Frontmatter)

type: text              # Optional. Inferred from behavior (Defaults to "text").

# --- Behavior: Action (The "Do") ---
do:
  name: my_tool_name    # Tool to execute
  args:                 # Arguments passed to the tool
    id: "{{ .user_id }}"

# --- Behavior: SAGA (The "Undo") ---
undo:
  name: revert_tool     # Executed if flow rolls back
  args:
    id: "{{ .user_id }}"

# --- Context Management ---
required_context:       # Fails if these keys are missing
  - "user_id"
default_context:        # Fallback values
  theme: "dark"
context_schema:         # Type validation for context values
  api_key: string
  retries: int
  tags: [string]

# --- Behavior: Input (The "Wait") ---
wait: true              # Pauses for simple text input (Enter)
# OR
input_type: confirm     # Pauses for typed input (e.g., [y/N])
input_options: ["A","B"]
save_to: my_variable    # Saves input (or tool result) to Context

# --- Flow Control ---
to: next_node_id        # Unconditional transition
# OR
transitions:
  - condition: input == 'success'
    to: success_node
  - to: fallback_node

on_error: error_handler_node  # Transition if Tool fails

# Signal Handlers
on_timeout: timeout_node      # Syntactic Sugar for on_signal["timeout"]
on_signal:
  interrupt: exit_node        # Handle specific signals from THIS node

on_signal_default:
  interrupt: global_exit      # RECOMMENDED: Define on Root Node to handle signals across the entire flow.
  quit: cleanup_node

timeout: "30s"            # Max time to wait for input

type: format

A technical node used for post-processing or converting content (e.g., Markdown to HTML). It utilizes the configured ContentConverter in the engine.

id: docs_render
type: format
format: markdown
message: |
  # Hello
  This is **markdown**.

Nodes of type format typically have a format property specifying the transformation type.

3. Formatting Rules

3.1. Markdown Body = Content

In Markdown files, any text below the frontmatter is the content.

---
do: init_db
to: menu
---
**Initializing Database...**
Please wait while we set up tables.

3.2. JSON/YAML = Explicit Content

In strictly structured files, use the content key.

id: start
do: 
  name: init_db
content: "Initializing Database..."
to: menu

4. Universal Action Patterns

4.1. Text + Action (The "Zero Fatigue" Pattern)

Render a message and immediately execute a backend task. The transition happens when the task receives a Result.

# Loading Screen
content: "Checking credentials..."
do: check_creds
save_to: auth_result
transitions:
  - condition: input.is_valid
    to: dashboard
  - to: login_fail

How it works:

  1. Engine renders "Checking credentials...".
  2. Engine executes check_creds.
  3. Tool returns result {"is_valid": true}.
  4. Result is saved to auth_result.
  5. Transition condition input.is_valid is evaluated (True).
  6. Engine moves to dashboard.

4.2. Explicit Questions & Options

For interactions that require specific choices, use type: question (optional if behaviors are clear) or input_type: choice.

# Simple Yes/No
content: "Proceed?"
input_type: confirm
save_to: proceed

# Multiple Choice (Syntactic Sugar)
content: "Pick a color:"
options:
  - "Red"
  - "Blue"
transitions:
  - condition: input == 'Red'
    to: red_pill

Note on Options: The options list implies input_type: choice. The engine presents these to the user (e.g. arrow keys in CLI).

4.3. Action Safety (Error Handling)

If an action fails, on_error takes precedence over to.

do: critical_op
on_error: rollback_node
to: success_node
  • Success: Goes to success_node.
  • Failure: Goes to rollback_node.

4.4. Scriptable Tools (v0.7+)

Define ad-hoc scripts inline (requires --unsafe-inline) or via tools.yaml.

# Ad-hoc execution (Dev Mode)
do:
  name: quick_script
  x-exec:
    command: python
    args: ["scripts/calc.py"]

4.5. Tool Arguments via TRELLIS_ARGS (v0.7.10+)

All tool arguments are passed as a single JSON object via the TRELLIS_ARGS environment variable. This provides a unified, language-agnostic interface for tool execution.

Example Flow Node:

do:
  name: greet_user
  args:
    name: "{{ user_name }}"
    greeting: "Hello"
    config:
      debug: true

Tool Implementation (PowerShell):

$TrellisArgs = $env:TRELLIS_ARGS | ConvertFrom-Json
$Name = $TrellisArgs.name
Write-Output "Hello, $Name!"

4.6. Global Signal Handlers (on_signal_default)

Define global signal handlers on your entry node (typically start.md) to handle signals like quit or interrupt from anywhere in the flow.

Example (start.md):

---
id: start
wait: true
to: menu
on_signal_default:
  quit: "finish"
  interrupt: "catch-interrupt"
---
# Welcome

Press Enter to begin...

How It Works:

  1. User triggers a signal (e.g., types /quit or presses Ctrl+C)
  2. Engine checks if the current node has an on_signal handler for that signal
  3. If not found, engine falls back to on_signal_default on the entry node
  4. If found in on_signal_default, transitions to the specified node
  5. If not found anywhere, returns ErrUnhandledSignal

Use Cases:

  • Graceful Exit: Always allow /quit to go to a cleanup node
  • Interrupt Handling: Catch Ctrl+C to save state before exiting
  • Timeout Fallback: Global timeout handler for all idle states

Best Practice: Define on_signal_default on your root node (start) to provide consistent signal handling across your entire flow.

4.7. "Good Citizen" Scripts (Graceful Shutdown)

Trellis v0.7.10+ uses a tiered shutdown strategy (SIGTERM -> Grace Period -> SIGKILL). For tools to benefit from this, they should be "Good Citizens":

  1. Handle Signals: Listen for SIGTERM (and SIGINT for local dev).
  2. Cleanup: On signal, flush buffers, save state, and exit promptly.
  3. Result Delivery: Always write the final result (JSON or text) to stdout.

Example (Python):

import signal
import sys
import json

def handle_shutdown(signum, frame):
    # Perform cleanup here
    sys.exit(0)

signal.signal(signal.SIGTERM, handle_shutdown)

# ... perform tool logic ...
print(json.dumps({"status": "success"}))

5. Property Dictionary

Property Type Description
do ToolCall Definition of side-effect to execute.
wait bool If true, pause for user input (default text).
content string Message to display to the user.
options []string Shorthand for choice input. Presents a menu.
input_type string text (default), confirm, choice, int.
input_default string Default value if user presses Enter.
input_options []string Options for choice input (Low-level).
messages map[string]string Map of locales to content. Used for internationalization. See I18n Guide.
next string The ID of the next node to transition to.
save_to string Context variable key to store Input or Tool Result.
to string Shorthand for single unconditional transition.
transitions []Transition List of conditional paths. Evaluated in order.
on_error string Target node ID if do fails.
on_timeout string Syntactic sugar for on_signal["timeout"].
on_interrupt string Syntactic sugar for on_signal["interrupt"].
on_signal map[string]string Handlers for global signals (interrupt, timeout).
on_signal_default map[string]string Global signal handlers (valid only on Root/Entry node).
tools []Tool Definitions of tools available to this node (for LLMs).
undo ToolCall SAGA compensation action if flow rolls back.
required_context []string Keys that MUST exist in context or flow errors.
default_context map[string]any Default values for context keys if missing.
context_schema map[string]string Type constraints for context values (fail fast on mismatch).
timeout string Duration (e.g. "30s") to wait for input before signaling timeout.

5.1. Context Schema (Typed Flows)

Use context_schema to validate types in the context before a node renders. Supported types:

  • string, int, float, bool
  • Slice types like [string], [int]
context_schema:
  api_key: string
  retries: int
  tags: [string]

If a value is missing or has the wrong type, execution fails with a ContextTypeValidationError.

5.2. The Confirm Convention (Unix Style)

When using input_type: confirm, Trellis follows the standard CLI convention:

  • Empty Input (Enter): Defaults to yes (True) unless an explicit input_default is provided.
  • Strict Validation: Only y, yes, true, 1 (True) or n, no, false, 0 (False) are accepted.
  • Normalization: Input is automatically converted to a canonical yes or no before being saved to save_to or evaluated in transitions.

Implementation Example:

input_type: confirm
input_default: "yes" # Overrides convention to make Enter = True
on_denied: stop_flow
to: continue_flow

6. Template Engine

Node content and tool arguments support Go's text/template syntax for dynamic interpolation.

6.1. Basic Examples

---
to: greet
save_to: username
---
What is your name?
---
to: done
---
Hello, {{ .username }}!

Welcome back, {{ default "friend" .nickname }}.

6.2. Accessing Tool Results

After a tool node executes successfully, the result is automatically added to the context under tool_result.

If the tool returns a JSON object (Map/Dictionary): The fields are flattened into tool_result for direct access, and the call ID is stored in _id:

---
to: next_step
---
## Last Tool Call

- Call ID: {{ .tool_result._id }}
- Status: {{ .tool_result.status }}
- Message: {{ .tool_result.message }}

{{ if eq .tool_result.status "success" }}Operation succeeded.{{ end }}

If the tool returns a scalar (String, Int, etc.): The result is stored in the result field, and the call ID is stored in _id:

- Call ID: {{ .tool_result._id }}
- Result: {{ .tool_result.result }}

6.3. Available Functions

Function Example Description
default {{ default "N/A" .key }} Returns .key if non-zero, otherwise the fallback
coalesce {{ coalesce .a .b .c }} Returns the first non-zero value
toJson {{ toJson .obj }} Serializes to JSON string
index {{ index .map "key" }} Accesses a map by dynamic key (built-in)

For the full reference — including HTMLInterpolator for browser output, reserved keys, and known limitations — see docs/reference/interpolation.md.