π«π· Version franΓ§aise
See also: ProcessTypes guide Β· Back to index
Blueprint β Adding an orchestration type to Orkeon
This document serves as a reproducible guide for implementing and documenting a new orchestration type. It builds on the FSM precedent (see FSM Orchestration) and covers the 8 required steps, from the Domain layer through to the final documentation.
Prerequisites
Before starting, identify:
- Type name: e.g.
StateGraph,AgenticSwarm,PipelineDAG - Doc number: create a file in
docs/orchestration/(e.g.docs/orchestration/new-type.md) - Example number: next free index in
examples/(e.g.104) - Initial scope: where the new type applies first (Task, Crew, Flow)
Step 1 β Domain: Generic framework
Folder: src/core/Orkeon.Domain/Common/<NomType>/
Create the following files (adapt the class names):
| File | Role | Reference model |
|---|---|---|
I<NomType>.cs |
Read-only + mutation interfaces in a single file | IStateMachine.cs (contains IStateMachine + IMutableStateMachine) |
<NomType>.cs |
Thread-safe main engine | StateMachine.cs |
<NomType>Builder.cs |
Fluent API to declare the graph/config | StateMachineBuilder.cs |
<NomType>Policy.cs |
Immutable configuration record with presets + CircuitBreakerStatus |
CircuitBreakerPolicy.cs |
<NomType>Result.cs |
Immutable record of the operation result | TransitionResult.cs |
<NomType>Exceptions.cs |
Type-specific exceptions | StateMachineExceptions.cs |
TransitionDefinition.cs |
internal sealed class for a unit transition (from, trigger, to, guard, action) |
TransitionDefinition.cs |
Note: The two interfaces (read-only and mutation) live in a single file by convention. The
TransitionDefinitionclass isinternaland exposed to tests via[assembly: InternalsVisibleTo].
Technical checklist
- [ ] Thread-safety:
lockorSemaphoreSlimdepending on the async need - [ ] Static presets:
Strict,Default,Permissive(immutable) - [ ] Events: at minimum
OnTransitionandOnCircuitBroken(or equivalent) - [ ]
CircuitBreakerStatusor equivalent with a full snapshot for observability - [ ] No external dependency (pure Domain)
- [ ]
[assembly: InternalsVisibleTo("Orkeon.Domain.Tests")]if there are internal classes
Code pattern β Engine
namespace Orkeon.Domain.Common.<NomType>;
public sealed class <NomType><TState, TEvent>
: I<NomType><TState, TEvent>, IMutable<NomType><TState, TEvent>
where TState : notnull
where TEvent : notnull
{
private readonly object _lock = new();
// ... internal state, counters, histogram
// Mandatory extension points:
// 1. Check limits BEFORE each operation
// 2. Action hook AFTER each successful operation
// 3. Events for external observability
// 4. Degraded mode (fallback state) when UseDegradedMode = true
}
Code pattern β Fluent builder
public sealed class <NomType>Builder<TState, TEvent>
where TState : notnull
where TEvent : notnull
{
public <NomType>Builder<TState, TEvent> WithInitialState(TState state) { ... }
public <NomType>Builder<TState, TEvent> WithTerminalStates(params TState[] states) { ... }
public <NomType>Builder<TState, TEvent> WithDegradedState(TState state) { ... }
public <NomType>Builder<TState, TEvent> WithCircuitBreaker(<NomType>Policy policy) { ... }
// Key functions for internal dictionaries (useful when TState is not an enum):
public <NomType>Builder<TState, TEvent> WithStateKey(Func<TState, string> keyFunc) { ... }
public <NomType>Builder<TState, TEvent> WithEventKey(Func<TEvent, string> keyFunc) { ... }
// Nested builder pattern for transitions/nodes:
public TransitionBuilder When(TState from, TEvent trigger) { ... }
// Shortcut for a simple transition without guard or action:
public <NomType>Builder<TState, TEvent> AddTransition(TState from, TEvent trigger, TState to) { ... }
public <NomType><TState, TEvent> Build() { ... }
public sealed class TransitionBuilder
{
public TransitionBuilder TransitionTo(TState to) { ... }
public TransitionBuilder WithGuard(Func<bool> guard, string? desc = null) { ... }
public TransitionBuilder WithGuard<TContext>(Func<TContext, bool> guard, string? desc = null) { ... }
public TransitionBuilder WithAction(Action action) { ... }
public TransitionBuilder WithAction(Action<TState, TState> action) { ... }
public <NomType>Builder<TState, TEvent> Done() { ... }
}
}
Step 2 β Domain: Specialization
Folder: src/core/Orkeon.Domain/<Scope>/ (e.g. Task/, Crew/, Flow/)
| File | Role |
|---|---|
<Scope><NomType>State.cs |
BOTH enums (states + events) in a single file |
<Scope><NomType>Machine.cs |
Static factory Create() + CreateBuilder() + GuardContext record |
FSM convention: In the existing implementation,
TaskExecutionState.cscontains both theTaskExecutionStateenum and theTaskExecutionEventenum. Group both in a single file when they are always used together.
Guard context type
public sealed record <Scope>GuardContext
{
// Loop counters (adapt to the orchestration type)
public int RetryCount { get; init; }
public int MaxRetries { get; init; } = 3;
// Type-specific counters
// FSM example: ToolCallCount, ValidationAttempts
// LangGraph example: NodeVisitCount, EdgeTraversalCount
// Agentic example: SubAgentSpawnCount, DelegationDepth
// Derived predicates
public bool CanRetry => RetryCount < MaxRetries;
// ...
}
Anti-hallucination guards (mandatory)
Every orchestration type MUST implement at minimum:
| Guard | Purpose | FSM example |
|---|---|---|
| Operation budget | Limit expensive calls | ToolCallCount < MaxToolCallsPerRound |
| Registered tool | Block hallucinated tools | IsToolRegistered == true |
| Retry limit | Avoid infinite loops | RetryCount < MaxRetries |
Step 3 β Domain: Configuration DTO
File: src/core/Orkeon.Domain/Configuration/<NomType>Config.cs
namespace Orkeon.Domain.Configuration;
/// <summary>
/// Immutable DTO for the <NomType> configuration coming from YAML.
/// All fields are nullable to allow crew β task inheritance.
/// </summary>
public sealed record <NomType>Config
{
public string? Preset { get; init; }
// Circuit breaker fields (common to all types)
public int? MaxTransitions { get; init; }
public int? StateTimeoutSeconds { get; init; }
public int? MaxStateVisits { get; init; }
public int? MaxTotalDurationSeconds { get; init; }
public bool? UseDegradedMode { get; init; }
// Type-specific fields (guards)
public int? MaxRetries { get; init; }
// ... adapt to the type
}
Modify CrewConfiguration.cs (which also contains TaskConfiguration):
// In the CrewConfiguration record (same file):
public <NomType>Config? <NomType> { get; init; }
// In the TaskConfiguration record (same CrewConfiguration.cs file):
public <NomType>Config? <NomType> { get; init; }
Note:
TaskConfigurationis defined in the same file asCrewConfiguration(src/core/Orkeon.Domain/Configuration/CrewConfiguration.cs). There is no separateTaskConfiguration.csfile.
Step 4 β Infrastructure: YAML β Domain bridge
File: src/core/Orkeon.Infrastructure/Configuration/<NomType>PolicyFactory.cs
namespace Orkeon.Infrastructure.Configuration;
public static class <NomType>PolicyFactory
{
/// <summary>
/// Hierarchical resolution: task override β crew default β preset β Strict fallback.
/// </summary>
public static <NomType>Policy Resolve(
<NomType>Config? crewDefault,
<NomType>Config? taskOverride)
{
// 1. Determine the base preset
// 2. Apply the crew-level overrides
// 3. Apply the task-level overrides (they override crew)
// 4. Convert int seconds β TimeSpan
}
/// <summary>
/// Creates a specialized FSM (concrete return type, not generic).
/// The method name must reflect the scope: CreateTaskFsm, CreateCrewFsm, etc.
/// </summary>
public static StateMachine<TaskExecutionState, TaskExecutionEvent> Create<Scope>Fsm(
<NomType>Config? crewDefault,
<NomType>Config? taskOverride)
{
var policy = Resolve(crewDefault, taskOverride);
return <Scope><NomType>Machine.Create(policy);
}
/// <summary>
/// Builds the guard context with hierarchical merging of the limits.
/// The task override wins for non-null fields, then crew default, then the record's default value.
/// </summary>
public static <Scope>GuardContext CreateGuardContext(
<NomType>Config? crewDefault,
<NomType>Config? taskOverride)
{
var effective = taskOverride ?? crewDefault;
return new <Scope>GuardContext
{
MaxRetries = effective?.MaxRetries ?? 3,
// ... other fields with default fallback
IsToolRegistered = true, // Runtime concern, not config
};
}
}
// --- Real example (CircuitBreakerPolicyFactory) ---
// The `CreateTaskFsm` method returns a concrete type, not a generic one.
// The `ApplyOverrides` method is private and handles the int seconds β TimeSpan conversion.
Resolution hierarchy (invariant across all types)
1. Task-level config (high priority β non-null fields override)
2. Crew-level config (default β non-null fields override the preset)
3. Named preset (if specified in Preset β base values)
4. <NomType>Policy.Strict (fallback when nothing is configured)
Step 5 β Infrastructure: YAML parsing
Modified files: src/core/Orkeon.Infrastructure/Configuration/Yaml/YamlConfigModels.cs
(the models) and Configuration/Yaml/YamlCrewMapper.cs (the mapping) β the loader
(YamlCrewDefinitionLoader.cs) only deserializes and delegates.
5.1 Add the YAML class
No [YamlMember] attributes β the existing models carry none: key resolution is
by convention (CamelCaseNamingConvention plus the CamelOrSnakeCaseTypeInspector
snake_case fallback in YamlDotNetSerializer). Follow the shape of the shipped
models β public, non-sealed, in the shared YamlConfigModels.cs:
public class <NomType>YamlConfig
{
public string? Preset { get; set; } // reachable as preset:
public int? MaxTransitions { get; set; } // reachable as maxTransitions: or max_transitions:
// ... plain properties, camelCase/snake_case both accepted by convention
}
5.2 Add the property on the THREE YAML models
The YamlCrewDefinitionLoader uses different models for single-file and multi-file loading:
// In CrewYamlConfig (single-file config.yaml loading):
public <NomType>YamlConfig? <NomType> { get; set; } // key: <nomType> by convention
// In CrewSettingsYamlConfig (multi-file loading: crew.yaml):
public <NomType>YamlConfig? <NomType> { get; set; }
// In TaskYamlConfig (used in both modes):
public <NomType>YamlConfig? <NomType> { get; set; }
Warning: There are THREE YAML models to modify, not two.
CrewSettingsYamlConfigis used only in multi-file mode (folder withcrew.yaml+agents.yaml+tasks.yaml).
5.3 Add the mapping
private static <NomType>Config? Map<NomType>(<NomType>YamlConfig? yaml)
{
if (yaml is null) return null;
return new <NomType>Config
{
Preset = yaml.Preset,
MaxTransitions = yaml.MaxTransitions,
// ...
};
}
5.4 Wire into the loading methods
Add <NomType> = Map<NomType>(...):
- in
YamlCrewMapper.MapTasks()β for the task level; - crew-level values flow through
CrewMappingSettings, which the loader populates inLoadFromStringAsync()(single-file) andLoadFromDirectoryCoreAsync()(multi-file) before delegating to the mapper.
Step 6 β Tests
Folder: tests/core/Orkeon.Domain.Tests/Common/<NomType>/
6.1 Generic framework tests
| File | Minimum tests |
|---|---|
<NomType>Tests.cs |
Correct initial state, valid operation, full cycle, invalid operation throws, TryFire false, terminal blocked, CanFire, OnTransition event |
<NomType>GuardTests.cs |
Guard true allowed, guard false blocked, multiple guards priority, action executed |
<NomType>CircuitBreakerTests.cs |
Max transitions trip, cycle detection, degraded mode, OnCircuitBroken, reset, histogram |
6.2 Specialization tests
| File | Minimum tests |
|---|---|
<Scope><NomType>MachineTests.cs |
Full happy path, each guard individually (budget, tool, retries, validation), cancel from any state, circuit breaker in a loop |
6.3 Infrastructure tests (optional but recommended)
| File | Minimum tests |
|---|---|
<NomType>PolicyFactoryTests.cs |
Preset-only resolution, crew override, task override overrides crew, Strict fallback |
Test command
dotnet test tests/core/Orkeon.Domain.Tests/ --filter "FullyQualifiedName~<NomType>"
Step 7 β Example
Folder: examples/06-engineering-devops/<NumExemple>-<nom-court>/
7.1 Copy an existing example
Start from example 102 or 103 as the base, then:
- Duplicate the
config.yaml - Add/replace the configuration block for the new type
- Adapt tasks and agents if necessary
7.2 config.yaml structure
# === Configuration block of the new type at crew level ===
<nomType>: # e.g. stateGraph, agenticSwarm
preset: "strict"
useDegradedMode: true
maxRetries: 3
# ... type-specific fields
agents:
my_agent: # mapping keyed by agent id β never a sequence
role: "..."
# ...
tasks:
task_1: # the id IS the key (TaskYamlConfig has no id: field)
description: "..."
# === Task-level override ===
<nomType>:
maxTransitions: 200
stateTimeoutSeconds: 600
# ...
7.3 Example README
README structure:
# Example <Num> β <Title>
## Goal
What and why.
## Diagram
ASCII or Mermaid of the graph/workflow.
## YAML configuration
Annotated block with the key values.
## Diff with example <Num-1>
What changes compared to the previous example.
## Run
Command to run the example.
Step 8 β Documentation
File: docs/orchestration/<nom-type>.md
8.1 Mandatory document structure
# <NomType> orchestration
## Overview
Introductory paragraph: what, why, where in the architecture.
## Architecture
### Domain layer β Generic framework
Table: Class | Role (point to the files)
### Domain layer β <Scope> specialization
Table: Class | Role
### Infrastructure layer β YAML integration
Table: Class | Role
### Domain layer β Configuration
Table: Class | Role
## Graph / Diagram
ASCII art of the state graph or workflow.
List the terminal states.
## Circuit breaker / Protection mechanisms
Table: Mechanism | Parameter | Description
Explain the two modes (exception vs degraded).
## Presets
Table: Preset | Values (reuse Strict, Default, Permissive)
## Observability
Exposed events, status/snapshot contents.
## Typical guards
Table: Guard | Protected transition/node | Condition
Explain the anti-hallucination guard.
## Configuration YAML
### Schema
Annotated YAML block with types and descriptions.
### Resolution hierarchy
ASCII diagram: task β crew β preset β fallback.
### YAML models
Table: C# model | YAML class | File
## Usage in C# code
### Manual creation (Fluent Builder)
Complete example with observability and guard context.
### Creation from the YAML configuration
Example with the PolicyFactory.
### Custom construction
Example with the generic builder.
## Example <Num>
Reference to the example, summary of the additions.
## Relation to the existing code
### StateTransitionManager
Explanation of the complementarity (lifecycle vs runtime).
### SequentialCrewOrchestrator
Explanation of the integration point (IProcessStrategy).
### <Other existing orchestration types>
How this type coexists with the previous ones.
## Tests
Table: File | Coverage
Test command.
8.2 Update the existing files
docs/INDEX.md:- Add a line in the Orchestration section
- Add the file to the "I want to understand the framework" reading path
docs/architecture/yaml-schema.md:- Add the YAML schema of the new configuration block
- Document the presets and default values
docs/orchestration/process-types.md:- Add the new type to the comparison matrix
- Add a dedicated section with pros/cons/use cases
- Update the decision tree
Stability criteria (final checklist)
Before considering the implementation complete, verify:
| Criterion | Expected mechanism | Verified |
|---|---|---|
| Recursive loops | Circuit breaker (4 mechanisms minimum) + typed guards | [ ] |
| Tool hallucinations | IsToolRegistered guard or equivalent |
[ ] |
| Observability | Events + snapshot/status with histogram | [ ] |
| Interruptibility | Cancel from any non-terminal state | [ ] |
| Multi-model determinism | Per-task configurable limits (Flash β Opus) | [ ] |
| 2-level YAML config | Crew default + task override | [ ] |
| Degraded mode | Auto transition to a safe state instead of an exception | [ ] |
| Tests | 30+ tests covering framework + specialization | [ ] |
| Documentation | Complete doc + index + features updated | [ ] |
| Example | Working config.yaml with README | [ ] |