A deterministic, multi-agent C# code generation pipeline that turns a natural-language prompt into a validated, runnable .NET project. Heavily inspired by Tamir Dresher's Deterministic Meets Squads and its reference repo, reimagined to execute the generated code in Azure Container Apps (ACA) Sandboxes running in West Central US.
Generated artifacts are not just statically analyzed — they are restored, built, tested, and smoke-run inside isolated ACA sandboxes before they reach the user. Findings from the sandbox feed the refinement loop until the build is clean or the iteration budget is exhausted.
Stack: .NET 10, Aspire AppHost, Azure Container Apps (Express region: West Central US), Azure Durable Task Scheduler, Azure Foundry (multi-deployment routing), Azure Blob Storage, Microsoft Entra ID (identity-based only — no SAS/shared keys).
The user submits a prompt in the web UI. The web tier persists state and starts a Durable Task orchestration. A worker container app drives each agent stage in sequence, calling Foundry deployments per stage, executing build/test commands in ACA sandboxes, and writing artifacts to Blob storage. The UI polls progressive state via per-stage tabs and exposes authenticated download links only after refinement reaches a terminal status.
flowchart LR
user([User]) --> web[Web UI / API\nCodegenSquad.Web]
web -->|enqueue + schedule| dts[(Durable Task\nScheduler)]
web -->|state, artifacts| blob[(Blob Storage)]
dts --> worker[Worker\nCodegenSquad.Worker]
worker -->|Intent / Plan / Generate /\nValidate / Refine| foundry[(Azure Foundry\nMulti-deployment)]
worker -->|exec restore/build/test/smoke| sandbox[(ACA Sandbox Group\nWest Central US)]
worker --> blob
web -->|"GET /api/generations/{id}"| dts
web -->|"GET /api/generations/{id}/artifacts/{aid}"| blob
sequenceDiagram
autonumber
participant U as User (Browser)
participant W as Web (CodegenSquad.Web)
participant J as Job Store (Blob)
participant D as Durable Task Scheduler
participant K as Worker (CodegenSquad.Worker)
participant F as Azure Foundry
participant S as ACA Sandbox Group
participant A as Artifact Store (Blob)
U->>W: POST /api/generations { prompt }
W->>J: EnqueueAsync(GenerationRequest)
W->>D: ScheduleNewOrchestrationInstance(GenerationOrchestration)
W-->>U: 202 Accepted { id }
D->>K: Start orchestration
rect rgba(60,140,255,0.08)
note over K,F: 1. Intent resolution (gpt-5.4)
K->>F: chat.completions(IntentRouter)
F-->>K: intent JSON
K->>J: UpdateState(intent)
end
rect rgba(60,140,255,0.08)
note over K,F: 2. Planning (gpt-5.4)
K->>F: chat.completions(Planning)
F-->>K: plan JSON
K->>J: UpdateState(plan)
end
rect rgba(60,140,255,0.08)
note over K,F: 3. Generation (gpt-5.3-codex)
K->>F: chat.completions(Generation)
alt 400 unsupported
K->>F: responses(Generation) /api/projects/.../openai/v1/responses
end
F-->>K: generated files
K->>J: UpdateState(files)
end
loop Validate -> Refine (max iterations)
rect rgba(46,204,113,0.10)
note over K,S: 4. Sandbox validation
K->>S: PUT /sandboxes (disk image)
S-->>K: sandbox id (Running)
K->>S: PUT /files (project tree)
K->>S: POST /executeShellCommand: dotnet restore / build / test / run --configuration Release
S-->>K: stdout / stderr / exitCode
K->>J: UpdateState(reports)
end
rect rgba(241,196,15,0.18)
note over K,F: 5. Refinement (gpt-5.4)
alt validation passed OR iteration budget exhausted
K-->>D: break loop
else issues remain
K->>F: chat.completions(Refinement)
F-->>K: patch set
K->>J: UpdateState(files, refinementIteration++)
end
end
end
K->>A: Upload project.zip + report.json
K->>J: Finalize (Succeeded / Failed / Rejected)
U->>W: GET /api/generations/{id}
W->>J: GetStateAsync
W-->>U: state + artifacts[].apiDownloadPath
note over U,W: Download buttons enable only when status is terminal (after refinement)
U->>W: GET /api/generations/{id}/artifacts/{aid} (Bearer token)
W->>A: Read artifact (managed identity)
W-->>U: project.zip / report.json
DTS is the deterministic orchestration runtime. It owns the question "given a workflow definition, what runs next, in what order, with what retry/replay semantics, surviving worker restarts?" — and it answers that question without any AI in the loop.
In this project:
- Workflow definition lives in
CodegenSquad.Worker/DurableGenerationTasks.cs.GenerationOrchestration : TaskOrchestrator<GenerationRequest, GenerationOutcome>is the orchestrator — replayable, deterministic code.- Each step is a
TaskActivity<TIn, TOut>:MarkRunningActivity,ResolveIntentActivity,PlanActivity,GenerateActivity,ValidateActivity,RefineActivity,UpdateProgressActivity,CompleteActivity,FailActivity. - The orchestrator calls them with
context.CallActivityAsync<...>(GenerationWorkflowNames.<Name>, input).
- Hosting. The
workercontainer app hosts the DTS worker via Aspire'sMicrosoft.DurableTask.Worker.AzureManagedintegration. Thewebcontainer app submits work viaMicrosoft.DurableTask.Client.AzureManaged. - Connection.
DurableTask__ConnectionString = "Endpoint=<dts-endpoint>;TaskHub=default;Authentication=ManagedIdentity;ClientID=<workload-mi-client-id>". - Region split. DTS isn't yet available in
westcentralus, so the scheduler runs incentraluswhile compute and the sandbox group sit inwestcentralus. The connection string crosses regions; data plane sandbox/Foundry calls stay in-region. - State persistence outside DTS. DTS gives us replayable workflow state. UI-visible state (intent, plan, files, reports, refinement iteration, artifacts) is written to Blob storage via
BlobGenerationJobStore.UpdateStateAsyncfrom inside the activities — that is what powers progressive tab updates in the UI.
Why DTS over a plain queue or in-memory orchestrator: deterministic replay, durable retries across worker recycles, and a single place to enforce the bounded refinement loop (pass, iteration budget reached, or non-improving score streak).
The "Squad" pattern (from tamirdresher/squad-agent-framework-demo) — the non-deterministic teammates
Tamir Dresher's squad-agent-framework-demo (companion to Deterministic Meets Squads) introduces a useful split that this project deliberately mirrors:
Deterministic durable workflow (DTS) + non-deterministic agent "squads" (Microsoft Agent Framework / MAF) wrapping LLM-backed work, so workflow correctness and AI variability are decoupled.
Concretely in the demo: a DTS workflow (IncidentExample.cs) does triage → enrichment → dynamic subsystem routing → mitigation → diagnose-loop. The non-deterministic parts are encapsulated in SquadAgent (a MAF AIAgent wrapping the GitHub Copilot CLI) and per-subsystem squads (DatabaseSquadExecutor, NetworkSquadExecutor, …) that the workflow routes to.
How this project re-uses that pattern:
| Squad demo concept | Codegen Squad equivalent |
|---|---|
Durable workflow (IncidentExample.cs) |
GenerationOrchestration in DurableGenerationTasks.cs |
Workflow executors (TriageExecutor, EnrichExecutor, …) |
DTS activities (ResolveIntentActivity, PlanActivity, GenerateActivity, ValidateActivity, RefineActivity, …) |
SquadAgent (MAF AIAgent wrapping Copilot CLI) |
FoundryModelAgent wrapping Azure Foundry chat-completions + Responses API with a per-stage deployment |
| Per-subsystem squads (DB / Network / Auth / Payments) | Per-stage StageAgents (IntentRouter, Planner, Generator, Reviewer, Refiner) with stage-specific Foundry deployments |
| Dynamic subsystem routing | Stage selection by GenerationStageRunner (file-type guardrails, refinement gating, .csproj protection) |
| AppHost wiring DTS emulator + Foundry Local | CodegenSquad.AppHost wiring DTS emulator container + Azure resources |
Key differences:
- The squad demo uses MAF + GitHub Copilot CLI as the agent runtime. We use Foundry deployments directly because we want per-stage model routing (codex for generation, gpt-5.4-mini for validation) and stay inside the Azure data plane.
- The squad demo's "deterministic vs non-deterministic" boundary is the contract between the DTS workflow and the
SquadAgent. Our equivalent boundary is the contract between DTS activities andIGenerationStageRunner/FoundryModelAgent. - We add a third execution surface beyond DTS + LLM: an ACA Sandbox Group that runs the generated code in isolation. That makes the refinement loop reactive to real
dotnet restore/build/test/runoutput, not just to LLM-self-graded findings.
Net: DTS is the bus that makes the pipeline reliable; the squad pattern is what makes the AI parts swappable and observable. ACA Sandboxes are the third leg — they turn "the model said it works" into "we just ran it".
| # | Stage | Component | Foundry deployment | Output persisted |
|---|---|---|---|---|
| 1 | Intent resolution | StageAgents.IntentRouter |
gpt-5.4 |
state.intent |
| 2 | Planning | StageAgents.Planner |
gpt-5.4 |
state.plan |
| 3 | Generation | StageAgents.Generator |
gpt-5.3-codex (fallback to Responses API) |
state.files |
| 4 | Validation | SandboxExecutionValidator + provider selector (SandboxGroup default, OnDemandSandbox scaffolded) |
n/a | state.reports, state.score, state.scoreHistory |
| 4b | Reviewer / static checks | StageAgents.Reviewer |
gpt-5.4-mini |
findings in report |
| 5 | Refinement | StageAgents.Refiner |
gpt-5.4 |
state.files, refinementIteration++ |
Notes:
- Generation fallback.
gpt-5.3-codexis exposed via the Responses API on the Foundry project endpoint (/api/projects/{project}/openai/v1/responses). The chat-completions path is tried first; on400 unsupported, the agent retries with typedmessageitems (input_textcontent) on the Responses endpoint. - Refinement is score-driven. Every validation computes a score (
0.00-1.00) from findings + sandbox execution outcomes. A pass requires score >= threshold (default0.85), no blocking findings, and design-review approval. - Refinement is bounded. Iterations stop on pass, on
GenerationConstraints.MaxRefinementIterations(default 3), or after two consecutive non-improving score deltas (epsilon = 0.02). - Project files are authoritative. Scaffolded
.csproj/.slnfiles are protected — the generator/refiner cannot overwrite them once set. - Failure context is preserved.
FailAsynckeeps the last known intent / plan / files / reports so the UI can still show what happened at the failure point.
Static checks alone don't tell you the project runs. ACA Sandbox Groups provide:
- Sub-second start from prewarmed pools
- Strong isolation per sandbox (safe for untrusted, model-generated code)
- Programmable lifecycle (
PUT /sandboxes,POST /executeShellCommand,DELETE /sandboxes/{id}) - Scale-to-zero billing
- Identity-based access via Microsoft Entra ID (
https://dynamicsessions.io/.defaultscope)
Data plane: https://management.{region}.azuredevcompute.io. Control plane (sandbox group ARM resource) is in management.azure.com. The application targets 2026-02-01-preview.
A quick mental-model correction worth calling out, because it trips a lot of people up:
ACA Express and ACA Sandboxes are two different ACA compute primitives that happen to launch in the same region. Express is not the platform that sandboxes run on. Both are siblings — like Apps and Jobs — under the broader Azure Container Apps umbrella.
| ACA primitive | Resource | What it hosts | Lifecycle owner | Used here for |
|---|---|---|---|---|
| Apps (standard) | Microsoft.App/containerApps in a managedEnvironment |
Long-running services with ingress, scaling, networking | You (provision env + apps) | Today: hosts web and worker |
| ACA Express | Microsoft.App/containerApps (no environment to provision) |
Same kind of services as Apps, but with no environment, sub-second cold starts, scale-to-zero by default | Platform (Microsoft manages capacity) | Future option for web / worker to drop the env management surface |
| Jobs | Microsoft.App/jobs |
Run-to-completion batch tasks | You | Not used |
| Dynamic Sessions | Microsoft.App/sessionPools |
Managed code-interpreter / pooled sessions accessed via HTTP | Platform pool | Not used |
| Sandboxes | Microsoft.App/SandboxGroups + child sandboxes on management.{region}.azuredevcompute.io |
Programmable, isolated, snapshot-able compute used to run untrusted / model-generated code | You (sandbox group + per-call sandboxes) | Today: runs dotnet restore/build/test/run against generated projects |
So:
- Sandboxes do not "run on" Express. A Sandbox Group is its own ARM resource (
Microsoft.App/SandboxGroups) with its own data plane (management.{region}.azuredevcompute.io). It does not need an ACA Environment, and it does not need Express. - Express is a hosting option for our own services (web/worker). It removes the need to manage an ACA Environment and gives sub-second cold starts and scale-to-zero defaults — useful when this app grows into an "AI-spins-up-an-endpoint-on-demand" pattern.
- What ties them together is the region. Both Express and Sandbox Groups launched in West Central US first. Deploying our services there keeps web/worker/sandbox all co-located, which minimizes data-plane latency and cross-region egress, and lets us migrate web/worker onto Express later without changing the sandbox topology.
For the avoidance of doubt — the live deployment confirmed against Azure today:
| Service | Resource | Hosting model | Network |
|---|---|---|---|
web |
Microsoft.App/containerApps ca-web-codegensquad-<suffix> |
Standard ACA Environment (Microsoft.App/managedEnvironments, SKU Consumption, workload profile Consumption) in westcentralus |
VNet-integrated (dedicated infra subnet + NAT egress) |
worker |
Microsoft.App/containerApps ca-worker-codegensquad-<suffix> |
Same environment as web |
Same |
| Sandbox Group | Microsoft.App/SandboxGroups sg-codegensquad-<suffix> |
Sandbox data plane (management.westcentralus.azuredevcompute.io) |
Identity-bound via Entra; control plane via ARM |
Not deployed on ACA Express today. Express has no
managedEnvironment, so it can't sit inside the existing VNet with private endpoints to Blob/Key Vault and the workload-identity-only access path the tenant policy enforces. We chose a standard ACA Environment specifically to keep VNet integration, private endpoints, and the no-SAS / managed-identity-only constraints intact. Express remains a candidate migration once those gaps close in preview.
Today this project deploys web and worker on a standard ACA Environment in West Central US (so we keep VNet integration, private endpoints to Blob/Key Vault, and a managed identity model that the tenant policy requires). Sandboxes are used for execution. Both live in the same region. Express is a candidate future migration for the services tier, not a prerequisite for the sandbox tier.
SandboxGroupExecutionProvider.BuildShellCommand issues, per project:
dotnet restore "<project>"
dotnet build "<project>" --no-restore --configuration Release
dotnet test "<project>" --no-build --no-restore --configuration Release # if RequireTests
dotnet run --project "<project>" --no-build --no-restore --configuration Release # if RequireRuntimeSmoke
Each command is prefixed with a .NET SDK bootstrap script so a sandbox image without dotnet on PATH can still self-install the SDK into $HOME/.dotnet (channel 10.0, fallback STS). This keeps the pipeline working on the default copilot sandbox image until a fully provisioned .NET image is wired in.
| Provider | Current status | How selected | Notes |
|---|---|---|---|
SandboxGroup |
Active (default) | Sandbox__Provider=SandboxGroup (or leave unset with current preference flags) |
Uses ACA Sandbox Group data plane directly. |
OnDemandSandbox |
Scaffolded (preview-gated) | Sandbox__Provider=OnDemandSandbox + Sandbox__EnableOnDemandSandbox=true + Sandbox__OnDemandSandboxImage=<image> |
Currently throws a clear "preview not enabled" message until DTS On-demand Sandboxes is enabled for the scheduler and runner profile wiring is completed. |
⚠️ Yes — supporting non-.NET stacks is constrained by what's preinstalled in the sandbox disk image.
The runtime stack of every generated project is decided by which sandbox disk image the worker boots:
- Disk image is configurable via env vars:
SANDBOX_GROUP_DISK_IMAGE_NAME(public image name), orSANDBOX_GROUP_DISK_IMAGE_ID(full ARM resource ID; takes precedence).
- These are passed through
infra/{worker,web}.parameters.json→infra/{worker,web}.bicep→ container app env (Sandbox__SandboxGroupDiskImageName/Sandbox__SandboxGroupDiskImageId). - For
.NETtoday, the defaultcopilotimage plus the runtime bootstrap script is sufficient. - For Python / Node.js / Go / Java, the practical options are:
- Pre-built public images that already include the needed runtime (preferred, fastest).
- Custom OCI images pushed to ACR, registered against the sandbox group via
sourcesRef.diskImage.id(full lifecycle, but you own the image build). - Bootstrap-on-demand, similar to the .NET fallback: download the runtime at execution time. This works for small toolchains but adds 30–120 s per sandbox boot and fails fast if egress is restricted.
- The pipeline already abstracts shell command construction in
SandboxGroupExecutionProvider.BuildShellCommand, so addingpytest,npm test, etc. is a focused change.
The honest summary: The framework is stack-agnostic. The blocker for "ask in Python, get Python" is having a sandbox disk image that ships with the right runtime for that stack.
- Managed identities only. Both web and worker container apps run with a workload-assigned User-Assigned Managed Identity. Foundry, Durable Task Scheduler, Blob Storage, and the Sandbox Group all consume that identity. No SAS, no shared key.
- Blob storage is private. Public network access is disabled; access uses private endpoints + private DNS, contributor role on the workload MI.
- Artifact downloads must go through the app. Direct
downloadUrilinks (returned for diagnostic completeness) fail withAuthorizationFailureunder tenant policy. The UI usesapiDownloadPath(/api/generations/{id}/artifacts/{aid}) which streams via the web app under bearer auth. - Authentication. Microsoft Entra ID JWT bearer on the API, PKCE SPA flow in the embedded UI. Configured via
Authentication__Authority,Authentication__Audience, andAuthentication__SpaClientId / SpaRedirectUri / SpaScope. - Durable Task Scheduler runs in
centralus(DTS isn't currently available in West Central US); compute is inwestcentralus. Cross-region connection string is wired viaDurableTask__ConnectionStringwithAuthentication=ManagedIdentity.
codegen-squad/
├── src/
│ ├── CodegenSquad.AppHost/ # Aspire orchestration for local dev
│ ├── CodegenSquad.Domain/ # Core models, enums, contracts
│ ├── CodegenSquad.Application/ # Stage runner, workflow names, abstractions
│ ├── CodegenSquad.Infrastructure/
│ │ ├── Foundry/ # FoundryModelAgent, StageAgents
│ │ ├── Execution/ # SandboxGroupExecutionProvider (data plane)
│ │ ├── Validation/ # SandboxExecutionValidator, dependency/static checks
│ │ ├── Jobs/ # BlobGenerationJobStore, FileGenerationJobStore
│ │ └── Options/ # Strongly-typed config
│ ├── CodegenSquad.Worker/ # Durable Task worker host, activities
│ ├── CodegenSquad.Web/ # Minimal API + embedded SPA UI
│ └── CodegenSquad.SandboxRunner/ # Optional in-cluster runner image
├── tests/ # xUnit projects per layer
├── infra/ # Bicep + ARM JSON (main, worker, web, sandbox-group)
└── azure.yaml # AZD service map (web, worker, sandbox-runner)
- Tabs auto-advance through Intent → Planner → Development → Validation → Refinement → Final Output based on progressive state writes from the worker.
- Each panel shows the live in-progress message for the current stage rather than a static "not available yet" placeholder.
- Validation/refinement/final tabs show score telemetry (
score,bestScore,passThreshold, andscoreHistorytrend). - The Generate button focuses the intent tab immediately, then progresses as the worker emits state updates.
- Artifact download buttons are rendered only after the job reaches a terminal status (
Succeeded/Failed/Rejected) — i.e., refinement has finished. Until then the UI says "Downloads are enabled after refinement finishes." - Downloads stream through the authenticated API endpoint (the raw blob URL is intentionally not used).
- The auth status banner displays
Signed in as <user>by parsing the access token claims (name,preferred_username,upn,email,unique_name,oidin that order), so the operator can tell which Entra identity is driving the session.
azd auth login
azd env new <env-name> # one-time
# Set the foundational config:
azd env set FOUNDRY_ENDPOINT "https://<resource>.services.ai.azure.com/api/projects/<project>"
azd env set AUTHENTICATION_AUTHORITY "https://login.microsoftonline.com/<tenant-id>/v2.0"
azd env set AUTHENTICATION_AUDIENCE "api://<api-app-id>"
azd env set AUTHENTICATION_SPA_CLIENT_ID "<spa-app-id>"
azd env set AUTHENTICATION_SPA_REDIRECT_URI "<https web endpoint>"
azd env set AUTHENTICATION_SPA_SCOPE "api://<api-app-id>/access_as_user"
# Optional: pin a sandbox disk image (e.g., with .NET preinstalled):
azd env set SANDBOX_GROUP_DISK_IMAGE_ID "<full-arm-resource-id>"
# or:
azd env set SANDBOX_GROUP_DISK_IMAGE_NAME "<public-image-name>"
# Optional: preview scaffold for DTS on-demand sandbox provider (currently gated):
azd env set SANDBOX_PROVIDER "OnDemandSandbox"
azd env set ENABLE_ON_DEMAND_SANDBOX "true"
azd env set ON_DEMAND_SANDBOX_IMAGE "<runner-image>"
azd env set ON_DEMAND_SANDBOX_PROFILE "codegen-runner"
azd up # provision + deploy
# or, after subsequent changes:
azd deploy worker --no-prompt
azd deploy web --no-promptRegion: westcentralus for compute and the sandbox group, centralus for Durable Task Scheduler.
dotnet build CodegenSquad.slnx -v minimal
dotnet test CodegenSquad.slnx -v minimal
# Run via Aspire AppHost (uses local DTS emulator + Azure resources for blob/foundry)
dotnet run --project src/CodegenSquad.AppHostThe AppHost reads SANDBOX_GROUP_DISK_IMAGE_NAME / SANDBOX_GROUP_DISK_IMAGE_ID from environment when publishing.
- Tamir Dresher — Deterministic Meets Squads: https://www.tamirdresher.com/blog/2026/05/21/deterministic-meets-squads
- Reference repo: https://github.com/tamirdresher/squad-agent-framework-demo
- Introducing Azure Container Apps Express: https://techcommunity.microsoft.com/blog/appsonazureblog/introducing-azure-container-apps-express/4519150
- ACA Express deep dive (Mighty BS): https://mightybs.com/posts/2026-05-13-fast-track-your-apps-introducing-azure-container-apps-express/
- ACA Sandboxes overview: https://github.com/microsoft/azure-container-apps/blob/main/docs/early/sandboxes-overview.md
- Building reliable AI coding workflows (the static-analysis approach this project deliberately goes beyond): https://techcommunity.microsoft.com/blog/educatordeveloperblog/building-reliable-ai-coding-workflows-using-modular-ai-agent-optimization/4523252