Skip to content

Commit 981832a

Browse files
docs: document workflow DSL feature, components, systems, and examples
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 5584002 commit 981832a

5 files changed

Lines changed: 231 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Mix 35+ components to build custom agents without inheritance bloat. The Entity-
110110
- **`SystemPromptConfigSpec`** — Declare system prompts as `${name}` placeholder templates with static strings, callable resolvers, or file paths as sources.
111111
- **`SystemPromptRenderSystem`** — ECS system (recommended priority -20) that resolves all `${name}` placeholders and writes a `RenderedSystemPromptComponent` for LLM callers.
112112
- **`UserPromptNormalizationSystem`** — ECS system (recommended priority -10) that injects trigger templates into outbound user messages and writes a `RenderedUserPromptComponent`. Slash-command skill context and ContextPool entries are injected later at call-time by `prepare_outbound_messages()`.
113+
- **Workflow State & Gates** — Declarative state machine DSL for building stateful agents. Define states, prompt profiles, and transition gates (`has`, `field`, `all_of`, etc.) that drive behavior over multiple ticks.
113114
- **Built-in Placeholders**`${_installed_tools}`, `${_installed_skills}`, `${_installed_mcps}`, `${_installed_subagents}` automatically expand to the current inventory.
114115
- **Provider Extension Seam** — A synchronous, narrow provider protocol (`BuiltinPlaceholderProvider`) for injecting domain-specific context into system prompts. Used by the scratchbook prompt provider.
115116
- **Callable Placeholders** — Pass a `() -> str` callable as a placeholder resolver for dynamic content; must be side-effect-free and return a string.
@@ -251,6 +252,7 @@ The `examples/` directory contains runnable demos for the major patterns in the
251252
| [`mcp_agent.py`](examples/mcp_agent.py) | MCP server integration and namespaced tool usage |
252253
| [`agent_dsl_json.py`](examples/agent_dsl_json.py) | Load multi-agent configuration from JSON file using Agent DSL (dual-mode) |
253254
| [`agent_dsl_markdown.py`](examples/agent_dsl_markdown.py) | Load primary agent + subagent from Markdown files using Agent DSL; demonstrates placeholders, triggers, skills, and subagent registry (dual-mode) |
255+
| [`workflow_agent.py`](examples/workflow_agent.py) | Workflow DSL: two-phase writing assistant with gate-driven `DRAFT→REVIEW→DONE` transitions, prompt profiles, and `@tool`-registered handlers (dual-mode) |
254256
| [`examples/e2e/plan_and_task/`](examples/e2e/plan_and_task/) | Interactive plan→review→execute workflow; recoverable state machine, review-gated planning, artifact persistence, and slash-command dispatch |
255257

256258

@@ -366,6 +368,7 @@ See [`docs/`](docs/) for detailed guides:
366368
- [Serialization](docs/features/serialization.md), World state persistence
367369
- [Logging](docs/features/logging.md), structlog integration
368370
- [Retry](docs/features/retry.md), RetryModel configuration
371+
- [Workflow DSL](docs/features/workflows.md), Declarative state machine and transition gates
369372

370373
### Agent Capabilities
371374
- [Context Management](docs/features/context-management.md), Checkpoint, undo, and compaction

