This document describes the runtime as an execution system: data model, control flow, persistence, determinism guarantees, and extension points.
ForrestRun executes workflow definitions into durable run records.
Execution contract:
- Load workflow definition.
- Create run record and initial state snapshot.
- Execute step-by-step with retry/branch logic.
- Persist step records and state versions after each step.
- Mark run terminal (
COMPLETED,FAILED, orCOMPLETED_WITH_ERRORS).
The runtime is local-first with SQLite persistence.
Top-level execution record.
Key fields:
run_idworkflow_idworkflow_versionworkflow_hashworkflow_yamlworkflow_stepsinput_hashstatuscreated_at,started_at,completed_aterror
One executed step instance in timeline order.
Key fields:
step_id,step_typeexecution_indexstatusattempt_countinput,outputerror,last_errorstate_before,state_afterstarted_at,finished_at,duration_ms
State manager abstraction used during execution (instead of ad-hoc raw dict mutation).
APIs:
get(key)set(key, value, step_name=None)exists(key)delete(key)snapshot()to_dict()set_step_output(step_name, output)diff(before, after)
Runtime state shape is namespaced:
{
"inputs": {},
"steps": {},
"runtime": {}
}Rules:
inputs: request/context input payload for the run.steps.<step_id>: owned output namespace per step.runtime: runtime-level metadata namespace.
Why this matters:
- prevents cross-step key collisions
- preserves step output ownership
- makes branch and replay analysis tractable
Workflows are YAML with identity metadata and ordered steps.
Workflow metadata:
workflow.idworkflow.version
Workflow-level input declarations:
inputs:declares what the workflow expects from the caller- Supports mapping form (with
description,required,default) or list shorthand inputs_contract(legacy list form) is still supported for backward compatibility- Workflows without input declarations infer available inputs from step references
Step types:
agent— delegates to an agent definition (LLM-backed reasoning)function— calls a plain Python function for deterministic logictool— calls a tool class for external actions
Agent pipeline step types (inside agents/*.yaml) are separate from workflow step types:
model— performs an LLM calltool— executes an allowed tool
Step controls:
inputsmapping from state paths (inputs.issue,steps.a.summary)inputscontract list ([issue, summary]) for declared read dependenciesoutputscontract list ([summary, sentiment]) for declared write boundariesretrypolicy (attempts,backoff,initial_delay)nextbranching rules (when/goto, optionaldefault)
Workflow-level control:
on_error: fail_fast | continue
Versioning behavior:
ai run <workflow_id>resolves latestvNfrom registry scan inworkflows/ai run <workflow_id>@<version>resolves exact versionai run <path/to/workflow.yaml>remains supported- run record stores workflow snapshot (
workflow_yaml) for replay stability even if files change later
The Executor dispatches each step based on its step_type:
Agent steps (type: agent):
- Resolve
AgentDefinitionfrom theAgentRegistryby agent id. - Instantiate
AgentExecutorwith the definition,LLMClient, andToolRegistry. - Run the agent's pipeline using its configured strategy (single, react, or custom).
- Capture
AgentResult(outputs, trace, iterations, token usage). - Write outputs to
steps.<step_id>in state; store trace inStepExecution.agent_trace.
Function steps (type: function):
- Resolve function callable from
functions/directory at parse time. - Build input dict from the step's
inputsmapping. - Call
function_callable(inputs)and capture the returned dict. - Write output to
steps.<step_id>in state.
Tool steps (type: tool):
- Resolve tool from
ToolRegistryby name. - Validate input against
input_schema. - Call
tool.execute(input, context)with runtime context. - Write output to
steps.<step_id>in state.
Agent YAML files live in agents/ and describe LLM-backed reasoning units:
agent:
id: code_reviewer
version: v1
model: gemini/gemini-2.5-flash
system: "You are a senior code reviewer."
strategy:
type: react
max_iterations: 5
tools:
- tools.file
pipeline:
- id: analyze
type: model
prompt: "Analyze this code: {{ inputs.diff }}"
- id: review
type: model
prompt: "Write review based on: {{ analyze.text }}"Key classes (module agent/):
AgentDefinition— parsed agent dataclass with pipeline, strategy, toolsAgentRegistry— discovers and resolves agents fromagents/directoryAgentExecutor— runs the agent pipeline with the configured strategyStrategyConfig— strategy type and parametersPipelineStep— individual step within an agent's pipeline
Strategies:
SingleCallStrategy— run pipeline once linearlyReActStrategy— observe→think→act loop with max iterations- Custom strategies via dotted import path (
strategy: custom+custom_handler)
Functions live in functions/ and are referenced by qualified name:
stubs.generate_summary→functions/stubs.py→generate_summary()formatters.format_markdown→functions/formatters.py→format_markdown()
Resolution happens at parse time (fail fast). Module: function_resolver.py.
Executor uses a pointer (current_step_id) rather than list-only traversal.
Benefits:
- linear flow support
- conditional branching support
- consistent resume semantics
For each step:
- Capture
state_beforesnapshot. - Build step input from
inputsmapping (or snapshot fallback). - Execute with retry policy.
- Validate output structure.
- Write output to
steps.<step_id>namespace. - Capture
state_aftersnapshot. - Persist
StepExecution+ new state version. - Resolve next step via branch rules or sequential fallback.
Step contracts make state boundaries explicit:
- reads =
inputs - writes =
outputs
Validation at workflow load:
- future-read detection for contract inputs
- output collision detection
Validation at runtime:
- output contract enforcement (missing or undeclared keys fail step)
This keeps step interfaces stable as workflows grow.
next rules are evaluated top-to-bottom:
- first
whenevaluatingTruewins - else
defaultif present - else branch resolution failure
Expression context is constrained (state, len, min, max, abs, string methods) to keep evaluation deterministic and safer.
Tools are first-class runtime components.
Interfaces:
ToolToolResultRuntimeContext
ToolRegistry stores tool objects, not plain functions.
Tool execution path includes:
- schema validation (
input_schema) - runtime context injection (
run_id,step_id, state, logger) - optional per-tool timeout/retries
- structured tool events:
TOOL_STARTTOOL_SUCCESSTOOL_ERROR
The runtime automatically discovers tools from the tools/ directory.
Discovery convention: every class in a module that satisfies the Tool protocol
(has name, description, input_schema, and execute) and whose class name
does not start with _ is instantiated and registered.
Classes imported from other modules (e.g. base classes) are skipped via __module__ check.
Built-in tools (tools.echo, tools.http, tools.file, tools.shell) are always available regardless of what's in tools/.
| Tool | Name | Description |
|---|---|---|
EchoTool |
tools.echo |
Returns input message (testing/examples) |
HttpTool |
tools.http |
HTTP/HTTPS requests with scheme validation |
FileTool |
tools.file |
Read/write/append/list files (sandboxed to root) |
ShellTool |
tools.shell |
Execute shell commands with timeout + output capture |
Modules:
tools/base.py—Toolprotocol,ToolResult,RuntimeContexttools/registry.py—ToolRegistrytools/discovery.py—ToolDiscovery,discover_tools(),register_discovered_tools()tools/echo.py— built-inEchoTooltools/http.py— built-inHttpTooltools/file.py— built-inFileTooltools/shell.py— built-inShellTooltools/validation.py— input schema validation
Tables:
runsstepsstate_versions
Properties:
- run metadata is durable
- step timeline is ordered (
execution_index) - full state evolution is queryable (
state_versions+state_before/state_after)
The runtime manages four memory tiers through MemoryManager:
| Tier | Class | Status | Purpose |
|---|---|---|---|
| Working | WorkingMemory |
Implemented | Active context for current execution (scratch store, sliding window, active task) |
| Episodic | EpisodicMemory |
Implemented | Historical run/interaction log (SQLite-backed, configurable summary truncation) |
| Semantic | SemanticMemory |
Implemented | Long-term knowledge store (SQLite + FTS5 full-text search) |
| Procedural | ProceduralMemory |
Implemented | Key/value rule store with SQLite persistence (no auto-learning from episodic history yet) |
Agents may opt into automatic memory injection via the memory_injection: [tier...] field in agents/*.yaml. When set, the requested tiers are formatted into a ## Memory preamble prepended to the system prompt at every model step. Default empty → prompts unchanged.
All tiers implement the MemoryTier protocol:
read(context) -> Dict[str, Any]write(payload) -> None
MemoryManager orchestrates all tiers:
hydrate_state(state)— reads from all tiers intoruntime.memory.<tier>namespace using deep-mergepersist_state(state)— writes state to all tiers after execution_tiers()— yields(name, tier)pairs for iteration_deep_merge()— recursive dict merge utility
Memory hooks are invoked at run start (hydrate) and run end (persist).
WorkingMemory is an in-memory ephemeral store for the current execution:
- Scratch — key-value store with byte budget enforcement (
put/get/remove/clear_scratch) - Entries — deque-based sliding window with
max_entries, auto-eviction - Active task — single current objective dict
write()auto-captures latest step output as a context entryreset()clears all state at run end- Configurable limits:
working_memory_max_entries,working_memory_max_scratch_bytesinRuntimeConfig
EpisodicMemory stores condensed episode records per run. It tracks:
workflow_id,run_id,statusinputs_summary(key names),outputs_summary(step names)error(if failed),created_at
Query API:
record(...)— write a single episode rowrecall(workflow_id, limit)— most recent episodes for a workflowrecall_all(limit)— most recent across all workflows
read() hydrates runtime.memory.episodic with past episodes for the current workflow,
giving steps and agents context about prior runs.
When no db_path is provided, EpisodicMemory operates as an in-memory stub
(backward compatible with tests and default CLI).
SemanticMemory stores long-term knowledge facts with full-text search:
store(key, content, tags, metadata)— insert/update with FTS5 index syncget(key)— exact key lookupdelete(key)— removes fact + FTS index entrysearch(query, limit)— FTS5 MATCH with BM25 ranking, prefix tokenization (tokenize='porter unicode61')search_by_tags(tags, match_all)— tag-based query with match_all/match_any modescount()— total fact count
Protocol-driven: read() looks for runtime.memory.semantic.query in state; write() persists facts from runtime.memory.semantic.store list.
Falls back to in-memory dict when no db_path is provided.
Not yet implemented: vector-similarity retrieval (embeddings + cosine distance) — roadmap item.
ProceduralMemory is a SQLite-backed key/value rule store. Rules are written and read explicitly via runtime.memory.procedural.store. Auto-learning (mining episodic history for reusable patterns) is not yet implemented — that remains on the roadmap.
Modules:
memory/base.py—MemoryTierprotocol andMemoryManagermemory/working.py—WorkingMemorymemory/episodic.py—EpisodicMemorymemory/semantic.py—SemanticMemorymemory/procedural.py—ProceduralMemory
WorkflowRegistry resolves workflow references to loaded workflow definitions.
Resolution rules:
ai run <workflow_id>— scansworkflows/**/*.yamland*.yml, selects highestvNfor matchingworkflow.idai run <workflow_id>@<version>— resolves exact versionai run <path>— direct file load (no registry scan)
Key classes:
WorkflowRegistry— scans directories, registers workflows, resolves by id/versionWorkflowReference— frozen dataclass withworkflow_idandversionparse_workflow_reference(value)— parses"id"or"id@version"strings
Module: workflow_registry.py
All runtime exceptions inherit from RuntimeErrorBase(Exception):
WorkflowValidationError— invalid workflow YAML (bad step ids, missing targets, invalid retry)StepExecutionError— step function, agent, or tool failure during executionToolNotFoundError— tool name not registered inToolRegistryBranchResolutionError— no matchingwhenrule and nodefaultRunNotFoundError— run id not found in storageReplayDataMissingError— incomplete step/state data for replayReplayMismatchError— reconstructed state diverges from recorded state during--verify-stateWorkflowIntegrityError— workflow YAML has been modified since the original run (blocks resume)AgentValidationError— agent definition invalid or missing dependenciesConfigValidationError— runtime configuration is invalidStorageValidationError— storage schema/version is incompatible
Module: errors.py
StructuredLogger emits JSON events to stderr:
info(event, payload)— informational eventserror(event, payload)— error eventsfrom_dataclass(event, obj)— serializes dataclass objects into log entries
Module: logging.py
Key functions in utils.py:
utc_now()— timezone-aware UTC timestampsha256_text(text)/sha256_json(data)— deterministic hashing for workflow and input fingerprintingresolve_path(path, state)— resolves dot-notation paths likesteps.generate_summary.summarybuild_step_input(input_spec, state)— materializes step input from state path mappingsafe_eval(expr, state)— constrained expression evaluation for branch conditions (AST-validated, allowsstate,len,min,max,abs, and string methods)format_template(value, state)— recursive template resolution in input specs
ai resume <run_id>:
- Validate run is resumable (
FAILEDonly). - Validate workflow hash — the runtime compares the stored workflow hash
against the current workflow hash and raises
WorkflowIntegrityErrorif they differ. This prevents resuming a run whose workflow definition has been modified since the original execution. - Determine resume step from recorded history.
- Load latest state snapshot.
- Continue execution from resume step.
Determinism principle:
- completed history is preserved
- resumed traversal uses same branch/step resolution logic
ai replay <run_id> is simulation, not execution.
Replay engine:
- loads run + step history + initial state
- replays step timeline by injecting recorded state transitions
- does not call agents/functions/tools
- optional
--verify-statechecks reconstructed state againststate_before
Use this for postmortems and reproducibility checks.
CLI modes:
inspectsummaryinspect --stepsstep-centric detailsinspect --state-historystate timeline and diff markersstate-diffdeep key-path state changesvisualizerun visualization:--ascii--timeline- default HTML (
.runs/<run_id>/visualization.html)
Diff markers:
+added-removed~changed
Visualization internals:
visualization/run_loader.py: loads run, steps, state, workflow metadatavisualization/graph_builder.py: execution nodes/edges + branch decision reconstructionvisualization/timeline_builder.py: state deltas + step timing/tool detail timelinevisualization/ascii_renderer.py: terminal-friendly viewvisualization/html_renderer.py: local standalone HTML report
Implemented:
- workflow id/version/hash snapshot on every run
- workflow hash + input hash capture
- step output namespacing
- persisted execution ordering
- state snapshots for before/after
- replay from persisted history
Known limitations:
- no full event-sourcing ledger
- no idempotency keys for side-effect tools
- no loop scheduler/loop detector beyond current guards
The runtime loads settings from runtime.yaml (if present), falling back to built-in defaults.
Precedence (highest wins):
CLI flag > runtime.yaml > built-in default
Config fields:
db_path— SQLite database path (default:runtime.db)workflows_dir— workflow YAML directory (default:workflows)tools_dir— tool discovery directory (default:tools)agents_dir— agent definition directory (default:agents)functions_dir— function discovery directory (default:functions)logging.level/logging.format— log configurationoverwrite_policy— state overwrite behavior:warn(default),strict,allowworking_memory_max_entries— max sliding window entries (default: 50)working_memory_max_scratch_bytes— max scratch store size (default: 256KB)episodic_max_summary_bytes— per-row truncation budget for episodic inputs/outputs summaries (default: 512)shell_allowlist/shell_denylist— regex patterns for ShellTool command filteringdefault_llm_provider— fallback provider for ambiguous model referencesllm:— provider configuration block (providers, models, API key env vars)
Module: config.py — RuntimeConfig, load_config(), apply_cli_overrides()
The Executor emits structured events at five lifecycle points via an optional EventCallback:
EventCallback = Callable[[str, Dict[str, Any]], None]Events:
| Event | When | Payload includes |
|---|---|---|
RUN_START |
Run begins | run_id, workflow_id |
STEP_START |
Step begins | run_id, step_id, step_type |
STEP_COMPLETE |
Step succeeds | run_id, step_id, duration_ms, handler_duration_ms/tool_duration_ms |
STEP_ERROR |
Step fails (after retries) | run_id, step_id, error |
RUN_COMPLETE |
Run finishes | run_id, status, total_duration_ms |
Usage: pass on_event to Executor.__init__() or to run_workflow().
Per-step timing captures both total step duration and call-specific latency:
duration_ms— total time from state_before snapshot to state_after persistencehandler_duration_ms— time spent inside agent/function execution (LLM call or function call)tool_duration_ms— time spent inside the tool'sexecute()method (tool steps)
Run-level duration is derived from started_at / completed_at timestamps.
Visualization renderers (HTML, ASCII) and CLI progress output surface these metrics.
Designed for future expansion:
- DAG scheduler and parallel step execution
- Typed state schemas
- Tool permissions and sandbox policies
- State redaction and compression for large payloads
- LLM streaming / token-level events
- Multi-agent composition (sub-workflow invocation)
- PostgreSQL storage backend
- Remote storage (S3 + DynamoDB)
- OpenTelemetry / Prometheus observability
- Vector/embedding search for semantic memory
- Procedural memory pattern extraction
The runtime includes a provider registry for managing LLM backends.
Key classes (module llm/):
LLMProvider— one provider (name, API key env var, base URL, model configs)ModelConfig— one model definition (model_id, temperature, max_tokens, extras)LLMRegistry— central lookup of providers and models
Credential management:
- API keys are resolved from environment variables at call time — never stored on disk.
provider.resolve_api_key()readsos.environ[api_key_env].registry.check_credentials()returns a quick health map.
Configuration (in runtime.yaml):
llm:
providers:
openai:
api_key_env: OPENAI_API_KEY
models:
gpt-4:
temperature: 0.2
max_tokens: 4096
anthropic:
api_key_env: ANTHROPIC_API_KEY
models:
claude-3-opus:
temperature: 0.3The registry is loaded from the llm section of RuntimeConfig during startup
and is available for agents and the LLM client to determine which model to use.
Agent pipeline model steps route requests through the LLMClient, which delegates
to provider-specific adapters:
agent pipeline step (type: model)
→ LLMClient.call()
→ resolve provider/model
→ adapter.call() (HTTP to provider API)
→ LLMResponse (text, usage, raw)
Adapters:
OpenAIAdapter— Chat Completions API (/v1/chat/completions)AnthropicAdapter— Messages API (/v1/messages)GeminiAdapter— Gemini generateContent API
All adapters use stdlib urllib (zero additional dependencies) with exponential-backoff retry on 429/5xx errors.
Status: Implemented — registry, config loading, credential resolution, OpenAI adapter, Anthropic adapter, Gemini adapter, Mock adapter, and LLM client are functional. All adapters use stdlib
urllibwith exponential-backoff retry on 429/5xx errors. Streaming scaffolding (types, SSE parser, adapterstream()methods) is present inllm/streaming.pyandllm/sse.pybut is not wired end-to-end through the strategy or executor — deferred.
Agent definitions (agents/*.yaml) describe LLM-backed reasoning units with
model, strategy, pipeline, tools, and prompt configuration. They are referenced
from workflow steps via type: agent.
Key classes (module agent/):
AgentDefinition— parsed definition dataclassAgentRegistry— discovers and resolves agents from directoryAgentExecutor— runs agent pipeline with configured strategyPromptRegistry— manages versioned prompts for A/B testingai list— list all agents inagents/ai run <agent_id>[@version]— resolves fromagents/, synthesizes a workflow wrapper, and merges defaults with-ioverrides
When ai run <ref> is invoked, the runtime first checks agents/ for a
matching agent.id. If found, the runtime synthesizes a workflow wrapper
for the resolved agent definition and then applies CLI -i inputs.
If no agent matches, the runtime falls back to workflow resolution (file path or
workflow registry).
ai export packages the project (runtime.yaml, workflows/, agents/, functions/, prompts/) into a portable .tar.gz bundle. ai import extracts it with path traversal protection. .env is excluded. There is no ai validate command.
| Capability | Status |
|---|---|
| Core execution engine | Implemented |
| Retry / backoff (fixed + exponential) | Implemented |
| Conditional branching + circular detection | Implemented |
| SQLite persistence (WAL, transactions) | Implemented |
| Resume from failure | Implemented |
| Workflow hash lock on resume | Implemented |
| Deterministic replay | Implemented |
| State diff & visualization (HTML + ASCII) | Implemented |
| Workflow versioning & registry | Implemented |
| Step contracts (input + output) | Implemented |
| Tool subsystem & auto-discovery | Implemented |
| Function resolution (fail-fast) | Implemented |
| LLM provider registry | Implemented |
| Credential resolution (env vars only) | Implemented |
| Agent definition system (pipeline + strategy) | Implemented |
| Project export / import (portable bundles) | Implemented |
| Agent-aware run resolution | Implemented |
Prompt versioning (id@vN) |
Implemented |
| Function step type | Implemented |
| Agent step type | Implemented |
| Tool step type | Implemented |
| LLM adapters (OpenAI, Anthropic, Gemini) with native function calling | Implemented |
| Adapter retry with backoff (429/5xx) | Implemented |
| Async executor with sync wrappers | Implemented |
| Built-in tools (Echo, HTTP, File, Shell) | Implemented |
| Working memory (scratch + sliding window) | Implemented |
| Episodic memory (SQLite) | Implemented |
| Semantic memory (SQLite + FTS5) | Implemented |
| Procedural memory (key/value store) | Implemented |
| Procedural memory auto-learning | Planned |
| Memory injection into agent prompts | Implemented |
| Cost accounting (per-step + per-run, persisted) | Implemented |
| Async-safe sync wrappers (worker-thread bridge) | Implemented |
| Lifecycle hooks (event callbacks) | Implemented |
| Timing telemetry (per-step + run-level) | Implemented |
safe_eval sandboxed expressions |
Implemented |
| Multi-workflow agents | Planned |
| DAG / parallel execution | Planned |
| LLM streaming end-to-end | Deferred (scaffolding present) |
| Vector/embedding search | Planned |
| PostgreSQL storage backend | Planned |
| OpenTelemetry / metrics | Planned |
| Tool permissions / sandboxing | Planned |
| Idempotency keys | Planned |