namespace TriggerAgent.ExecutionEngine.StepRegistry;
using TriggerAgent.Domain.Contracts;
using TriggerAgent.Domain.Enums;
public sealed class StepRegistry : IStepRegistry
{
private readonly Dictionary<StepNumber, IStep> _steps = new();
public void Register(IStep step)
{
if (_steps.ContainsKey(step.StepNumber))
throw new InvalidOperationException($"Step {step.StepNumber} already registered.");
_steps[step.StepNumber] = step;
}
public IStep Resolve(StepNumber stepNumber)
{
if (!_steps.TryGetValue(stepNumber, out var step))
throw new KeyNotFoundException($"No step registered for {stepNumber}.");
return step;
}
}
namespace TriggerAgent.ExecutionEngine.Orchestrator;
using TriggerAgent.Domain.Contracts;
using TriggerAgent.Domain.Enums;
using TriggerAgent.ExecutionEngine.StepRegistry;
using TriggerAgent.SharedKernel.Interfaces;
public sealed class StepOrchestrator : IStepOrchestrator
{
private readonly IStepRegistry _registry;
private readonly ISessionStore _sessionStore;
public StepOrchestrator(IStepRegistry registry, ISessionStore sessionStore)
{
_registry = registry;
_sessionStore = sessionStore;
}
public async Task<IStepOutput> ExecuteAsync(Guid sessionId, StepNumber step, IStepInput input)
{
var session = await _sessionStore.GetAsync(sessionId)
?? throw new KeyNotFoundException($"Session {sessionId} not found.");
if (session.CurrentStep != step)
throw new InvalidOperationException(
$"Session is at {session.CurrentStep}, cannot execute {step}.");
var handler = _registry.Resolve(step);
var context = new StepContext(session, input);
var output = await handler.ExecuteAsync(context);
await _sessionStore.SaveAsync(session);
return output;
}
}
namespace TriggerAgent.Api.Controllers;
using Microsoft.AspNetCore.Mvc;
using TriggerAgent.Api.Controllers.Dto;
using TriggerAgent.Domain.Contracts;
using TriggerAgent.Domain.Enums;
using TriggerAgent.ExecutionEngine.Orchestrator;
using TriggerAgent.SharedKernel.Interfaces;
using TriggerAgent.Steps.Step1_Greeting;
[ApiController]
[Route("api/[controller]")]
public sealed class SessionController : ControllerBase
{
private readonly ISessionStore _sessionStore;
private readonly IStepOrchestrator _orchestrator;
public SessionController(ISessionStore sessionStore, IStepOrchestrator orchestrator)
{
_sessionStore = sessionStore;
_orchestrator = orchestrator;
}
[HttpPost("start")]
public async Task<IActionResult> StartSession()
{
var session = await _sessionStore.CreateAsync();
var output = await _orchestrator.ExecuteAsync(
session.SessionId, StepNumber.Step1_Greeting, EmptyInput.Instance);
var greeting = output as GreetingOutput;
var payload = new StepPayload
{
Message = greeting?.DisplayPayload ?? output.DisplayPayload,
NextAction = greeting?.NextAction ?? string.Empty
};
return Ok(ApiResponse<StepPayload>.Ok(payload, session.SessionId, output.Step));
}
[HttpGet("{sessionId:guid}")]
public async Task<IActionResult> GetSession(Guid sessionId)
{
var session = await _sessionStore.GetAsync(sessionId);
if (session is null)
return NotFound(ApiResponse<object>.Fail("Session not found.", sessionId, 0));
var payload = new StepPayload
{
Message = $"Session active at {session.CurrentStep}.",
NextAction = session.IsComplete ? "NONE" : "CONTINUE"
};
return Ok(ApiResponse<StepPayload>.Ok(payload, session.SessionId, session.CurrentStep));
}
}
23. src/TriggerAgent.Api/Program.cs
using TriggerAgent.Domain.Contracts;
using TriggerAgent.ExecutionEngine.Orchestrator;
using TriggerAgent.ExecutionEngine.StepRegistry;
using TriggerAgent.Infrastructure.Storage;
using TriggerAgent.SharedKernel.Interfaces;
using TriggerAgent.Steps.Step1_Greeting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddSingleton<ISessionStore, InMemorySessionStore>();
var registry = new StepRegistry();
registry.Register(new GreetingStep());
builder.Services.AddSingleton<IStepRegistry>(registry);
builder.Services.AddSingleton<IStepOrchestrator>(new StepOrchestrator(registry, builder.Services.BuildServiceProvider().GetRequiredService<ISessionStore>()));
builder.Services.AddCors(opts =>
opts.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
var app = builder.Build();
app.UseCors();
app.UseStaticFiles();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.Run();