Find, Create, Critique Cycle¶
This diagram traces a single pass through the FCC workflow as orchestrated by the simulation engine. The entry point is SimulationEngine.run(start_node, initial_payload) at src/fcc/simulation/engine.py:50, which walks a WorkflowGraph one node at a time, binds each node to a persona plus an action, invokes an LLM (or mock), validates the deliverable, and records each turn to a MessageHistory. A developer reading this trace wants to understand where persona prompts are assembled, when the event bus emits lifecycle events, and where a bad LLM response is caught before corrupting state. The spine of the loop is deceptively small — three collaborators do most of the work.
The sequence below shows one end-to-end workflow step: from the engine fetching the next node to the completion event being published.
sequenceDiagram
participant Caller
participant SimulationEngine
participant PersonaRegistry
participant WorkflowGraph
participant WorkflowActionRegistry
participant ActionEngine
participant AIClient
participant ValidationEngine
participant MessageHistory
participant EventBus
Caller->>SimulationEngine: run(start_node, initial_payload)
loop for each node in graph
SimulationEngine->>WorkflowGraph: get_node(node_id)
WorkflowGraph-->>SimulationEngine: WorkflowNode(persona_id, action_id)
SimulationEngine->>PersonaRegistry: by_id(persona_id)
PersonaRegistry-->>SimulationEngine: PersonaSpec
SimulationEngine->>WorkflowActionRegistry: get_action(action_id)
WorkflowActionRegistry-->>SimulationEngine: WorkflowAction
SimulationEngine->>ActionEngine: get_action_prompt(persona, action)
ActionEngine-->>SimulationEngine: {system, user}
SimulationEngine->>AIClient: complete(system, user)
AIClient-->>SimulationEngine: raw_output
SimulationEngine->>ValidationEngine: validate_output(raw_output, action)
ValidationEngine-->>SimulationEngine: ValidationResult
SimulationEngine->>MessageHistory: record(turn)
Note over EventBus: emits workflow.step
SimulationEngine->>EventBus: publish(workflow.step)
end
Note over EventBus: emits workflow.completed
SimulationEngine->>EventBus: publish(workflow.completed)
SimulationEngine-->>Caller: MessageHistory
Failure modes split cleanly along collaborator boundaries. An AIClient.complete timeout surfaces as an exception that aborts the loop before validation; the prior step's events are already on the bus, but the current turn is not recorded. A ValidationEngine rejection does not raise — it returns a ValidationResult whose failures are attached to the recorded turn, and the engine continues. Missing persona or action IDs raise KeyError from the respective registry, which is typically caught upstream by the CLI. To instrument in production, subscribe to workflow.step for per-turn latency metrics and to workflow.completed for end-to-end duration; the observability layer's instrument_simulation_engine() wires both automatically via @traced spans.
Every arrow in the diagram is synchronous except the two EventBus.publish calls, which enqueue onto the bus and return immediately. Subscribers registered via EventSubscriberPlugin receive the events out-of-band and have their own DLQ on failure. This separation is why adding a new compliance or metrics subscriber never slows down the critical path of the workflow.
Steps in detail¶
- Caller to SimulationEngine: run — The caller invokes
run(start_node, initial_payload); the engine initialises aMessageHistoryand prepares to iterate nodes reachable fromstart_node. - SimulationEngine to WorkflowGraph: get_node — For each iteration the engine looks up the current node by ID, returning a
WorkflowNodewithpersona_idandaction_idslots plus outgoing edges. - SimulationEngine to PersonaRegistry: by_id — The persona-ID from the node is resolved to a full
PersonaSpec(including the RISCEAR block used to construct the system prompt). - SimulationEngine to WorkflowActionRegistry: get_action — The action-ID is resolved to a
WorkflowActionthat defines the expected output schema and prompt template. - SimulationEngine to ActionEngine: get_action_prompt — The action engine at
src/fcc/workflow/action_engine.py:51renders a persona-aware{system, user}prompt pair from the RISCEAR spec plus the action template. - SimulationEngine to AIClient: complete — The rendered prompt is sent to the configured
BaseAIClient(anthropic, openai, ollama, litellm, or mock); it returns a raw string completion. - SimulationEngine to ValidationEngine: validate_output — The
compliance/pipeline.pyvalidator checks the raw output against the action's expected schema and returns aValidationResultwith any failures attached. - SimulationEngine to MessageHistory: record — The engine appends a
Turn(persona, action, prompt, output, validation) to the in-memory history. - SimulationEngine to EventBus: publish(workflow.step) — A
workflow.stepevent is emitted; subscribers (tracing, collaboration, compliance) receive it asynchronously. - SimulationEngine to EventBus: publish(workflow.completed) — After the loop exits a single
workflow.completedevent fires with final history metadata. - SimulationEngine to Caller: return MessageHistory — The completed history is returned for rendering, scoring, or downstream FCC phases.
See also¶
- Entry point:
src/fcc/simulation/engine.py:50 - Action prompt builder:
src/fcc/workflow/action_engine.py:51 - Related class diagram:
../class-diagrams/simulation-engine.md - Related event types:
src/fcc/messaging/events.py—EventType.WORKFLOW_STEP,EventType.WORKFLOW_COMPLETED