17 KiB
Backend-Developer.md — Developer Notes
Project: UltimateAgents — Orchestration Service
Stack: C# 13 / .NET 9 / ASP.NET Core / SQLite / Dapper
Last Updated: 2026-04-07
These notes are for developers working on the backend. Read Architecture.md first for the full system design. This document focuses on practical day-to-day guidance: how to set up, where things live, what rules to follow, and common pitfalls.
Table of Contents
- Getting Started
- Project Layout Cheat Sheet
- Domain Rules — Must Know
- Adding a New Feature — Step-by-Step
- Database & Migrations
- Working with the Orchestrator Engine
- Agent Matching
- Scheduling & Events
- Container Runtime
- Error Handling & Escalation
- Testing Guidelines
- Logging Conventions
- Common Pitfalls
- Environment Variables & Config
- Useful Commands
1. Getting Started
Prerequisites
- .NET 9 SDK (
dotnet --versionshould show9.x.x) dotnet restore
# Restore packages
dotnet restore
# Run the API
dotnet run --project src/UltimateAgents.Api
# Run the CLI
dotnet run --project src/UltimateAgents.Cli -- task list
The service uses a local SQLite database and in-process C# events. No external message broker or Docker is required for local development.
2. Project Layout Cheat Sheet
src/
├── UltimateAgents.Domain/ ← Pure C# models, no framework dependencies
├── UltimateAgents.Application/ ← Use cases, commands, queries, interfaces
├── UltimateAgents.Infrastructure/← Dapper, runtime adapter, RabbitMQ
├── UltimateAgents.Api/ ← ASP.NET Core host, controllers, DTOs
└── UltimateAgents.Cli/ ← System.CommandLine frontend
Dependency direction (strict — never reverse):
Api → Application → Domain
Infrastructure → Application → Domain
Cli → Api (via HTTP client)
Domainhas zero NuGet dependencies.Applicationdepends only onDomainand its own interfaces (defined inApplication/Interfaces/).Infrastructureimplements those interfaces. It is the only project that touches Dapper, the runtime adapter, or RabbitMQ.- Controllers in
Apicall Application services only — never repositories directly.
3. Domain Rules — Must Know
These rules are enforced in the domain layer. Do not bypass them in controllers or services.
Complexity is always auto-evaluated
// Always use the evaluator — never set ComplexityLevel manually in external code
var level = ComplexityEvaluator.Evaluate(task.ResolveTimeEstimate);
ResolveTimeEstimate |
Result |
|---|---|
null |
ExtremHard |
> 60 |
Hard |
> 30 |
Middle |
≤ 30 |
Low |
Tasks are always flat — no parent/subtask nesting
Use next_task and TaskGroup to compose tasks. Never introduce a “parent task” concept.
next_task/previous_taskexpress ordering within or across groups.ForeachTaskexpansion creates independent task instances linked bynext_task/previous_task, placed into a sequential group.GotoTaskjumps to an arbitrary target (task or group) and merges an additional prompt into it.ConditionTaskevaluates a boolean expression and routes to a true or false branch.
Chain cycles are rejected at write time
DependencyGraph.HasCycle() is called inside CreateTaskCommand and CreateGroupCommand before the task or group is persisted. It validates the entire chain — task NextTaskId links, group Next pointers, and parent‚child relationships — as one unified directed graph. If a cycle is detected, throw DependencyCycleException — the API maps this to HTTP 422.
Agent isolation is absolute
Agents run inside their assigned container. The domain model enforces that an agent cannot reference tools or volumes outside its declared container spec. Never pass cross-container paths in task prompts.
4. Adding a New Feature — Step-by-Step
Follow this checklist to keep the architecture clean.
- Domain first. Does the feature need a new entity, value object, or enum? Add it to
UltimateAgents.Domain. Write unit tests for any domain logic. - Define the interface. If the feature needs infrastructure (DB query, external call), add an interface to
Application/Interfaces/. - Write the use case. Add a command or query class in
Application/. Use a plain service class to implement the handler. No Dapper here. - Implement infrastructure. Implement the new interface in
Infrastructure/. Use Dapper and schema scripts for persistence. - Expose via API. Add or extend a controller in
Api/Controllers/. Add a DTO and a FluentValidation validator. - Wire up DI. Register the new service/repository in
Program.csor a dedicatedServiceCollectionExtensionsclass. - Add CLI command (if user-facing). Add a subcommand to the appropriate file in
Cli/Commands/. - Write tests. Unit-test domain + application layers; integration-test the API endpoint with a local SQLite database.
5. Database & Migrations
Database schema
- The SQLite schema is defined in
Infrastructure/Persistence/DatabaseSchema.sql. - Use Dapper for lightweight mapping and parameterized SQL queries.
- Many-to-many relationships are represented using explicit join tables.
- Schema updates are managed via versioned SQL scripts, not EF Core migrations.
6. Working with the Orchestrator Engine
OrchestratorEngine runs as an IHostedService. It watches for eligible tasks and drives them through their lifecycle.
Task lifecycle states
Created → Queued → Running → Succeeded
↘ Failed → Retrying → Succeeded
↘ Escalated
Rules for state transitions
- Only
OrchestratorEngineis allowed to write task execution state. Controllers must use theStartTaskCommandapplication service, which queues the intent — never set state directly from a controller. - State is persisted in SQLite after every transition for status queries. The local database is the source of truth.
- Goto task: when a task completes and
GotoTaskIdis set, the engine mergesGotoPromptinto the target task'sPrompt(append by default) before transitioning. Never overwrite the original prompt.
Group execution
Tasks are organised into TaskGroup entities. A group has a GroupType and a Children list of GroupChildRef. Children can be tasks (any type) or nested TaskGroup instances.
Sequential— the engine activates the first child; when it completes, activates the next sibling child in order. After the last child completes, the group itself is marked complete; the engine then activatesgroup.Next(if set).Parallel— all children are dispatched simultaneously. When every child is complete, the group is marked complete andgroup.Nextis activated.- Nested groups follow the same rules recursively — a nested group must itself be fully complete before its parent considers it done.
group.Nextis aGroupChildRefand can point to a task or another group.- The engine uses local coordination and database transactions to prevent the same group from being activated twice.
7. Agent Matching
Agent selection uses the Strategy pattern. The matching strategy is resolved from the agent's ChooseRule field.
// Registered in DI as keyed services
services.AddKeyedScoped<IAgentMatchStrategy, ExactSingleTagStrategy>(AgentMatchRule.ExactSingle);
services.AddKeyedScoped<IAgentMatchStrategy, SupersetTagStrategy>(AgentMatchRule.Superset);
services.AddKeyedScoped<IAgentMatchStrategy, AllRequiredTagsStrategy>(AgentMatchRule.AllRequired);
Order of precedence when multiple agents match: prefer agents with the fewest extra skills (most specific match). If equal, pick the agent with the lowest current load.
No match found: the dispatcher retries matching after the configured backoff period. After max_retries, the task is escalated.
8. Scheduling & Events
Schedule types
| Type | Behavior |
|---|---|
Immediate |
Queued for dispatch as soon as the task is created. |
TimeBased |
Cron or ISO 8601 repeat expression. Evaluated by SchedulerService. |
DatetimeEvent |
Fires once at the specified UTC datetime. Use ISO 8601 format. |
This version does not use signal-based schedule triggers. Tasks are scheduled using immediate, time-based, or datetime-event rules. Internal coordination is handled through in-process C# events dispatched via IEventDispatcher.
9. Container Runtime
The IContainerRuntime interface abstracts the local isolated runtime. Switch the implementation via config:
"RuntimeAdapter": "LocalSandbox"
Local development
Use the local runtime adapter. No Docker or Kubernetes runtime is required.
Runtime spec validation
Before StartAsync is called, the ContainerSpec is validated:
- All declared tools must have a
nameandversion. resources.cpuandresources.memorymust be positive values.network_policymust be explicitly declared (empty is allowed, butnullis rejected).
Never pass user-controlled strings directly into container exec calls. Always use the tool definition's pre-declared
usage_guidelinesto build the command. This prevents command injection.
10. Error Handling & Escalation
Controller layer
All controllers are wrapped by the global exception middleware in Api/Middleware/ExceptionMiddleware.cs. Do not add try/catch blocks in controllers for domain exceptions — let the middleware handle them.
// Good
var result = await _createTaskService.ExecuteAsync(command, ct);
return Ok(result);
// Bad — don't do this
try { ... } catch (TaskNotFoundException) { return NotFound(); }
Escalation flow
- Agent reports failure with
Escalation.ReasonandEscalation.Prompt. EscalationHandlerchecks the task'sRetryPolicy.- If retries remain: re-queue the task with incremented attempt counter and apply backoff.
- If retries exhausted: publish an
EscalationEventand set task state toEscalated. - The human operator receives the escalation via the configured notification channel (webhook or log alert — configured externally).
Always populate Escalation on failure. A missing escalation object on a failed task is treated as a bug and logged at Error level.
11. Testing Guidelines
Unit tests (UltimateAgents.Domain.Tests, UltimateAgents.Application.Tests)
- Test domain rules in isolation — no database, no Docker.
- Mock
ITaskRepository,IContainerRuntime, andIEventDispatcherwith Moq. - Cover every
ComplexityEvaluator.Evaluate()branch. - Cover cycle detection: test graphs with cycles, linear chains, and parallel groups.
dotnet test tests/UltimateAgents.Domain.Tests
dotnet test tests/UltimateAgents.Application.Tests
Integration tests (UltimateAgents.Api.Tests)
- Use local SQLite or file-backed database instances for integration testing.
- Test the full HTTP request → DB → response cycle.
- Seed data via the configured persistence layer in the test fixture, not via the API.
dotnet test tests/UltimateAgents.Api.Tests
Coverage target
- Domain layer: ≥ 90% line coverage.
- Application layer: ≥ 80% line coverage.
- API controllers: covered by integration tests, not unit tests.
12. Logging Conventions
Use Serilog with structured properties. Always include correlation context.
_logger.LogInformation(
"Task {TaskId} transitioned to {NewState} by agent {AgentId} in container {ContainerId}",
task.Id, newState, agent.Id, container.Id);
Mandatory fields for task/agent log entries:
| Field | Description |
|---|---|
TaskId |
Guid of the task |
AgentId |
Guid of the acting agent (if applicable) |
ContainerId |
Runtime container ID |
Event |
Short event name (e.g. TaskStarted) |
ErrorContext |
Exception message / stack trace on error |
Log levels:
| Level | When to use |
|---|---|
Debug |
Detailed internal flow (disabled in production) |
Information |
State transitions, task/agent lifecycle events |
Warning |
Retries, unexpected but recoverable situations |
Error |
Escalations, unhandled exceptions, data integrity issues |
Never log task Prompt content at Information or above — it may contain sensitive instructions. Use Debug only.
13. Common Pitfalls
| Pitfall | Why it happens | Fix |
|---|---|---|
DependencyCycleException at runtime |
A next_task or next_group chain forms a cycle that wasn’t caught at write-time |
Always call DependencyGraph.HasCycle() in CreateTaskCommand and CreateGroupCommand before saving |
| Agent never matched | Agent’s TaskTags don’t intersect with task’s TaskTags |
Check ChooseRule on the agent; verify tags are using the same enum string values |
| Cache state out of sync with DB | The local state cache or temporary store is stale compared to the database | Write to SQLite first in a transaction, then update any local cache. Never update cache before the DB commit. |
| Container start fails silently | IContainerRuntime.StartAsync threw but the exception was swallowed |
Always await container calls; propagate exceptions to EscalationHandler |
| Foreach tasks run in wrong order | ForeachTask.ForeachTemplateTaskId set but next_task/previous_task links not generated |
ForeachExpander in the application layer must set ordering links on generated task instances |
| Double-scheduling in multi-instance deploy | Two API instances pick up the same eligible task | Use local database locking and coordination; ensure task state transitions are serialized through SQLite. |
| User input injected into container commands | Prompt strings passed directly to ContainerRuntime.ExecAsync |
Only use pre-declared tool usage_guidelines to construct exec commands; validate/sanitize any user-supplied values |
14. Environment Variables & Config
All settings follow the ASP.NET Core configuration hierarchy: environment variables override appsettings.json.
| Environment Variable | Default | Purpose |
|---|---|---|
Database__ConnectionString |
(required) | SQLite connection string or file path |
ContainerRuntime |
LocalSandbox |
Local runtime adapter name |
Jwt__Authority |
(required in production) | OIDC authority URL |
Jwt__Audience |
orchestration-api |
JWT audience claim |
Orchestrator__RetryPolicy__MaxRetries |
3 |
Global default max retries |
Orchestrator__RetryPolicy__Backoff |
exponential |
fixed or exponential |
For local development, copy appsettings.Development.json.example to appsettings.Development.json and fill in values. This file is .gitignored.
15. Useful Commands
# Run all tests
dotnet test
# Run only unit tests
dotnet test --filter "Category=Unit"
# Add a new schema migration
# (Use SQL scripts or schema versioning tools as appropriate.)
# Roll back last schema change (dev only)
# (Use SQL script rollback or schema versioning practices.)
# Watch-mode for API development
dotnet watch run --project src/UltimateAgents.Api
# Build the CLI as a self-contained binary
dotnet publish src/UltimateAgents.Cli -c Release -r linux-x64 --self-contained
# Check for vulnerable NuGet packages
dotnet list package --vulnerable --include-transitive
# Format code
dotnet format
# View structured logs (if using Seq locally)
open http://localhost:5341
Questions? Check Architecture.md for design decisions, or features.md for the full feature specification.