Esta seção define os pilares arquiteturais, regras de design e estratégias que governam todo o projeto.
Nota: Para o histórico evolutivo e justificativas de design (Log de Decisões), consulte DECISIONS.md.
Tecnicamente, o Trellis é um Reentrant Deterministic Finite Automaton (DFA) with Controlled Side-Effects.
ActionCallTool), garantindo que a lógica de transição permaneça pura e testável.O Core da Trellis não conhece banco de dados, não conhece HTTP e não conhece CLI. Ele define Portas (Interfaces) que o mundo externo deve satisfazer. Essa arquitetura desacoplada torna o Trellis leve o suficiente para ser embutido em CLIs simples ou usado como biblioteca “low-level” dentro de frameworks de Agentes de IA maiores.
A API primária para interagir com o engine.
Engine.Render(state): Retorna a view (ações) para o estado atual e se é terminal.Engine.Navigate(state, input): Computa o próximo estado dado um input.Engine.Inspect(): Retorna o grafo completo para visualização.Engine.Name: Nome/Rótulo identificador do grafo (útil para logs e introspecção).As interfaces que o engine usa para buscar dados.
GraphLoader.GetNode(id): Abstração para carregar nós. O Loam implementa isso via adapter.GraphLoader.ListNodes(): Descoberta de nós para introspecção.Interface experimental para “Durable Execution” (Sleep/Resume).
StateStore.Save(ctx, sessionID, state): Persiste o snapshot da execução.StateStore.Load(ctx, sessionID): Hidrata uma sessão anterior.Interface para controle de concorrência em ambiente distribuído (v0.7).
DistributedLocker.Lock(ctx, key, ttl): Adquire lock distribuído (ex: Redis).The pkg/session package acts as the orchestrator for state durability. It wraps the StateStore to add concurrency control (locking) and lifecycle management (atomic “Load or Create”).
Hybrid Locking Strategy (Process + Distributed):
To balance performance and safety, the Manager uses a Two-Level Locking strategy:
sync.Mutex): Prevents race conditions between goroutines within the same process instance. Cheap and fast.The Distributed Lock is acquired lazily only inside critical sections (Load/Save), wrapped by the Local Mutex execution.
Deferred Unlock (Best Effort Release):
The engine ignores (but logs) errors during the lock release (Unlock) phase inside a defer.
Concurrency Strategy (Reference Counting): To prevent memory leaks in high-traffic scenarios, the Manager uses a Reference Counting mechanism for session locks. Locks are created on demand and automatically deleted when the reference count drops to zero.
sequenceDiagram
participant Caller
participant Manager (Global)
participant Entry (Ref)
Caller->>Manager (Global): Acquire(ID)
Manager (Global)->>Manager (Global): Lock Global -> Inc Ref -> Unlock Global
Manager (Global)-->>Caller: Entry (Ref)
Caller->>Entry (Ref): Lock()
Note right of Caller: Critical Section
Caller->>Entry (Ref): Unlock()
Caller->>Manager (Global): Release(ID)
Manager (Global)->>Manager (Global): Lock Global -> Dec Ref -> Del if 0 -> Unlock Global
graph TD
Host[Host Application / CLI] -->|Driver Port| Engine
MCP[MCP Client / Inspector] -->|Driver Port| Engine
subgraph "Trellis Core"
Engine[Engine - Runtime]
Domain[Domain - Node, State]
end
Engine -->|Driven Port| Loader[GraphLoader Interface]
Loader -.->|Adapter| Loam[pkg/adapters/loam]
Loader -.->|Adapter| Memory[pkg/adapters/memory]
Loader -.->|Adapter| GoDSL[pkg/dsl]
Host -->|Uses| Store[StateStore Interface]
Store -.->|Adapter| File[pkg/adapters/file]
Store -.->|Adapter| Redis[pkg/adapters/redis]
Store -.->|Adapter| Memory[pkg/adapters/memory]
trellis/
├── cmd/
│ └── trellis/ # Entrypoint (CLI)
├── internal/ # Detalhes de implementação (Privado)
│ ├── presentation/ # TUI & Renderização
│ ├── runtime/ # Engine de execução
│ └── validator/ # Lógica de validação
├── pkg/ # Contratos Públicos (Safe to import)
│ ├── adapters/ # Adaptadores (File, Redis, Loam, HTTP, MCP)
│ ├── domain/ # Core Domain (Node, State)
│ ├── ports/ # Interfaces (Driver & Driven)
│ ├── registry/ # Registro de Ferramentas
│ ├── runner/ # Loop de Execução e Handlers
│ └── session/ # Gerenciamento de Sessão e Locking
└── go.mod
O sistema impõe restrições explícitas para prevenir a “Complexidade Oculta”:
A lógica complexa nunca deve residir no grafo (Markdown).
condition: user.age > 18 && user.status == 'active' (Exige parser complexo).condition: is_adult_active (O Host resolve e retorna bool).Veja Interactive Inputs para detalhes sobre como o Host gerencia inputs.
O compilador deve ser implacável.
O Trellis segue a filosofia Convention over Configuration para o início do fluxo.
start.loam.Loader busca por um arquivo chamado start.md (ou start.json) na raiz do diretório.jump_to: modules/auth), o engine busca por modules/auth/start.md.Nota: Embora seja possível injetar um
Stateinicial diferente via código (engine.Navigate(ctx, customState, input)), a CLI e os Runners padrão assumemstartcomo entrypoint.
Com a introdução do StateStore, o ciclo de Hot Reload tornou-se “Stateful”. Ao detectar uma mudança, o Engine é recarregado, mas o Runner tenta reidratar o estado anterior.
sequenceDiagram
participant W as File Watcher
participant C as CLI (RunWatch)
participant E as Engine (New)
participant S as SessionManager
participant R as Runner
Note over W, R: Loop de Desenvolvimento
W->>C: Change Detected
C->>E: Initialize New Engine
alt Compile Error
C->>C: Log & Wait for fix
else Success
C->>S: LoadOrStart(sessionID)
S->>C: Return InitialState
C->>C: Validate Node exists & Context
C->>R: Run(Engine, InitialState)
end
Estratégias de Recuperação (Guardrails):
start se o nó atual for removido.required_context surgirem sem dados na sessão.WaitingForTool se o nó mudar de tipo.O Trellis adota Semantic Versioning (SemVer). Durante a fase inicial (v0.x), priorizamos a agilidade e a evolução da API. A partir da v1.0.0, seguiremos uma política estrita:
Nota sobre Module Fatigue: Para evitar a complexidade de gestão de múltiplos módulos Go (ex:
/v2), o Trellis foca em evoluir dentro do lifecycle da v1 pelo maior tempo possível, utilizando deprecations claras e guias de migração.
Esta seção mapeia os trade-offs arquiteturais assumidos na versão 0.6 para garantir leveza e robustez.
Para resolver vazamentos de memória sem um Garbage Collector pesado, o pacote pkg/session utiliza Reference Counting:
Acquire/Release. Um erro do desenvolvedor (panic fantasma ou defer ausente) pode criar um vazamento permanente para aquele ID.Manager usa um Global Mutex (mu) para proteger o mapa de locks. Em concorrência extrema (>100k Lock/Unlock ops/sec), este lock global torna-se um ponto de contenção.Manager suportaria sharding (ShardCount).O Adaptador Redis evita workers em background (“Serverless-friendliness”):
List() limpa entradas expiradas do Índice ZSET.List() for chamado raramente, o índice ZSET pode conter “Entradas Zumbis” (IDs cujas chaves reais já expiraram) até a próxima listagem.List() incorre uma penalidade de escrita (ZREMRANGEBYSCORE).file.Store) nunca deleta sessões .json antigas automaticamente.trellis session rm) ou jobs externos (cron). Nenhuma lógica de auto-pruning existe dentro do binário para mantê-lo simples.Para garantir a estabilidade do Core enquanto o projeto evolui, definimos uma pirâmide de testes rígida:
internal/runtime (Engine), internal/validator, pkg/session (Concurrency), pkg/runner (Execution Loop).pkg/adapters/* (Abrangendo Loaders, Stores e Protocols).loam vs memory (Graph), file vs redis (State Store).GraphLoader, StateStore) respeitem o mesmo contrato comportamental.tests/ (exercita cmd/trellis externamente).cmd -> runner -> engine -> fs). O arquivo tests/certification_test.go é a fonte da verdade para a conformidade do engine.Esta seção detalha o funcionamento interno do engine, ciclo de vida e tratamento de dados.
O Engine segue um ciclo de vida estrito de Resolve-Execute-Update para garantir previsibilidade.
sequenceDiagram
participant Host
participant Engine
participant Loader
Host->>Engine: Render(State)
Engine->>Loader: GetNode(ID)
Loader-->>Engine: Node
Engine->>Engine: Interpolate Content & Tool Args (Deep)
Engine-->>Host: Actions (View/ToolCall)
Host->>Host: User Input / Tool Result
Host->>Engine: Navigate(State, Input)
Engine->>Loader: GetNode(ID)
Loader-->>Engine: Node
rect rgba(77, 107, 138, 1)
note right of Engine: Update Phase
Engine->>Engine: Apply Input (save_to) -> NewState
end
rect rgba(82, 107, 56, 1)
note right of Engine: Resolve Phase
Engine->>Engine: Evaluate Conditions (Transitions)
end
Engine-->>Host: NextState (with new ID)
Fases do Ciclo:
save_to estiver definido).Na versão 0.7, o Engine adotou a semântica de “Actions Universais”, removendo a necessidade estrita de definir type: tool. O comportamento do nó é inferido por suas propriedades:
do, executa uma ferramenta.wait ou input_type, aguarda input do usuário.content (ou corpo Markdown), renderiza texto.Futuro (DSL): Para ver como o Trellis evoluirá para suportar “Macro Nodes” (
type: flow) e sintaxe mais compacta via um Compilador de Grafo, consulte docs/architecture/dsl_compiler.md.
Padrões e Restrições:
text) + init_db (do).do E wait.WaitingForTool e WaitingForInput) simultaneamente.No modo watch, o Runner orquestra o recarregamento do motor e a reidratação do estado usando um SignalContext hierárquico.
sequenceDiagram
participant W as Watcher (fsnotify)
participant O as Orchestrator (internal/cli)
participant S as SignalContext
participant R as Runner (pkg/runner)
Note over W, R: Ciclo de Hot Reload (Signal-Aware)
W->>O: Evento: file.md alterado
O->>S: Cancel(Reload)
S->>R: ctx.Done() propagado
par Graceful Shutdown
R->>R: Interrompe IO (Stdin Block)
R-->>O: Retorna ctx.Err() (Reload)
and UI Update
O->>O: Log "Change detected in file.md"
end
O->>O: Aguarda estabilização (100ms)
O->>S: NewSignalContext()
O->>R: Nova Iteração: Run(newCtx, engine, state)
R->>R: Resume at 'CurrentNode'
Estratégias de Recuperação (Guardrails):
WaitingForTool, mas o nó foi alterado para text (ou deletado), o motor reseta o status para Active para evitar travamentos.logger.Error.watch, se nenhum ID de sessão for fornecido, um ID determinístico baseado no hash do caminho do repositório (watch-<hash>) é gerado para evitar colisões entre projetos.O protocolo de side-effects permite que o Trellis solicite a execução de código externo (ferramentas) de forma determinística e segura.
O Trellis trata chamadas de ferramenta como “Chamadas de Sistema” (Syscalls). O Engine não executa a ferramenta; ele pausa e solicita ao Host que a execute.
tool e emite uma ação CALL_TOOL.WaitingForTool, aguardando o resultado.Navigate passando o ToolResult. O Engine retoma a execução verificando transições baseadas nesse resultado.sequenceDiagram
participant Engine
participant Host
participant External as "External API/Script"
Note over Engine: Estado: Active (Node A)
Engine->>Host: Render() -> ActionCallTool(ID="tool_1", Name="calc", Args={op:"add"})
Note over Engine: Estado: WaitingForTool (Pending="tool_1")
Host->>External: Executa Ferramenta (Async)
External-->>Host: Retorna Resultado (ex: "42")
Host->>Engine: Navigate(State, Input=ToolResult{ID="tool_1", Success=true, Result="42"})
Note over Engine: Valida ID & Resume
Engine->>Engine: Avalia Transições do Node A (ex: if input == "42")
Engine->>Host: NewState (Node B)
Graças a este desacoplamento, a mesma definição de grafo pode usar ferramentas implementadas de formas diferentes dependendo do adaptador:
.sh, .py) ou funções Go embutidas.tools.yaml ou inline (x-exec).
TRELLIS_ARGS (JSON unificado).You can define available tools directly in the Node’s frontmatter. This allows the Engine to be aware of the tool’s schema (name, description, parameters) without needing hardcoded Go structs.
type: text
tools:
- name: get_weather
description: Get current temperature
parameters:
type: object
properties:
city: { type: string }
---
The weather is...
To support modularity, the tools key in Frontmatter is polymorphic. It accepts both inline definitions and string references to other files.
tools:
- name: local_tool # Inline Definition
description: ...
- "modules/tools/math.md" # Reference (Mixin)
The loam.Loader implements a recursive resolution strategy with Shadowing (Last-Write-Wins).
flowchart TD
Start([Resolve Tools]) --> Init[Init Visited Set]
Init --> Iterate{Iterate Items}
Iterate -->|String Import| CheckCycle{Cycle?}
CheckCycle -- Yes --> Error(Error: Cycle Detected)
CheckCycle -- No --> Load[Load Referenced File]
Load --> Recurse[Recursive Resolve]
Recurse --> MergeImport[Merge Imported Tools]
MergeImport --> Iterate
Iterate -->|Map Definition| Decode[Decode Inline Map]
Decode --> MergeInline[Merge/Shadow Tool]
MergeInline --> Iterate
Iterate -- Done --> Flatten[Flatten Map to List]
Flatten --> End([Return Tool List])
style MergeInline stroke:#f66,stroke-width:2px,color:#f66
style MergeImport stroke:#66f,stroke-width:2px,color:#66f
Technical Constraints:
[]any): The Loader accepts generic types to support this UX. This requires manual schema validation at runtime.visited set).O Trellis garante a execução at-most-once para Efeitos Colaterais (Tool Calls) usando chaves determinísticas.
O Contrato:
IdempotencyKey.SessionID + NodeID + StepIndex + ToolName.Diagrama de Sequência:
sequenceDiagram
participant E as Engine
participant S as State
participant T as Tool (Side Effect)
E->>E: Render(State)
E->>S: Get History Length (Simulation Step)
E->>E: Generate Hash(SessionID, NodeID, StepIndex, ToolName)
E->>T: Call(Args, Metadata["idempotency_key"])
Note over T: External System (e.g., API, DB)<br/>deduplicates using Key
O Trellis suporta o Padrão SAGA nativamente, permitindo transações distribuídas confiáveis sem um coordenador de banco de dados central.
Toda “Ação” (Efeito Colateral) pode ter uma “Reversão” (Transação Compensatória) correspondente definida diretamente no nó.
type Node struct {
Do *ToolCall // A Ação Primária (ex: Cobrar Cartão)
Undo *ToolCall // A Ação Compensatória (ex: Estornar Cartão)
}
Isso garante Localidade de Comportamento: o código que reverte uma ação reside ao lado da própria ação.
Quando uma ferramenta falha com on_error: rollback, OU quando um nó transita explicitamente para to: rollback, o Engine entra em Modo Rollback:
undo, o Engine a executa.Garantia de Ciclo de Vida: O Engine garante que
OnNodeLeaveseja emitido para o nó que iniciou o rollback (seja por erro ou transição) antes que a sequência de rollback comece, assegurando observabilidade consistente.
sequenceDiagram
participant Engine
participant Host
Note over Engine: State: Active (Step 2)
Engine->>Host: Execute "Ship Item"
Host-->>Engine: Error ("Out of Stock")
Note over Engine: Transition: on_error: rollback
Engine->>Engine: Status = RollingBack
Engine->>Engine: Pop Step 2 (Failed)
Engine->>Engine: Pop Step 1 (Completed "Charge")
Note right of Engine: Step 1 has Undo "Refund"
Engine->>Host: Execute "Refund"
Host-->>Engine: Result "Refunded"
Engine->>Engine: Pop Start
Note over Engine: State: Terminated
O Trellis suporta nativamente a orquestração de processos assíncronos sem violar seu modelo determinístico, delegando a gestão temporal ao Host/Runner.
Success: true para o Engine. O Engine não bloqueia.PENDING. O Engine entra em estado WaitingForCallback (novo estado proposto) ou permanece em WaitingForTool com flag de persistência.Navigate(ToolResult) quando o evento ocorre.sidecars).ProcessAdapter avançado mantém subprocessos vivos e converte sys.exit ou stdout em eventos (signals) que transicionam o grafo (ex: on_signal: process_crash -> restart).Para suportar UX rica em YAML (objetos aninhados) mantendo o Domínio Core simples (map[string]string), o loam.Loader implementa uma Estratégia de Achatamento (Flattening).
Problema: O domain.ToolCall.Metadata do Core é estritamente um map[string]string para garantir protocolos de serialização planos (HTTP Headers, JSON simples). No entanto, usuários querem definir configurações complexas como x-exec naturalmente no YAML.
Solução: O Adaptador aceita map[string]any e o achata recursivamente usando notação de ponto (ou traço para prefixos específicos) antes de criar o Nó de Domínio.
Exemplo:
YAML Input:
metadata:
x-exec:
command: python
args: ["main.py"]
Domain Representation:
Metadata: {
"x-exec-command": "python",
"x-exec-args": "main.py"
}
O Runner serve como a ponte entre o Engine Core e o mundo externo. Ele gerencia o loop de execução, lida com middleware e delega IO para um IOHandler.
A partir da v0.7.5, o Runner foi refatorado para implementar a interface lifecycle.Worker (Run(context.Context) error), tornando-o compatível com supervisores e gerenciadores de processos da biblioteca lifecycle. O Runner agora é stateful (encapsula Engine e State inicial) e single-use.
Nota: O Runner é instanciado com todas as suas dependências (Engine, Initial State) e executa até a conclusão ou erro. Ele não deve ser reutilizado.
sequenceDiagram
participant CLI
participant Runner
participant SessionManager
participant Engine
participant Store
Note over CLI: PrintBanner() (Branding)
CLI->>Router: Start(Background)
Note right of Router: Captures Signal & Input
CLI->>Runner: Run(sessionID)
Runner->>SessionManager: LoadOrStart(sessionID)
SessionManager->>Store: Load(sessionID)
alt Session Exists
Store-->>SessionManager: State
else New Session
SessionManager->>Engine: Start()
Engine-->>SessionManager: State
SessionManager->>Store: Save(InitialState)
end
SessionManager-->>Runner: State
loop Execution Loop
Runner->>Engine: Render(State)
Engine-->>Runner: Actions (Text/Tools)
Runner->>CLI: Output / Wait Input
Note right of CLI: Router feeds Input Event
CLI-->>Runner: Input
Runner->>Engine: Navigate(State, Input)
Engine-->>Runner: NewState
Runner->>Store: Save(NewState)
end
Note over CLI: logCompletion(nodeID)
Note over CLI: handleExecutionError()
O Trellis suporta dois modos primários de operação:
TextHandler): Para uso interativo TUI/CLI. Bloqueia no input do usuário através de um canal (inputChan). Suporta a opção WithStdin() para leitura direta de os.Stdin em aplicações autônomas.JSONHandler): Para automação headless e integração de API.Restrição Chave para Modo JSON:
JSONHandler devem ser strings JSON de linha única.signals.Context().Done(): Sinal de Usuário Explícito (SIGINT). Mapeia para "interrupt".ctx.Done() (Parent): Orquestrador Externo (Watch Reload). Tratado como Saída Limpa (sem mapeamento de sinal).inputCtx.Done() (Deadline): Mapeia para "timeout".O comportamento de nós de texto segue a semântica de State Machine pura:
type: text): São, por padrão, Non-Blocking (Pass-through) para o Engine.
wait: true.wait: true: Força pausa para input (ex: “Pressione Enter”) em ambos os modos.type: question: Pausa explícita aguardando resposta (hard step).flowchart TD
Start([Engine.Render]) --> Content{Has Content?}
Content -- Yes --> EmitRender[ActionRenderContent]
Content -- No --> CheckInput
EmitRender --> CheckInput
CheckInput{Needs Input?}
CheckInput -->|wait: true| YesInput
CheckInput -->|type: question| YesInput
CheckInput -->|input_type != nil| YesInput
CheckInput -->|"Default (Pass-through)"| AutoNav[Navigate - State, Empty]
YesInput --> EmitRequest[ActionRequestInput]
EmitRequest --> Stop([Runner Pauses])
AutoNav --> Result{Status?}
Result -- Terminated --> Stop
Result -- Active --> Next([Next State - Loop])
O tratamento de input em Go, especialmente com os.Stdin, é bloqueante por natureza. O pacote lifecycle, através do InputSource, abstrai o padrão Stdin Pump, garantindo que leituras sejam não-bloqueantes e canceláveis via Contexto, evitando “leitores fantasmas”. O TextHandler do Trellis agora atua apenas como consumidor desses eventos pré-processados.
pump) lê do Reader subjacente eternamente.string ou error) são enviados para events.Router.NewInteractiveRouter despacha esses eventos para os handlers registrados.flowchart LR
Stdin[os.Stdin] -->|ReadString| Pump((Pump Goroutine))
Pump -->|inputResult| Chan[inputChan]
subgraph "Input(ctx) Call"
Chan -->|Select| Consumer[Runner]
Timer[Context Timeout] -.->|Cancel| Consumer
end
Consumer -->|Sanitized Input| Engine
Stewardship Note: This pattern prevents multiple goroutines from fighting over
bufio.Reader. TheRunnerautomatically memoizes the handler instance to ensure that reusing aRunnerinstance also reuses the single Pump goroutine.
No Windows, o comportamento padrão do os.Stdin difere significativamente do Unix. Pressionar Ctrl+C frequentemente fecha o stream Stdin imediatamente (enviando io.EOF) antes que o handler de sinal do SO possa interceptar a interrupção. Isso leva a uma condição de corrida onde a aplicação trata a interrupção como um simples End-Of-File ou “User Quit” em vez de um sinal.
A Solução:
Para mitigar isso, a biblioteca lifecycle (pkg/termio) detecta se está rodando em um Terminal Windows e, se sim, abre CONIN$ diretamente. Isso é feito transparentemente pelo NewInteractiveRouter, garantindo robustez de sinais e input em todas as plataformas.
Para manter a arquitetura limpa, diferenciamos onde cada responsabilidade reside:
LifecycleHooks (OnNodeEnter, OnTransition).StateStore (Persistência), ToolInterceptor (Segurança), SignalManager (Interrupção).Essa separação garante que o Core permaneça uma Máquina de Estados Pura e Determinística, enquanto o Runner assume a responsabilidade pela “sujeira” (Timeouts, Discos, Sinais de SO).
.trellis/sessions/ no diretório de trabalho atual..git ou .terraform), facilitando o desenvolvimento e evitando colisões globais em ambientes multi-projeto.Para gerenciar o ciclo de vida dessas sessões persistentes, o Trellis expõe comandos administrativos (“Chaos Control”):
ls): Enumera sessões ativas no workspace.rm): Permite “matar” sessões travadas ou limpar o ambiente.Essa camada é crucial para operações de longa duração, onde “desligar e ligar de novo” (resetar o processo) não é suficiente para limpar o estado.
Maintenance Note: O file.Store não implementa Auto-Pruning (limpeza automática) de sessões antigas. Cabe ao administrador ou desenvolvedor executar
trellis session rmperiodicamente ou configurar scripts externos de limpeza (cron) se o diretório de sessões crescer excessivamente.
Embora o File Store permita durabilidade, ele impõe restrições arquiteturais específicas:
O Trellis adota a propriedade save_to para indicar a intenção de persistir a resposta de um nó no contexto da sessão.
type: question
text: "Qual é o seu nome?"
save_to: "user_name" # Salva input em context["user_name"]
Regras de Execução:
save_to armazena o input como recebido (any).O Trellis adota uma arquitetura plugável para interpolação de variáveis via interface Interpolator.
strings.ReplaceAll (``).O Trellis força Strict Mode em todos os adaptadores para resolver o problema do float64 em JSON. Números são decodificados como json.Number ou int64 para garantir integridade de IDs e timestamps.
Serialização Padrão (Snake Case):
Para garantir interoperabilidade, o Engine serializa seu estado para JSON usando chaves em snake_case (ex: current_node_id, pending_tool_call), independentemente da nomeação interna das structs em Go. Isso permite integração mais limpa com ferramentas externas e inspeção manual de sessão.
Fail Fast (Required Context):
Nós servem como fronteiras de dados e podem impor contratos de execução:
required_context:
- user_id
- api_key
Se uma chave estiver faltando, o Engine aborta a execução com ContextValidationError.
Fail Fast (Typed Context):
Para garantir tipagem de dados, um nó pode declarar context_schema:
context_schema:
api_key: string
retries: int
tags: [string]
O Engine valida tipos antes de renderizar o nó e aborta a execução com ContextTypeValidationError
se houver tipos inválidos ou campos ausentes.
Valores Padrão (Mocking):
Nós (convencionalmente start) podem definir valores de fallback para simplificar o desenvolvimento local:
default_context:
api_url: "http://localhost:8080"
Para facilitar testes automatizados e integração, o Trellis permite injetar o estado inicial.
Engine.Start(ctx, initialData map[string]any)--context '{"user": "Alice"}'trellis.WithEntryNode("custom_start") para sobrescrever o ponto de entrada padrão (“start”).initialData (Runtime) > default_context (File).A partir da v0.7.9, o adaptador HTTP suporta notificações em tempo real via Server-Sent Events (SSE). Isso permite que interfaces ricas (Web, Mobile) reajam a mudanças de estado sem polling.
Arquitetura do StreamManager:
O StreamManager gerencia o ciclo de vida das conexões SSE:
GET /events?session_id=....watch=context,history.Navigate ou Signal, ele gera um StateDiff (Delta) e o StreamManager despacha para todos os inscritos daquela sessão.Fluxo de Atualização (Sequence Diagram):
sequenceDiagram
participant UI as Browser / Mobile
participant Srv as HTTP Adapter
participant SM as StreamManager
participant Eng as Engine (Core)
UI->>Srv: GET /events?session_id=123
Srv->>SM: Subscribe(session_123)
SM-->>UI: 200 OK (Stream Open)
Note over UI, Eng: Fluxo de Interação
UI->>Srv: POST /navigate (Input: "next")
Srv->>Eng: Navigate(State, "next")
Eng-->>Srv: NewState
Srv->>Srv: Diff(OldState, NewState) -> Delta
Srv->>SM: Broadcast(session_123, Delta)
SM-->>UI: data: { "current_node_id": "done", ... }
Note over UI: UI reage ao Delta
Garantias de Concorrência:
O StreamManager utiliza um sync.RWMutex para proteger o mapa de inscritos, garantindo que o Broadcast (leitura do mapa) possa ocorrer em paralelo com novas inscrições, enquanto a remoção de clientes desconectados (escrita) é serializada com segurança.
Recursos avançados para escalabilidade, segurança e integração.
Para escalar fluxos complexos, o Trellis suporta Sub-Grafos via organização de diretórios.
to: Transição local (mesmo arquivo/contexto).jump_to: Transição para um Sub-Grafo ou Módulo externo (mudança de contexto).modules/checkout/start)./ (forward slash).Atalho para menus de escolha simples.
Para mitigar riscos de execução arbitrária, o Runner aceita interceptadores. (Veja o Security Guide para Criptografia e PII).
type ToolInterceptor func(ctx, call) (allowed bool, result ToolResult, err error)
[y/N]). O trecho metadata.confirm_msg no nó pode personalizar o alerta.Mecanismo robusto para recuperação de falhas em ferramentas. (Veja o Native SAGA Guide para orquestração automática e o Manual SAGA Guide para a abordagem manual).
ToolResult.IsError for true:
save_to (evita context poisoning).on_error ou on_error: "retry".on_error: Transita para o nó de recuperação.Mecanismos para controle de fluxo assíncrono e limites de execução.
Timeouts (Sinal de Sistema):
timeout: 30s (declarativo no nó).on_timeout: "retry_node" (Sugar) ou on_signal: { timeout: ... }.context.DeadlineExceeded automaticamente para o sinal "timeout"."timeout" não for tratado via on_signal, o Runner encerra a execução com erro (timeout exceeded). Isso evita loops infinitos ou estados zumbis.Sinais Globais (Interrupções):
POST /signal (e.g., Interrupt, Shutdown).on_signal no estado atual.on_signal).fail fast).flowchart TD
Start([Execute Tool]) --> Result{Result.IsError?}
Result -- No --> Success[Apply save_to & Transitions]
Result -- Yes --> HasHandler{Has on_error?}
HasHandler -- Yes --> Recovery([Transition to on_error Node])
HasHandler -- No --> FailFast
style FailFast fill:#f00,stroke:#333,color:#fff
style Recovery fill:#6f6,stroke:#333,color:#000
O namespace sys.* é reservado no Engine.
save_to não pode escrever em sys (proteção contra injeção).O Trellis suporta a conversão de sinais do sistema operacional (ex: Ctrl+C / SIGINT) em transições de estado.
on_signal: Define um mapa de sinais para nós de destino.on_interrupt mapeia para on_signal["interrupt"].type: text
wait: true
on_signal:
interrupt: confirm_exit
Se o sinal “interrupt” for recebido enquanto o nó estiver ativo, o Engine transitará para confirm_exit em vez de encerrar o processo.
Consistency Note: Quando um sinal dispara uma transição, o evento
OnNodeLeaveé emitido para o nó interrompido, mantendo a consistência do ciclo de vida.
O mecanismo de on_signal é a base para extensibilidade do fluxo via eventos:
on_signal: { timeout: "retry_node" } ou on_timeout: "retry_node". (Implementado)on_signal: { interrupt: "exit_node" } ou on_interrupt: "exit_node". (Implementado)on_signal: { payment_received: "success" }. Disparado via POST /signal. (Implementado)context.webhook_data).flowchart TD
Start([User Input]) --> Wait{Waiting Input?}
Wait -- Ctrl+C / Timeout --> Sig[SignalManager: Capture Signal]
InputAPI([API / Webhook]) -.-> Sig
Sig --> Engine[Engine.Signal]
Engine --> Handled{Has on_signal?}
Handled -- Yes --> Leave[Emit OnNodeLeave]
Leave --> Transition[Transition to Target Node]
Transition --> Reset[SignalManager: Reset Context]
Reset --> Resume([Resume Execution])
Handled -- No --> Exit
style Sig fill:#783578,stroke:#333
style Reset fill:#4a4a7d,stroke:#333
style Exit fill:#f00,stroke:#333,color:#fff
Para garantir operação robusta em produção (especialmente em ambientes de memória compartilhada como Pods Kubernetes), o Trellis impõe limites no input do usuário na camada do Runner. Isso se aplica globalmente a todos os adaptadores (CLI, HTTP, MCP).
TRELLIS_MAX_INPUT_SIZE.Veja Deployment Strategies para conselhos de provisionamento.
Responsável por converter visualmente o grafo e estados.
(( ))): Nó inicial ou com ID “start”.[/ /]): Nós que exigem interação do usuário.[[ ]]): Nós que executam efeitos colaterais.[ ]): Nós de texto simples ou lógica interna.⏱️): Anotação visual no label do nó.Arestas e Transições:
-->): Transições padrão.-.->): Transições entre arquivos (jump_to).-. ⚡ .->): Transições disparadas por on_signal.A flag --session <id> permite sobrepor o estado de uma sessão ao grafo estático.
Implementação Atual (v0.6 - “Heatmap”):
A -> B -> A -> C, o grafo mostra A, B e C pintados.B ou Start.Evolução Futura (Vision):
Para debugging forense de falhas complexas (Saga/Loops), o modelo visual precisará evoluir:
🔴 #1, #3) aos nós para indicar a ordem da sequência de passos.sequenceDiagram) pode ser mais legível que um Flowchart, mostrando temporalidade no eixo Y.Decisão Sóbria: Mantivemos a v0.6 simples (Heatmap) pois resolve 80% dos casos (“Onde parei?” e “Por onde passei?”) sem complexidade de renderização dinâmica. É uma ferramenta de Orientação, não de Perícia.
Adaptador REST API (internal/adapters/http).
POST /navigate, GET /graph./events notifica clientes sobre mudanças (Hot-Reload).
fsnotify (via Loam).text/event-stream.sequenceDiagram
participant Dev as Developer
participant Loam as Loam (FileWatcher)
participant Server as HTTP Server
participant Client as Browser (UI)
Client->>Server: GET /events (Inscreve-se)
Server-->>Client: 200 OK (Stream aberto)
Dev->>Loam: Salva arquivo MD/JSON
Loam->>Server: Notifica FileChangeEvent
Server->>Client: Envia SSE: "reload"
Client->>Client: window.location.reload()
Client->>Server: GET /render (Busca estado atualizado)
Expõe o Trellis como um servidor MCP (Model Context Protocol).
navigate, render_state.Para suportar sessões persistentes escaláveis, o adaptador Redis implementa uma estratégia de indexação especializada.
trellis:session:<id>) com um TTL opcional.ZSET (trellis:session:index) rastreia sessões ativas usando o timestamp de expiração como score.List() realiza a manutenção. Ela remove entradas expiradas do índice (ZREMRANGEBYSCORE) antes de retornar sessões válidas.Trade-off: Este design mantém o adaptador stateless (sem necessidade de workers em background), alinhando-se com arquiteturas Serverless. No entanto, significa que
List()incorre um custo de escrita. Para ambientes de alto throughput exigindo listagem somente leitura, este comportamento pode ser desabilitado em favor de um garbage collector externo (Trabalho Futuro).
sequenceDiagram
participant App
participant Adapter
participant Redis(ZSET)
participant Redis(Key)
App->>Adapter: Save(session)
Adapter->>Redis(Key): SET session JSON (TTL)
Adapter->>Redis(ZSET): ZADD index (Score=Now+TTL)
App->>Adapter: List()
Adapter->>Redis(ZSET): ZREMRANGE (Score < Now)
Adapter->>Redis(ZSET): ZRANGE (All)
Redis(ZSET)-->>Adapter: [session_ids]
Adapter-->>App: [session_ids]
O Trellis oferece suporte camadas de middleware para garantir conformidade com políticas de segurança (Encryption at Rest) e privacidade (PII Sanitization).
Para proteger o estado da sessão (que pode conter chaves de API e dados do usuário) em armazenamento não confiável (como disco ou REDIS compartilhado), utilizamos o Envelope Pattern.
O middleware criptografa todo o estado da sessão e o armazena dentro de um “Estado Envelope” opaco.
graph LR
Engine[Engine State] -->|Plain JSON| Middleware[Encryption Middleware]
Middleware -->|"AES-GCM (Key A)"| Cipher[Ciphertext Blob]
Cipher -->|Wrap| Envelope[Envelope State]
Envelope -->|Save| Store[Storage Adapter]
subgraph "Envelope State"
Ctx["__encrypted__: <base64>"]
end
Um middleware separado permite a sanitização de dados sensíveis (Personally Identifiable Information) antes da persistência.
password, ssn, api_key) por ***.***), o que pode impedir a retomada se o fluxo depender desses dados. Use este middleware para Compliance de Logs ou quando a durabilidade do dado sensível não for crítica.O Trellis fornece três camadas de observabilidade, cada uma com propósitos distintos:
OnNodeEnter, OnNodeLeave, OnToolReturn, etc.node_id: ID do nó.tool_name: Nome da ferramenta (nunca vazio).type: Tipo do evento (node_enter, node_leave, tool_call, tool_return).tool_call é preservado para estabilidade histórica de observabilidade, mesmo que o campo do Nó agora seja Do.log/slog e Prometheus sem acoplar essas dependências ao Core (ex: examples/structured-logging).O diagrama abaixo ilustra onde cada evento é emitido durante o ciclo Navigate:
sequenceDiagram
participant Host
participant Engine
participant Hooks
Note over Engine: Start Navigation (Node A)
Engine->>Hooks: Emit OnNodeEnter(A)
Engine->>Engine: Render Content
alt is Tool Call
Engine->>Hooks: Emit OnToolCall(ToolSpec)
Engine-->>Host: ActionCallTool
Host->>Host: Execute Tool
Host->>Engine: Return Result
Engine->>Hooks: Emit OnToolReturn(Result)
end
Engine->>Engine: Update Context (save_to)
Engine->>Hooks: Emit OnNodeLeave(A)
Engine->>Engine: Resolve Transition -> Node B
O Runner implementa a interface TypedWatcher[*domain.State] da biblioteca github.com/aretw0/introspection, permitindo observação do estado interno do Engine durante a execução.
type TypedWatcher[T any] interface {
State() T // Retorna snapshot do estado atual
Watch(ctx context.Context) <-chan StateChange[T] // Stream de mudanças de estado
}
State() *domain.State:
State.Snapshot()).sync.RWMutex para acesso concorrente.lastState já capturado.Watch(ctx context.Context) <-chan StateChange:
droppedCount para futura instrumentação).package main
import (
"context"
"fmt"
"github.com/aretw0/trellis/pkg/runner"
"github.com/aretw0/trellis/pkg/observability"
)
func main() {
r := runner.NewRunner(/* ... */)
ctx := context.Background()
// Agregador consolida múltiplos watchers
agg := observability.NewAggregator()
agg.AddWatcher(r) // Runner implementa TypedWatcher
changes := agg.Watch(ctx)
go func() {
for change := range changes {
state := change.NewState
fmt.Printf("Node: %s | Context: %v\n",
state.CurrentNodeID, state.Context)
}
}()
r.Run(ctx)
}
| Operação | Proteção | Comportamento |
|---|---|---|
State() |
RWMutex.RLock() |
Leituras paralelas permitidas |
Watch() |
Mutex.Lock() |
Registro serializado |
broadcastState() |
RWMutex.RLock() |
Broadcast paralelo às leituras |
| Cleanup (ctx) | Mutex.Lock() |
Remoção copy-and-swap (thread-safe) |
sequenceDiagram
participant Runner
participant Watcher1
participant Watcher2
participant SlowWatcher
Runner->>Runner: State Transition
Runner->>Runner: broadcastState(newState)
par Non-blocking Send
Runner-->>Watcher1: chan <- StateChange
Runner-->>Watcher2: chan <- StateChange
Runner--xSlowWatcher: DROP (chan full)
end
Note over Runner: droppedCount++
Runner->>Runner: Continue Execution
Decisão de Design: O broadcast nunca bloqueia o Runner. Watchers lentos perdem eventos ao invés de stall na execução. Isso preserva o determinismo do Engine e evita deadlocks.
| Camada | Propósito | Uso Típico |
|---|---|---|
| Hooks | Auditoria, Logs, Métricas | Prometheus, OpenTelemetry |
| Visualization | Análise estrutural, Debugging | CI/CD, Documentação |
| Introspection | Dashboards, Debugging interativo | REPL, Web UI, Estado em tempo real |
O ProcessAdapter permite que o Trellis orquestre scripts locais (.sh, .py, .js, etc.) como ferramentas de primeira classe.
Engine -> ToolCall -> ProcessAdapter -> os/exec.Security Model (v0.7 - Strict Registry):
O adaptador segue uma política de “Allow-Listing” rigorosa. Scripts não podem ser invocados arbitrariamente pelo Markdown. O Host Go deve registrar explicitamente quais comandos estão disponíveis.
tool_name -> command + default_args.exec.Command diretamente, evitando sh -c para mitigar injeção de comandos.TRELLIS_ARGS.sequenceDiagram
participant State as Engine State
participant Adapter as ProcessAdapter
participant OS as OS/Shell
participant Script as deployment.py
State->>Adapter: Execute(ToolCall{name="deploy", args={env="prod"}})
Adapter->>Adapter: Lookup "deploy" in Registry
Adapter->>OS: exec("python3 deployment.py", ENV: TRELLIS_ARGS="{...}")
OS->>Script: Run Process
Script-->>OS: Stdout: "Deployment ID: 123"
OS-->>Adapter: Return Stdout
Adapter-->>State: ToolResult{Result="Deployment ID: 123"}