docs/components.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,3 +730,56 @@ from pathlib import Path
730730
world.add_component(agent, WorkspaceBindingComponent(workspace_root=Path("/workspace")))
731731
# When a subagent is spawned, it inherits this binding by default (InheritancePolicy).
732732
```
733+
734+
## Workflow Components
735+
736+
### WorkflowDefinitionComponent
737+
Holds the compiled workflow definition for an entity. This component is NOT serialized by design.
738+
739+
| Name | Type | Description |
740+
| :--- | :--- | :--- |
741+
| `compiled` | `CompiledWorkflow` | The frozen, validated workflow model |
742+
743+
**Used by:** `WorkflowStateSystem`, `WorkflowPromptPlaceholderProvider`
744+
745+
### WorkflowRuntimeComponent
746+
Holds the mutable runtime workflow state for an entity. This component IS serialized.
747+
748+
| Name | Type | Default | Description |
749+
| :--- | :--- | :--- | :--- |
750+
| `current_state_id` | `str` | (required) | The ID of the active workflow state |
751+
| `transition_history` | `list[str]` | `[]` | History of committed transition IDs |
752+
753+
**Used by:** `WorkflowStateSystem`, `WorkflowPromptPlaceholderProvider`
754+
755+
### WorkflowBindingComponent
756+
Binds an agent key to the workflow for this entity. This component IS serialized.
757+
758+
| Name | Type | Description |
759+
| :--- | :--- | :--- |
760+
| `agent_key` | `str` | The key used to look up prompt profiles |
761+
762+
**Used by:** `WorkflowPromptPlaceholderProvider`
763+
764+
### WorkflowGateSnapshotComponent
765+
Records the last evaluated gate snapshot for debugging and logging. This component IS serialized.
766+
767+
| Name | Type | Description |
768+
| :--- | :--- | :--- |
769+
| `state_id` | `str` | The state ID being evaluated |
770+
| `evaluated_at_tick` | `int` | The tick number of evaluation |
771+
| `matched_transition_id` | `str | None` | The ID of the matched transition, if any |
772+
773+
**Added by:** `WorkflowStateSystem`
774+
775+
### WorkflowLastTransitionComponent
776+
Records the most recent committed transition for history and exact-once semantics. This component IS serialized.
777+
778+
| Name | Type | Description |
779+
| :--- | :--- | :--- |
780+
| `from_state_id` | `str` | The source state ID |
781+
| `to_state_id` | `str` | The target state ID |
782+
| `transition_id` | `str` | The ID of the committed transition |
783+
| `tick` | `int` | The tick number when the transition occurred |
784+
785+
**Added by:** `WorkflowStateSystem`

docs/features/workflows.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Workflow DSL
2+
3+
The ECS-based LLM Agent framework provides a declarative Domain Specific Language (DSL) for building stateful agents. Workflows allow you to define a graph of states, prompt profiles, and transition gates that drive an agent's behavior over multiple ticks.
4+
5+
## Architecture Overview
6+
7+
Workflows are implemented as a set of ECS components and a dedicated system:
8+
9+
- **`WorkflowDefinitionComponent`**: Holds the compiled workflow graph.
10+
- **`WorkflowRuntimeComponent`**: Tracks the current state and transition history.
11+
- **`WorkflowBindingComponent`**: Binds an agent key to the workflow.
12+
- **`WorkflowStateSystem`**: Evaluates gates and commits transitions (priority -25).
13+
- **`WorkflowPromptPlaceholderProvider`**: Injects the active state's prompt into the system prompt via the `${_workflow_state_prompt}` placeholder.
14+
15+
## Quick Start
16+
17+
The following example defines a simple two-state workflow where an agent starts in a `DRAFT` state and moves to `REVIEW` once a `DoneMarker` component is attached to the entity.
18+
19+
```python
20+
from ecs_agent.core import World
21+
from ecs_agent.workflows import workflow, install_workflow, has
22+
from ecs_agent.systems.workflow_state import WorkflowStateSystem
23+
from ecs_agent.systems.system_prompt_render_system import SystemPromptRenderSystem
24+
from ecs_agent.prompts.contracts import SystemPromptConfigSpec, PromptTemplateSource
25+
26+
# 1. Define the workflow
27+
spec = workflow(
28+
"my-flow",
29+
initial="DRAFT",
30+
profiles={
31+
"agent": {
32+
"draft_p": "You are in draft mode. Help the user write a plan.",
33+
"review_p": "You are in review mode. Critique the plan.",
34+
}
35+
},
36+
states={
37+
"DRAFT": {
38+
"bind": {"agent": "draft_p"},
39+
"go": {"REVIEW": has(DoneMarker)},
40+
},
41+
"REVIEW": {
42+
"bind": {"agent": "review_p"},
43+
"go": {},
44+
},
45+
},
46+
)
47+
48+
# 2. Setup World and Entity
49+
world = World()
50+
eid = world.create_entity()
51+
52+
# 3. Install workflow and prompt config
53+
install_workflow(world, eid, spec, agent_key="agent")
54+
world.add_component(eid, SystemPromptConfigSpec(
55+
template_source=PromptTemplateSource(inline="${_workflow_state_prompt}")
56+
))
57+
58+
# 4. Register systems
59+
world.register_system(WorkflowStateSystem(priority=-25), priority=-25)
60+
world.register_system(SystemPromptRenderSystem(priority=-20), priority=-20)
61+
```
62+
63+
## Gate Primitives
64+
65+
Gates are expressions evaluated against the entity's components to determine if a transition should fire.
66+
67+
| Primitive | Description |
68+
| :--- | :--- |
69+
| `has(Component)` | Matches if the component is present on the entity. |
70+
| `absent(Component)` | Matches if the component is absent from the entity. |
71+
| `field(Component, "attr") == value` | Matches if the component is present and the attribute equals the value. |
72+
| `all_of([gate1, gate2])` | Matches if all child gates match. |
73+
| `any_of([gate1, gate2])` | Matches if any child gate matches. |
74+
| `not_(gate)` | Negates the child gate. |
75+
76+
```python
77+
from ecs_agent.workflows import has, absent, field, all_of, any_of, not_
78+
79+
# Complex gate example
80+
gate = all_of([
81+
has(StatusComponent),
82+
field(StatusComponent, "value") == "ready",
83+
not_(has(ErrorComponent))
84+
])
85+
```
86+
87+
## Shared Prompt Profiles
88+
89+
Prompt profiles are grouped by `agent_key`. Multiple states can bind to the same profile. When a transition occurs between two states that share the same profile, the `${_workflow_state_prompt}` placeholder remains identical. This prevents unnecessary cache invalidation in the `SystemPromptRenderSystem`, ensuring stable prefix caching for LLM providers.
90+
91+
```python
92+
profiles = {
93+
"main": {
94+
"standard": "You are a helpful assistant.",
95+
"expert": "You are a subject matter expert.",
96+
}
97+
}
98+
99+
states = {
100+
"IDLE": {
101+
"bind": {"main": "standard"},
102+
"go": {"ACTIVE": has(StartFlag)},
103+
},
104+
"ACTIVE": {
105+
"bind": {"main": "standard"}, # Shared profile: no prompt churn on transition
106+
"go": {"DONE": has(EndFlag)},
107+
}
108+
}
109+
```
110+
111+
## System Ordering Contract
112+
113+
For workflows to function correctly, systems must be registered in a specific order. The `WorkflowStateSystem` must run after any systems that mutate components used in gates (like `UserPromptNormalizationSystem` script handlers) and before the `SystemPromptRenderSystem`.
114+
115+
Recommended priorities:
116+
1. **`UserPromptNormalizationSystem`** (priority -30): Processes triggers and runs script handlers that might attach "gate" components.
117+
2. **`WorkflowStateSystem`** (priority -25): Evaluates gates and commits transitions.
118+
3. **`SystemPromptRenderSystem`** (priority -20): Resolves `${_workflow_state_prompt}` based on the newly committed state.
119+
4. **`ReasoningSystem`** (priority 0): Performs LLM inference using the rendered prompt.
120+
121+
## TriggerSpec Interaction
122+
123+
Triggers defined in `UserPromptConfigComponent` can use `action="script"` to run Python code when a keyword or event is matched. These scripts can attach components to the entity, which the `WorkflowStateSystem` will observe in the same tick. This allows slash commands to drive immediate state transitions.
124+
125+
```python
126+
async def handle_activate(world, eid, text):
127+
world.add_component(eid, FlagComponent())
128+
return None
129+
130+
# User types "/activate" -> FlagComponent attached -> Workflow transitions to ACTIVE -> Prompt updates
131+
```
132+
133+
## Checkpoint and Resume
134+
135+
The workflow system is designed to be serializable, with one important exception: the `WorkflowDefinitionComponent`.
136+
137+
- **`WorkflowRuntimeComponent`**: Contains the `current_state_id` and `transition_history`. This component IS serialized and restored.
138+
- **`WorkflowDefinitionComponent`**: Contains the compiled graph. This component is NOT serialized because it may contain non-serializable callables or large static data.
139+
140+
**Resume Contract**: After loading a world from a checkpoint (e.g., via `Runner.load_checkpoint`), the caller must re-install the workflow definition using `install_workflow` or manually adding the `WorkflowDefinitionComponent`. The runtime state will be preserved, and the agent will resume from the correct state.
141+
142+
## Transition Semantics
143+
144+
The `WorkflowStateSystem` evaluates all transitions from the current state in every tick.
145+
146+
- **0 matches**: No transition occurs. The state remains unchanged.
147+
- **1 match**: The transition is committed. `current_state_id` is updated, the transition ID is appended to `transition_history`, and a `WorkflowLastTransitionComponent` is attached for the current tick.
148+
- **>1 match**: This is considered an ambiguous state. The system attaches an `ErrorComponent` and a `TerminalComponent` with the reason `workflow_ambiguous_transition`, halting execution.

docs/systems.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The table below summarizes the recommended priorities for each system. Priority
2323
| RAGSystem | -10 | Retrieves context via vector search before reasoning. |
2424
| SystemPromptRenderSystem | -20 | Resolves `${name}` placeholders from `SystemPromptConfigSpec` and produces a cached `RenderedSystemPromptComponent` on first render. |
2525
| UserPromptNormalizationSystem | -10 | Injects trigger templates into user messages and produces `RenderedUserPromptComponent`. ContextPool injection happens later at call-time. |
26+
| WorkflowStateSystem | -25 | Evaluates workflow gates and commits transitions. |
2627
| SubagentWaitSystem | -5 | Handles `subagent_wait` tool and background session notifications. |
2728
| PromptContextCollectorSystem | 0 | Collects tool/subagent results into the context pool. |
2829
| ToolApprovalSystem | -5 | Filters pending tool calls before execution. |
@@ -135,6 +136,31 @@ world.register_system(UserPromptNormalizationSystem(), priority=-10)
135136

136137
---
137138

139+
## 1c. WorkflowStateSystem
140+
141+
The `WorkflowStateSystem` evaluates declarative gate expressions against an entity's components and commits state transitions in the workflow graph.
142+
143+
- **Constructor**: `__init__(self, priority: int = -25)`
144+
- **Queries**: `WorkflowDefinitionComponent`, `WorkflowRuntimeComponent`
145+
- **Produces**: `WorkflowLastTransitionComponent`, `WorkflowGateSnapshotComponent`
146+
- **Recommended Priority**: -25
147+
148+
### Behavior
149+
The system iterates through all transitions defined for the entity's current state in the `WorkflowDefinitionComponent`. It evaluates each transition's gate expression (e.g., `has(C)`, `field(C, "attr") == value`).
150+
151+
- If exactly one transition matches, the system updates `WorkflowRuntimeComponent.current_state_id`, appends the transition ID to `transition_history`, and attaches a `WorkflowLastTransitionComponent`.
152+
- If zero transitions match, it is a no-op (state remains unchanged).
153+
- If more than one transition matches simultaneously, the system attaches an `ErrorComponent` and a `TerminalComponent` with the reason `workflow_ambiguous_transition`.
154+
155+
### Usage Example
156+
```python
157+
from ecs_agent.systems.workflow_state import WorkflowStateSystem
158+
159+
world.register_system(WorkflowStateSystem(priority=-25), priority=-25)
160+
```
161+
162+
---
163+
138164
## 2. ReasoningSystem
139165

140166
The ReasoningSystem serves as the primary cognitive engine for an entity. It coordinates with an LLM provider to generate text responses and identify necessary tool interactions.

examples/e2e/plan_and_task/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The workflow follows a structured lifecycle:
2323
- **Log Truncation** — Structured log fields `last_user_prompt`, `prompt_text` (user normalization), and `prompt_text` (system render) are truncated to 200 characters to keep logs readable without losing signal.
2424
- **ECS Core**: Uses `SystemPromptRenderSystem`, `UserPromptNormalizationSystem`, `ReasoningSystem`, `ToolExecutionSystem`, and `MemorySystem`.
2525
- **Prompt Configuration**: The planner entity declares `SystemPromptConfigSpec` with `DRAFT_INTERVIEW_SYSTEM_PROMPT`, and `SystemPromptRenderSystem` bridges the rendered value into `LLMComponent.system_prompt` before reasoning.
26+
- **Workflow DSL**: Uses `install_workflow` and `WorkflowStateSystem` (priority -25) to manage the phase graph and automatic prompt-profile selection via `${_workflow_state_prompt}`.
2627
- **State Machine**: Explicit phase transitions managed by `WorkflowStateMachine`.
2728
- **Artifacts**: Durable persistence of plans, state, and execution evidence via `PlanTaskScratchbookAdapter`.
2829
- **Controller**: `PlanController` manages the high-level workflow logic and review gates.

0 commit comments

Comments
 (0)