Rules are YAML files that pattern-match error contexts. Actions are Python functions that fix them. Together they form theow's memory: once the LLM solves a problem, the rule-action pair fires instantly next time.
Theow's rule system is a production rule system, the same if-then pattern used in classical expert systems like OPS5, CLIPS, and Drools. The when/then contract is inspired by Gherkin syntax from BDD: when declares the conditions under which a rule fires, then declares the actions to execute. Rules can be hand-authored or generated autonomously by the LLM during exploration.
name: fix-missing-config
description: Config file missing from expected path
when:
- fact: stderr
contains: "FileNotFoundError"
- fact: stderr
regex: "No such file.*?(?P<filepath>/[\w/.-]+\.ya?ml)"
then:
- action: restore_config
params:
path: "{filepath}"
tags: [config, filesystem]
collection: defaultFacts are conditions checked against the error context dict. All facts in a rule are ANDed.
Each fact targets a context key (fact) and uses one operator:
| Operator | Description |
|---|---|
equals |
Exact string match |
contains |
Substring match |
regex |
Regex with optional named capture groups ((?P<name>...)) |
Captures from regex facts are passed to action params as {name} placeholders.
when:
# Exact match
- fact: exit_code
equals: "1"
# Substring
- fact: stderr
contains: "connection refused"
# Regex with captures
- fact: stderr
regex: "ModuleNotFoundError: No module named '(?P<module>[^']+)'"Facts can include examples to improve vector search recall without changing the matching logic:
when:
- fact: stderr
regex: "ImportError.*(?P<module>\\w+)"
examples:
- "ImportError: cannot import name 'foo' from 'bar'"
- "ModuleNotFoundError: No module named 'baz'"Examples are embedded alongside the rule description in ChromaDB. They help the resolver find rules for similar but not identical error messages.
Actions execute sequentially. Each references a registered action name and optional params:
then:
- action: install_package
params:
package: "{module}"
- action: restart_service
params:
name: "app"Params support {placeholder} syntax. Placeholders are resolved from regex captures first, then from the full context dict.
tags are string labels for filtering. The resolver can narrow candidates by tag before falling back to semantic search.
collection controls which ChromaDB collection the rule belongs to. Defaults to "default". Use collections to isolate rule sets (e.g. ops, tests, deploy).
Free-text notes field for the LLM to leave context about incomplete rules:
notes: "Handles the basic case. Needs extension for nested configs."Actions live in .theow/actions/ as Python files with the @action decorator:
# .theow/actions/install_package.py
from theow import action
@action("install_package")
def install_package(package: str) -> dict:
"""Install a Python package."""
import subprocess
result = subprocess.run(
["pip", "install", package],
capture_output=True, text=True,
)
if result.returncode != 0:
return {"status": "error", "stderr": result.stderr}
return {"status": "ok", "installed": package}Actions are discovered on startup from .theow/actions/*.py. The decorator name ("install_package") is what rules reference in then.action.
Actions should do one atomic fix and return. Do not run verification or rebuild commands inside the action. The recovery loop handles that.
Register actions directly on the engine instead of using files:
@agent.action("install_package")
def install_package(package: str) -> dict:
"""Install a Python package."""
...Rules without llm_config are deterministic: they run Python actions directly.
Rules with llm_config are probabilistic: they trigger an LLM conversation with a stored prompt instead of running code.
name: fix-complex-config
description: Configuration error requiring analysis
when:
- fact: stderr
contains: "ConfigurationError"
llm_config:
prompt_template: "file://prompts/fix_config.md"
tools: [read_file, write_file]
constraints:
max_tool_calls: 10
max_tokens: 4096
allow_escalation: true
use_secondary: falseprompt_template can be an inline string or file:// reference relative to the .theow/ directory. Placeholders like {stderr} are resolved from context.
tools lists registered tool names the LLM can use. Internal signal tools (_done, _give_up) are added automatically.
use_secondary routes the conversation to llm_secondary instead of the primary model.
-
Exploration creates an ephemeral rule in
rules/ephemeral/. The LLM writes the YAML and action file, calls_test_rule_match()to verify facts, then_submit_rule(). -
Validation checks that all facts match the current context, the action file loads, and no conflicting rule exists.
-
The recovery loop executes the action and re-runs the original function. If it succeeds, the rule is promoted from
rules/ephemeral/torules/. -
On promotion, the rule is indexed in ChromaDB. Next time the same error occurs, the resolver finds it instantly.
-
If the rule fails, it's rejected. With
archive_llm_attempt=True, failed rules are moved torules/failed/with JSON metadata for debugging. Otherwise they're deleted. -
Incomplete rules (budget exhausted before
_submit_rule()) are tagged[incomplete]and kept in ephemeral. The next exploration attempt can continue from them via_list_ephemeral_rules().