Skip to content

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

  1. Caller to SimulationEngine: run — The caller invokes run(start_node, initial_payload); the engine initialises a MessageHistory and prepares to iterate nodes reachable from start_node.
  2. SimulationEngine to WorkflowGraph: get_node — For each iteration the engine looks up the current node by ID, returning a WorkflowNode with persona_id and action_id slots plus outgoing edges.
  3. 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).
  4. SimulationEngine to WorkflowActionRegistry: get_action — The action-ID is resolved to a WorkflowAction that defines the expected output schema and prompt template.
  5. SimulationEngine to ActionEngine: get_action_prompt — The action engine at src/fcc/workflow/action_engine.py:51 renders a persona-aware {system, user} prompt pair from the RISCEAR spec plus the action template.
  6. 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.
  7. SimulationEngine to ValidationEngine: validate_output — The compliance/pipeline.py validator checks the raw output against the action's expected schema and returns a ValidationResult with any failures attached.
  8. SimulationEngine to MessageHistory: record — The engine appends a Turn (persona, action, prompt, output, validation) to the in-memory history.
  9. SimulationEngine to EventBus: publish(workflow.step) — A workflow.step event is emitted; subscribers (tracing, collaboration, compliance) receive it asynchronously.
  10. SimulationEngine to EventBus: publish(workflow.completed) — After the loop exits a single workflow.completed event fires with final history metadata.
  11. 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.pyEventType.WORKFLOW_STEP, EventType.WORKFLOW_COMPLETED