Skip to content

The generated tree ​

aid gen writes into app/. This page is what lands there, which files are yours, and what happens on the second run.

Two numbers, one rule ​

TreeFiles it shipsManaged by the generator
The tutorial from getting started2421
examples/support-copilot6244

The difference is not the generator being inconsistent. It is the rule: the generator only manages what it can reproduce from the spec. A file you write - app/main.py, an eval dataset, a baseline, a document a retriever reads - appears on disk and never in the manifest. app/app.spec.yaml and app/aid.manifest.json are always unmanaged, for the obvious reason.

The tutorial tree, in full:

text
app/
  aid.manifest.json                     unmanaged
  app.spec.yaml                         unmanaged
  business/
    __init__.py                         business
    prompts/__init__.py                 business
    tools/__init__.py                   business
  generated/
    __init__.py                         generated
    contracts/__init__.py               generated
    deps.py                             generated
    evals/__init__.py                   generated
    gateway/__init__.py                 generated
    gateway/models.py                   generated
    guardrails.py                       generated
    observability.py                    generated
    pipelines/__init__.py               generated
    pipelines/hello.py                  generated
    prompts/__init__.py                 generated
    prompts/greet.py                    generated
    requests.py                         generated
    routes/__init__.py                  generated
    routes/hello.py                     generated
    schemas.py                          generated
    tools/__init__.py                   generated
.github/
  workflows/
    ci.yml                              scaffold
    evals.yml                           scaffold, when a `gate.ci` eval is declared
    infra-plan.yml                      scaffold
    release.yml                         scaffold, when a deployment declares `image`

evals.yml is scaffolded when the spec declares an eval with gate.ci: true; examples/support-copilot has two, the tutorial has none. infra-plan.yml is scaffolded for every project, because it detects its own inputs instead of assuming an infra/ directory exists. release.yml is scaffolded when a deployment declares deployments[].image (spec 0.4), because a project that never named a registry repository has nothing to publish.

