docs
This commit is contained in:
381
docs/Backend-Developer.md
Normal file
381
docs/Backend-Developer.md
Normal file
@@ -0,0 +1,381 @@
|
||||
# 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](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
|
||||
|
||||
1. [Getting Started](#1-getting-started)
|
||||
2. [Project Layout Cheat Sheet](#2-project-layout-cheat-sheet)
|
||||
3. [Domain Rules — Must Know](#3-domain-rules--must-know)
|
||||
4. [Adding a New Feature — Step-by-Step](#4-adding-a-new-feature--step-by-step)
|
||||
5. [Database & Migrations](#5-database--migrations)
|
||||
6. [Working with the Orchestrator Engine](#6-working-with-the-orchestrator-engine)
|
||||
7. [Agent Matching](#7-agent-matching)
|
||||
8. [Scheduling & Events](#8-scheduling--events)
|
||||
9. [Container Runtime](#9-container-runtime)
|
||||
10. [Error Handling & Escalation](#10-error-handling--escalation)
|
||||
11. [Testing Guidelines](#11-testing-guidelines)
|
||||
12. [Logging Conventions](#12-logging-conventions)
|
||||
13. [Common Pitfalls](#13-common-pitfalls)
|
||||
14. [Environment Variables & Config](#14-environment-variables--config)
|
||||
15. [Useful Commands](#15-useful-commands)
|
||||
|
||||
---
|
||||
|
||||
## 1. Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 9 SDK (`dotnet --version` should show `9.x.x`)
|
||||
- `dotnet restore`
|
||||
|
||||
```bash
|
||||
# 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)
|
||||
```
|
||||
|
||||
- `Domain` has **zero** NuGet dependencies.
|
||||
- `Application` depends only on `Domain` and its own interfaces (defined in `Application/Interfaces/`).
|
||||
- `Infrastructure` implements those interfaces. It is the only project that touches Dapper, the runtime adapter, or RabbitMQ.
|
||||
- Controllers in `Api` call 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
|
||||
|
||||
```csharp
|
||||
// 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_task` express ordering within or across groups.
|
||||
- `ForeachTask` expansion creates independent task instances linked by `next_task` / `previous_task`, placed into a sequential group.
|
||||
- `GotoTask` jumps to an arbitrary target (task or group) and merges an additional prompt into it.
|
||||
- `ConditionTask` evaluates 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.
|
||||
|
||||
1. **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.
|
||||
2. **Define the interface.** If the feature needs infrastructure (DB query, external call), add an interface to `Application/Interfaces/`.
|
||||
3. **Write the use case.** Add a command or query class in `Application/`. Use a plain service class to implement the handler. No Dapper here.
|
||||
4. **Implement infrastructure.** Implement the new interface in `Infrastructure/`. Use Dapper and schema scripts for persistence.
|
||||
5. **Expose via API.** Add or extend a controller in `Api/Controllers/`. Add a DTO and a FluentValidation validator.
|
||||
6. **Wire up DI.** Register the new service/repository in `Program.cs` or a dedicated `ServiceCollectionExtensions` class.
|
||||
7. **Add CLI command** (if user-facing). Add a subcommand to the appropriate file in `Cli/Commands/`.
|
||||
8. **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 `OrchestratorEngine` is allowed to write task execution state. Controllers must use the `StartTaskCommand` application 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 `GotoTaskId` is set, the engine merges `GotoPrompt` into the target task's `Prompt` (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 activates `group.Next` (if set).
|
||||
- `Parallel` — all children are dispatched simultaneously. When every child is complete, the group is marked complete and `group.Next` is activated.
|
||||
- Nested groups follow the same rules recursively — a nested group must itself be fully complete before its parent considers it done.
|
||||
- `group.Next` is a `GroupChildRef` and 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.
|
||||
|
||||
```csharp
|
||||
// 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:
|
||||
|
||||
```json
|
||||
"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 `name` and `version`.
|
||||
- `resources.cpu` and `resources.memory` must be positive values.
|
||||
- `network_policy` must be explicitly declared (empty is allowed, but `null` is rejected).
|
||||
|
||||
> **Never pass user-controlled strings directly into container exec calls.** Always use the tool definition's pre-declared `usage_guidelines` to 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.
|
||||
|
||||
```csharp
|
||||
// Good
|
||||
var result = await _createTaskService.ExecuteAsync(command, ct);
|
||||
return Ok(result);
|
||||
|
||||
// Bad — don't do this
|
||||
try { ... } catch (TaskNotFoundException) { return NotFound(); }
|
||||
```
|
||||
|
||||
### Escalation flow
|
||||
|
||||
1. Agent reports failure with `Escalation.Reason` and `Escalation.Prompt`.
|
||||
2. `EscalationHandler` checks the task's `RetryPolicy`.
|
||||
3. If retries remain: re-queue the task with incremented attempt counter and apply backoff.
|
||||
4. If retries exhausted: publish an `EscalationEvent` and set task state to `Escalated`.
|
||||
5. 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`, and `IEventDispatcher` with Moq.
|
||||
- Cover every `ComplexityEvaluator.Evaluate()` branch.
|
||||
- Cover cycle detection: test graphs with cycles, linear chains, and parallel groups.
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
```csharp
|
||||
_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 `.gitignore`d.
|
||||
|
||||
---
|
||||
|
||||
## 15. Useful Commands
|
||||
|
||||
```bash
|
||||
# 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](Architecture.md) for design decisions, or [features.md](features.md) for the full feature specification.
|
||||
Reference in New Issue
Block a user