Skip to content

Orchestration

Overview

The Orchestration class provides the framework for coordinating multiple agents in complex workflows. It manages agent lifecycle, data flow between agents, and execution sequencing.

OrchestrationRuntimeConfiguration

OrchestrationRuntimeConfiguration inherits Orchestration<ChatMessage, ChatMessage> & IRuntimeConfiguration making it both a Orchestration Graph and a Stateful Runtime Configuration

"Orchestration Graph"

Think of Orchestration<TInput, TOuput> as a Graph of connected Nodes: OrchestrationRunnable<TInput, TOutput> connected by conditional transitions:OrchestrationAdvancers<TInput, TOutput> and based off the condition evaluation Can Create a New process:RunnableProcess which holds all the information for the next graph tick cycle.

Orchestration<TInput, TOuput>

The Orchestration requires 3 main things to configure

  1. Generic TInput/TOutput Parameters
    • Defines the required Input for Invoking the Orchestration and the expected output result
  2. Set SetEntryRunnable(initialRunnable) [runnable requires TInput type of the Orchestration generic TInput type]
  3. Set SetRunnableWithResult(outputRunnable) [runnable requires TOutput type of the Orchestration generic TOutput type]

OrchestrationRunnable<TInput, TOutput>

To Set up a Runnable:

  1. Set Generic TInput/TOutput Parameters
  2. Create the initializer
  3. Implement the Invoke

Bare Minimum

csharp
public class BareMin : OrchestrationRuntimeConfiguration
{
    AgentRunnable agent;
    public BareMin()
    {
        agent = new AgentRunnable(this) { AllowDeadEnd = true }; //Set deadend to disable reattempts and finish the execution
        SetEntryRunnable(agent);
        SetRunnableWithResult(agent); 
    }

     public override void OnRuntimeInitialized()
    {
        base.OnRuntimeInitialized();

        agent.OnAgentRunnerEvent += (sEvent) =>
        {
            // Forward agent runner events (including streaming) to runtime
            this.OnRuntimeEvent?.Invoke(new ChatRuntimeAgentRunnerEvents(sEvent, Runtime?.Id ?? string.Empty));
        };
    }
}

public class AgentRunnable : OrchestrationRunnable<ChatMessage, ChatMessage>
{
    TornadoAgent Agent;

    public Action<AgentRunnerEvents>? OnAgentRunnerEvent { get; set; }

    public AgentRunnable(Orchestration orchestrator) : base(orchestrator)
    {
        TornadoApi api = new TornadoApi("your-api-key");
        string instructions = $"""
                You are a helpful research assistant. Given a query, come up with a set of web searches, 
                to perform to best answer the query. Output between 1 and 5 terms to query for. 
                """;

        Agent = new TornadoAgent(
            client: api,
            model: ChatModel.OpenAi.Gpt5.V5Mini,
            name: "Research Agent",
            instructions: instructions,
            streaming: true);
    }

    public override async ValueTask<ChatMessage> Invoke(RunnableProcess<ChatMessage, ChatMessage> process)
    {
        Conversation conv = await Agent.RunAsync(
        appendMessages: new List<ChatMessage> { process.Input },
        streaming: Agent.Streaming,
        onAgentRunnerEvent: (sEvent) =>
        {
            OnAgentRunnerEvent?.Invoke(sEvent);
            return ValueTask.CompletedTask;
        });

        return conv.Messages.Last();
    }
}

Connecting Nodes

More advanced connected Orchestration from MagneticOne implementation attempt