The three owned tiers ​

  • app/generated/** is the generator's. It is rewritten whenever the spec or the generator changes, so an edit here survives until the next run and then becomes drift.
  • app/business/** is yours. It is created once, never overwritten, never deleted - not even by --force. Tool bodies, prompt overrides and domain rules live here.
  • .github/workflows/** is scaffolded once and then yours. It is the only thing outside app/, and it exists because a generated project is not deployable without a pipeline: the workflow runs the determinism gate (aid gen --check), lint, build and tests on every pull request, and checks the committed infra/ tree against the spec when the project has one. The eval workflow runs on demand and nightly, because it measures a real model and a pull request from a fork cannot hold the credential it needs. That same workflow records the spans each eval run produces (.aid-traces) and reads them back with aid cost, so a run reports what it spent; setting the repository variable AID_COST_BUDGET turns that report into a ceiling the run fails against. The infra plan is manual for the same reason the eval run is: it assumes a cloud role through OIDC (AID_INFRA_ROLE_ARN, AID_INFRA_REGION), and a plan against a real account is something a human dispatches. It initialises the environment root before calling aid infra plan, which drives tofu or cdk but does not run init itself. The release workflow is the one pipeline a tag pushes: it re-checks the tagged tree against the spec, then builds the image the deployment named and pushes it to that registry, tagged with both the release name and the commit. Its credential is the token the run already holds, which is why it asks for packages: write and nothing wider, and publishing an image is not deploying it - no infrastructure is applied and no service restarts.

Every file carries the tier in its first line, so a reader can tell without consulting the manifest:

text
# Code generated by AID Framework "@aid-framework/generator-py-fastapi" v0.1.0. DO NOT EDIT.
# Owned by the generator: regenerate with `aid gen`. See aid.manifest.json.
text
# Created once by AID Framework "@aid-framework/generator-py-fastapi" v0.1.0. Edit freely.
# The generator never overwrites this file: it is yours. See aid.manifest.json.
text
# Generated once by `aid gen` for the `py-fastapi` target, then yours.
# A scaffold file is never overwritten and never recorded in aid.manifest.json,
# so edit it freely: a new runner, a cache, an extra toolchain.

What each generated module is for ​

ModuleComes from
schemas.pytypes with kind: struct - the response and structured-output models.
requests.pyOne request model per HTTP trigger, derived from the pipeline's declared inputs.
contracts/A module per tool, holding its input and output types.
prompts/<name>.pyA rendered template per prompts entry, plus its variable contract.
pipelines/<name>.pyThe steps as awaitables, in declared order, with guardrail enforcement inlined.
routes/<name>.pyThe HTTP endpoint for one pipeline.
agents.pyagent_router, carrying POST /agents/{agent_id}/run and POST /agents/resume.
guardrails.pyThe policy table and guardrail_block, which turns a block into an HTTP 400.
gateway/models.pyModel aliases, params, cost profiles and the fallback chain as data.
deps.pyThe Deps dataclass, build_deps() and the get_deps() singleton.
observability.pyTrace hooks the runtime calls around each step.
sse.pyEvent-stream framing. Emitted for a streaming deployment only.
DockerfileThe image a deployment with target: container runs.
retrieval/bindings.py for query-time search, ingest/<store>.py for the write path.
evals/<name>.pyOne module per eval, runnable through aid eval.

retrieval/ is conditional: the tutorial spec declares no retriever and gets neither bindings.py nor ingest/, while support-copilot gets both. contracts/__init__.py is always emitted because it holds ToolContext - the dataclass the generated adapter hands a tool - even when no tool is declared. evals/__init__.py is only a package marker; the eval modules beside it appear once evals is declared.

Dockerfile is conditional too, and on a different axis: it exists only when a deployment declares target: container. Where the app runs is the only thing that can justify an image, and a project that deploys nowhere as a container is not missing a file. Because the build needs pyproject.toml, which sits above app/, the context is the application root and the file is named explicitly:

console
docker build -f app/generated/Dockerfile -t support-copilot .
docker run --rm -p 8080:8080 -e OPENAI_API_KEY=... support-copilot

The image runs app.main:app - the same entry point the developer runs locally - as a non-root user, with exec so the platform's SIGTERM reaches uvicorn. No secret is a build argument or an environment layer: the process reads a declared secret when a model is called.

The manifest ​

app/aid.manifest.json has exactly three top-level keys:

json
{
  "manifestVersion": 1,
  "generatedBy": {
    "generator": "@aid-framework/generator-py-fastapi",
    "generatorVersion": "0.1.0",
    "irVersion": "0.1.0"
  },
  "files": [
    {
      "path": "app/generated/routes/hello.py",
      "owner": "generated",
      "sha256": "�",
      "generator": "@aid-framework/generator-py-fastapi",
      "generatorVersion": "0.1.0"
    }
  ]
}

Two things to read carefully.

generatorVersion is 0.1.0, not the package version. The generator plugin declares its own version in its manifest, independent of the npm package that ships it. This field stays at 0.1.0 across package releases - it is the plugin's contract version, not a stale install. Use irVersion to reason about IR compatibility and manifestVersion to reason about the manifest's own shape.

There is no spec hash. Drift is computed per file from sha256 against what is on disk, so a spec change that does not alter a file leaves it unchanged. That is what makes regeneration cheap and what makes a CI job that runs aid gen and asserts a clean tree meaningful.

The second run ​

Regenerating an unmodified tree is a no-op, and the summary says so:

console
$ aid gen
py-fastapi: 0 created, 0 updated, 17 unchanged, 3 kept, 2 scaffold kept

kept is the important word: it is a business/ file that already existed, and the generator declined to touch it. scaffold kept is the same promise for a workflow file: it was written once, it is yours now, and the generator will neither rewrite it nor count an edit as drift. unchanged means a generated file whose bytes already match the plan.

When you edit a generated file ​

The manifest hash is the detector. If app/generated/routes/hello.py on disk does not match its recorded hash, the generator treats it as a human edit and:

  1. refuses to overwrite it,
  2. writes the new bytes beside it as app/generated/routes/hello.py.aid-rej,
  3. exits non-zero with guidance naming the path.

--force discards the edit and rewrites the file. It does not touch business/. To keep the edit instead, run aid eject <path>: it moves the file to business/ and stops the generator from ever planning that path again.

Files that are no longer in the plan are reported as "no longer generated" and left on disk. Deletion needs an explicit prune, because it is the one operation with no recovery.

Infrastructure ​

aid infra gen writes the same kind of tree one level up. Everything under infra/generated/ is derived and managed; everything else under infra/ is yours:

text
infra/
  aid.infra.manifest.json               managed record
  generated/
    deploy.plan.json                    generated
    deploy.auto.tfvars.json             generated
    deploy.cdk-context.json             generated
    README.md                           generated
    modules/serverless/                 generated, when a deployment targets serverless
      versions.tf
      variables.tf
      main.tf
      outputs.tf
    modules/container/                  generated, when a deployment targets container
      versions.tf
      variables.tf
      main.tf
      outputs.tf
    modules/batch/                      generated, when a deployment targets batch on AWS
      versions.tf
      variables.tf
      main.tf
      outputs.tf
    modules/edge/                       generated, when a deployment targets edge on AWS
      versions.tf
      variables.tf
      main.tf
      outputs.tf
    modules/cloudrun/                   generated, when a deployment targets container or serverless on GCP
      versions.tf
      variables.tf
      main.tf
      outputs.tf
    modules/cloudrunjob/                generated, when a deployment targets batch on GCP
      versions.tf
      variables.tf
      main.tf
      outputs.tf
    cdk/serverless.cjs                  generated, when a deployment targets serverless on AWS
  envs/dev/                             yours
    main.tf
    variables.tf
  envs/gcp-dev/                         yours, a sibling root for `--cloud gcp`
    main.tf
    variables.tf

The plan and both spellings of it are always emitted. A module is emitted only when a deployment declares its target, so a serverless-only project on AWS gains no modules/container directory to ignore. On GCP serverless and container share modules/cloudrun, because both run the same image, and batch becomes modules/cloudrunjob, because a Cloud Run job takes the same image too. edge exists only on AWS, because it is Lambda@Edge on CloudFront. Each module's bytes are constant: it loops over the generated tfvars instead of naming services, which is what lets it be validated once and be correct for every project.

Beside the modules sits one CDK file: generated/cdk/serverless.cjs. It is the AWS CDK counterpart of modules/serverless - the construct an app adds to a stack with addServerless(scope, context, inputs) - and it reads deploy.cdk-context.json, the same document the HCL module reads, so the two backends cannot disagree about what a service is. It is emitted only on AWS and only when a deployment targets serverless; OpenTofu/Terraform stays the default and CDK is the alternative. The extension is .cjs rather than .js because a generated project may be an ES module, and a .js construct there would be parsed as ESM before require could run. Its bytes are constant for the same reason the modules' are: cdk synth validates it once in CI instead of once per project.

The env root is yours and is never written by aid infra gen. infra/envs/<env>/main.tf (AWS) or infra/envs/gcp-<env>/main.tf (GCP) calls one module per platform the plan targets - ../../generated/modules/serverless, ../../generated/modules/container, ../../generated/modules/batch, ../../generated/modules/edge, or ../../generated/modules/cloudrun on GCP - plus ../../generated/modules/cloudrunjob when a deployment targets batch there - and variables.tf declares the inputs no spec can supply. An edge-only root declares none of image_uris, secret_arns or the network inputs: the function is a zip, and its configuration is the secret the module creates.

InputWhy it cannot be derived
image_urisdeployments[].image is a ghcr.io registry, and Lambda cannot pull from it.
secret_arnsAn ARN names one account, so it belongs to the environment, not the spec.
sizingMemory and timeout are a cost decision, not an inference from the pipeline.
container_sizingCPU units, memory and task count are a budget decision.
batch_sizingvCPU, memory, attempts and the run timeout are a budget decision; GCP reads the same input as a Cloud Run job's CPU, memory, retries and task timeout.
batch_subnet_ids, batch_security_group_idsA batch task runs in a network you own; the module refuses an empty subnet list rather than guessing a default VPC.
batch_assign_public_ip, batch_max_vcpus, batch_log_retention_daysPublic addressing, queue ceiling and log retention are operator decisions.
edge_zip_pathThe function's archive is built from the app; where it lands is a build decision, and the module refuses to plan without it.
edge_sizingMemory and timeout are a budget decision; Lambda@Edge caps the timeout at 30 seconds.
edge_aliases, edge_certificate_arnA hostname and its certificate belong to a domain you own.
edge_price_classWhich CloudFront edge locations serve the app is a cost decision.
bedrock_regionBedrock models may live in a different region than the stack; empty means the stack's region.
gcp_projectA GCP project id names one account, so the provider is configured from it and the module names it in a Vertex grant.
regionCloud Run names the region in a URL; it is a deployment choice.
secret_idsA Secret Manager id names one project, the GCP counterpart of an ARN.
cloudrun_sizingCPU, memory, instance bounds and concurrency are a budget decision.
vpc_cidrA private range is an operator decision; the module refuses to guess one.
public_subnet_ids, private_subnet_idsOnly meaningful with vpc_id, when you bring a VPC.

The modules turn a spec mistake into a plan-time refusal rather than a broken deploy. The serverless module refuses a streaming service (an HTTP API cannot deliver a streamed body; on GCP the same service deploys as serverless on Cloud Run), a service with no image, and a secret with no ARN. The container module refuses those too, plus two services claiming one path - a load balancer routes by path, not by method - a load balancer with fewer than two public subnets, and a service with fewer than two private subnets. The Cloud Run module refuses a service with no image, a secret with no Secret Manager id, and a name Cloud Run cannot route; it does not refuse streaming, because Cloud Run forwards a streamed body. The batch module refuses a deployment with no pipeline (the job would run nothing), one with no image, a secret with no ARN, and an empty subnet list. The Cloud Run job module refuses the same missing pipeline and image, a secret with no Secret Manager id, and a name Cloud Run cannot accept; it needs no subnet, because a job runs on Google's network. The edge module refuses a build with more than one edge deployment (one distribution serves the app), a streaming service, an edge_zip_path that does not exist, a timeout above 30 seconds, and memory outside 128 to 3008 MB. Each refusal is a precondition whose message names the input to fix.

The container module runs each service as a Fargate task behind one load balancer, in private subnets with egress through a single NAT gateway and reachable only from the balancer. Tasks listen on 8080 because that is the port the generated Dockerfile exposes, and the target group probes health_check_path (default /) accepting any answer below 500: the generated app declares no health route, and a 404 still proves the task is serving.

The module sets no registry credentials, so the tag in image_uris must be pullable without them: a public ghcr.io package, or the image copied to ECR. A private package needs a pull secret that belongs to your account, and the module will not invent one.

The batch module runs each job on Fargate behind one managed compute environment and one job queue, with a job definition per batch deployment and a CloudWatch log group per job. The job command is python -m app.generated.batch <pipeline>, using the pipeline the deployment names: a build with more than one manual pipeline cannot resolve a default, and a job definition is the thing that knows which job it is. Secrets become job-definition secrets entries resolved by the execution role, so a secret value never enters state or a plan file. Submission targets the queue the module outputs, for example aws batch submit-job --job-queue <job_queue_arn> --job-name nightly --job-definition <arn>.

A service that names bedrock in its providers gets its credentials from IAM rather than from an API key, so each module adds the grant that makes that true. The serverless and container modules attach bedrock:InvokeModel to the function or task role, add bedrock:InvokeModelWithResponseStream only when the service streams, and set AWS_REGION from bedrock_region (or the stack's region) because the Bedrock client reads it. The batch module had no task role at all, which leaves a job with no credentials, so it now creates one and passes it as taskRoleArn; the grant applies to a job only when a job in that build names Bedrock. The policy is scoped to foundation-model/* and to the account's inference-profile/*, which is what a cross-region inference profile needs. A build that names only openai or anthropic gets no policy, and every other service is unaffected.

An edge deployment becomes one Lambda@Edge function on one CloudFront distribution. The function must live in us-east-1, and CloudFront can associate only a numbered version, so the module pins a us-east-1 provider alias, publishes the function, and associates its qualified_arn through an origin-request association that includes the body. The role trusts both lambda.amazonaws.com and edgelambda.amazonaws.com, because a replica is invoked by the latter. Lambda@Edge forbids environment variables, so the module leaves the function's environment empty and creates an empty Secrets Manager secret named after the function; the generated handler reads it on its first request and copies its entries into os.environ, which is how the same pipeline code sees the configuration a serverless deployment passes through environment. Populate that secret after the first apply. The association answers origin requests for the app's HTTP pipeline routes; agent routes stay in app.main:app, which a Lambda@Edge association does not run. CloudFront still requires an origin, so the module names a documented placeholder and never fetches from it.

--cloud gcp selects Cloud Run for a container service, and for a serverless one too. The gen2 Functions API accepts only a source zip and builds it, so it would mean a second packaging pipeline; Cloud Run is what gen2 runs on, and it takes the image the generator already produces. A serverless service still scales to zero and serves one request per instance unless cloudrun_sizing lowers that further, so the function model is kept. Nothing else changes: the image is the same one the Dockerfile builds, because that Dockerfile already binds $PORT on 0.0.0.0. The module creates a service account per service, grants it secretAccessor on each declared secret, references secrets by secret_key_ref rather than inlining them, and creates one Artifact Registry repository for the images. Invocation is public by default but through an explicit allUsers binding, so it can be turned off with allow_unauthenticated = false. gcp_project and region are the only account facts it needs; there is no VPC input, because Cloud Run provisions no network of its own.

streaming is served here from either target, so a service that must move to a container on AWS because API Gateway cannot stream needs no change on GCP.

A batch deployment lands on a Cloud Run job. The stable hashicorp/google provider has no Cloud Batch resource, and Cloud Run jobs is the service that runs one container to completion and then stops: task_count = 1 and parallelism = 1, so a retried attempt cannot run two model calls at once. The module invokes the same entrypoint the AWS module does, python -m app.generated.batch <pipeline>, seals secrets into secret_key_ref, grants the job's service account secretAccessor and roles/logging.logWriter, and grants roles/aiplatform.user when a model names the google provider. batch_sizing maps to CPU, memory, retries (attempts - 1) and the task timeout. Creating the job does not run it; gcloud run jobs execute <name> --region <region> --wait starts it, which keeps a deploy from spending model budget.

A service whose models name the google provider gets roles/aiplatform.user on that project, and GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_REGION in its environment. Both are needed: the Vertex adapter reads its project and location from the environment and Cloud Run sets neither, and the role is what lets the service account call the model. No key is stored, because the same service account authenticates. This mirrors the AWS bedrock grant, and it is derived from services[].providers rather than from a flag, so a build that names no provider gets no grant and no extra variable. A service that declares either variable itself keeps its own value, so pointing one service at another project is a decision, not an overwrite.

Generated AWS modules exist for serverless, container, batch and edge; GCP has one that covers container and serverless (Cloud Run) and one that covers batch (a Cloud Run job). --cloud gcp refuses a plan containing a target it has no module for, naming each service, rather than emitting a tree that silently omits it.

aid infra plan and aid infra apply spawn the backend but never run init, because initialising is a decision about a backend and a lock table, not a consequence of the spec. Run it yourself in the env root first:

console
tofu -chdir=infra/envs/dev init
tofu -chdir=infra/envs/gcp-dev init
aid infra plan --env dev

The generated modules are validated in CI: a job runs tofu init -backend=false and tofu validate over both fixtures under packages/cli/test/fixtures/infra/envs, which are asserted byte-identical to what the emitter writes. A second job runs cdk synth over packages/cli/test/fixtures/infra/cdk and fails if the construct produces any validation message - the counterpart check for the backend that is not the default. Both prove the committed fixture is what the emitter writes and that the toolchain still accepts it.

What you write ​

  • app/main.py - the ASGI entry point. aid gen never emits it. See Serving.
  • app/business/tools/__init__.py - the IMPLEMENTATIONS mapping the generated executor reads. Adding a tool means adding it here too.
  • Any prompt override or tool body the example ships in business/.
  • app/evals/*.json datasets and *.baseline.json files.

Read next: Serving for what to do with the tree once it exists.

Latest release v0.1.98 · Released under the Apache-2.0 licence · Licence · Notice · Security