csharp
 public MagenticOneConfiguration() 
    {
        TornadoApi client = new TornadoApi(Environment.GetEnvironmentVariable("OPENAI_API_KEY"), LLmProviders.OpenAi);
        RecordSteps = true;

        // Create all the specialized runnables
        planner = new PlanningRunnable(client, this);
        webSurfer = new WebSurferRunnable(client, this);
        fileSurfer = new FileSurferRunnable(client, this);
        coder = new CoderRunnable(client, this);
        terminal = new TerminalRunnable(client, this);
        orchestrator = new OrchestratorRunnable(client, this);
        directOrchestrator = new DirectOrchestratorRunnable(client, this);
        exit = new ExitRunnable(this) { AllowDeadEnd = true };

        // Setup orchestration flow: Planning -> specialized agents -> orchestrator for final summary -> exit
        planner.AddAdvancer((plan) => plan.RequiredAgents.Contains("WebSurfer") ||
            plan.OriginalTask.ToLower().Contains("search") ||
            plan.OriginalTask.ToLower().Contains("web") ||
            plan.OriginalTask.ToLower().Contains("research"), webSurfer);

        planner.AddAdvancer((plan) => plan.RequiredAgents.Contains("FileSurfer") ||
            plan.OriginalTask.ToLower().Contains("file") ||
            plan.OriginalTask.ToLower().Contains("document") ||
            plan.OriginalTask.ToLower().Contains("write"), fileSurfer);

        planner.AddAdvancer((plan) => plan.RequiredAgents.Contains("Coder") ||
            plan.OriginalTask.ToLower().Contains("code") ||
            plan.OriginalTask.ToLower().Contains("program") ||
            plan.OriginalTask.ToLower().Contains("script"), coder);

        planner.AddAdvancer((plan) => plan.RequiredAgents.Contains("Terminal") ||
            plan.OriginalTask.ToLower().Contains("command") ||
            plan.OriginalTask.ToLower().Contains("terminal") ||
            plan.OriginalTask.ToLower().Contains("run"), terminal);

        // If no specific agents are needed, go directly to directOrchestrator
        planner.AddAdvancer((plan) => !RequiresSpecializedAgent(plan), directOrchestrator);

        // All specialized agents flow to orchestrator for final summary
        webSurfer.AddAdvancer(orchestrator);
        fileSurfer.AddAdvancer(orchestrator);
        coder.AddAdvancer(orchestrator);
        terminal.AddAdvancer(orchestrator);

        // Both orchestrators produce final result and exit
        orchestrator.AddAdvancer((result) => !string.IsNullOrWhiteSpace(result.TaskResult), exit);
        directOrchestrator.AddAdvancer((result) => !string.IsNullOrWhiteSpace(result.TaskResult), exit);

        // Configure entry and exit points
        SetEntryRunnable(planner);
        SetRunnableWithResult(exit);
    }

Using the builder

csharp
 public OrchestrationRuntimeConfiguration BuildComplexAgent(TornadoApi client, bool streaming = false, string chromaUri = "http://localhost:8001/api/v2/",string conversationFile = "AgentV10.json", string withLongtermMemoryID = "AgentV10")
    {
        AgentRunnable RunnableAgent = new AgentRunnable(client, this, streaming);
        ModeratorRunnable inputModerator = new ModeratorRunnable(client, this);
        VectorSearchRunnable vectorSearchRunnable = new VectorSearchRunnable(client, this, chromaUri);
        WebSearchRunnable webSearchRunnable = new WebSearchRunnable(client, this);
        VectorSaveRunnable vectorSaveRunnable = new VectorSaveRunnable(client, this, chromaUri);
        ExitPathRunnable exitPathRunnable = new ExitPathRunnable(this);
        VectorEntitySaveRunnable vectorEntitySaveRunnable = new VectorEntitySaveRunnable(client, this, chromaUri);
        ChatPassthruRunnable chatPassthruRunnable = new ChatPassthruRunnable(this);

        return new OrchestrationBuilder(this)
            .SetEntryRunnable(inputModerator)
            .SetOutputRunnable(RunnableAgent)
            .WithRuntimeInitializer((config) =>
            {
                RunnableAgent.OnAgentRunnerEvent += (sEvent) =>
                {
                    // Forward agent runner events (including streaming) to runtime
                    config.OnRuntimeEvent?.Invoke(new ChatRuntimeAgentRunnerEvents(sEvent, config.Runtime?.Id ?? string.Empty));
                };
                return ValueTask.CompletedTask;
            })
            .WithRuntimeProperty("LatestUserMessage", "")
            .WithRuntimeProperty("MemoryCollectionName", $"{withLongtermMemoryID}")
            .WithRuntimeProperty("EntitiesCollectionName", $"{withLongtermMemoryID}Entities")
            .WithChatMemory(conversationFile)
            .WithDataRecording()
            .AddParallelAdvancement(inputModerator,
                new OrchestrationAdvancer<ChatMessage>(webSearchRunnable),
                new OrchestrationAdvancer<ChatMessage>(vectorSearchRunnable),
                new OrchestrationAdvancer<ChatMessage>(vectorEntitySaveRunnable),
                new OrchestrationAdvancer<ChatMessage>(chatPassthruRunnable))
            .AddCombinationalAdvancement<string>(
                fromRunnables: [webSearchRunnable, vectorSearchRunnable, vectorEntitySaveRunnable, chatPassthruRunnable],
                condition: _ => true,
                toRunnable: RunnableAgent,
                requiredInputToAdvance: 4,
                combinationRunnableName: "CombinationalContextWaiter")
            .AddParallelAdvancement(RunnableAgent,
                new OrchestrationAdvancer<ChatMessage>(vectorSaveRunnable),
                new OrchestrationAdvancer<ChatMessage>(exitPathRunnable))
            .AddExitPath<ChatMessage>(exitPathRunnable, _ => true)
            .CreateDotGraphVisualization("ChatBotAgent.dot")
            .Build();
    }

Best Practices

  • Design clear data contracts between stages
  • Handle errors at appropriate levels
  • Implement timeout mechanisms
  • Monitor orchestration health
  • Test each stage independently