Skip to content

docs: add AGENTS.md contributor guide for AI agents#369

Merged
ethanndickson merged 3 commits into
mainfrom
docs/add-agents-md
Jul 1, 2026
Merged

docs: add AGENTS.md contributor guide for AI agents#369
ethanndickson merged 3 commits into
mainfrom
docs/add-agents-md

Conversation

@ethanndickson

Copy link
Copy Markdown
Member

Adds an AGENTS.md contributor guide at the repo root to give AI coding agents the high-signal context they need to work in this provider safely.

The document captures the provider's framework idioms (terraform-plugin-framework, not SDKv2), the resource file layout, essential make commands, and — most importantly — the project-specific patterns and anti-patterns distilled from past fixes (e.g. handling unknown values at plan/validate time, refreshing cached entitlements after a license apply, not stripping cty sensitivity marks in plan modifiers, and avoiding defer inside retry loops). Each anti-pattern references the PR/issue it was learned from.

Contents were verified against the current tree: Makefile targets, the util.go helpers, provider.go entitlement plumbing, the CI Terraform 1.01.9 acceptance matrix, and the golangci paralleltest config.

@ethanndickson ethanndickson self-assigned this Jun 23, 2026
@ethanndickson ethanndickson changed the base branch from main to graphite-base/369 June 24, 2026 05:12
@ethanndickson ethanndickson changed the base branch from graphite-base/369 to ethan/ai-provider-resource June 24, 2026 05:12
@ethanndickson ethanndickson force-pushed the docs/add-agents-md branch 4 times, most recently from f77324f to 2ec71e9 Compare June 24, 2026 07:56
@ethanndickson ethanndickson force-pushed the ethan/ai-provider-resource branch from d51dad0 to b1d81a3 Compare June 24, 2026 08:10
@ethanndickson ethanndickson force-pushed the ethan/ai-provider-resource branch from b1d81a3 to 6221b88 Compare June 24, 2026 12:32
@ethanndickson ethanndickson force-pushed the docs/add-agents-md branch 2 times, most recently from d7a0e3f to 3db900a Compare June 24, 2026 13:21
@ethanndickson ethanndickson force-pushed the ethan/ai-provider-resource branch 3 times, most recently from 839989b to 0d0e3b8 Compare June 24, 2026 14:42
@ethanndickson ethanndickson force-pushed the docs/add-agents-md branch 2 times, most recently from faaed66 to ce7bd51 Compare June 24, 2026 15:08
@ethanndickson ethanndickson force-pushed the ethan/ai-provider-resource branch 2 times, most recently from de42560 to 2e2abe6 Compare June 24, 2026 15:15
@ethanndickson ethanndickson force-pushed the ethan/ai-provider-resource branch from 2e2abe6 to 18615e5 Compare June 25, 2026 04:21
@ethanndickson ethanndickson requested a review from johnstcn June 25, 2026 09:43
Comment thread AGENTS.md Outdated

ethanndickson commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

Merge activity

  • Jul 1, 5:55 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 1, 5:58 AM UTC: Graphite rebased this pull request as part of a merge.
  • Jul 1, 5:58 AM UTC: @ethanndickson merged this pull request with Graphite.

@ethanndickson ethanndickson changed the base branch from ethan/ai-provider-resource to graphite-base/369 July 1, 2026 05:55
@ethanndickson ethanndickson changed the base branch from graphite-base/369 to main July 1, 2026 05:56
@ethanndickson ethanndickson merged commit bb7fd49 into main Jul 1, 2026
4 checks passed
@ethanndickson ethanndickson deleted the docs/add-agents-md branch July 1, 2026 05:58
ethanndickson added a commit that referenced this pull request Jul 1, 2026
> **Stack** — builds on #369 (AGENTS.md contributor guide) and the `coderd_ai_provider` resource in #368; review/merge those first.

Relates to CODAGT-607
Relates to CODAGT-736

## Summary

Adds `coderd_agents_model` for managing Coder Agents admin-managed chat model configurations from Terraform — binding a model identifier to a configured AI provider — with full CRUD + import, generated docs/examples, acceptance/unit tests and an integration test with the provider resource. The resource is marked experimental via a warning callout in its docs.

## Approach

- **`model_config` as normalized JSON.** The open-ended per-call tuning blob (token limits, temperature, cost/pricing, provider options) is a single JSON string backed by a custom type that compares by JSON semantic equality, so whitespace, key ordering, and equivalent number spellings (a `3.00` cost returned as `3`) don't produce perpetual diffs. An empty `jsonencode({})` is rejected at plan time because Coder discards it.
- **Provider-specific controls live under `provider_options`.** Common per-call knobs — reasoning effort, reasoning summary, parallel tool calls, and web search — are nested per provider rather than top-level, since each provider names them differently. Anthropic uses `effort` (and/or `thinking.budget_tokens`), `send_reasoning`, `web_search_enabled`, and `disable_parallel_tool_use`; OpenAI uses `reasoning_effort`, `reasoning_summary`, `parallel_tool_calls`, and `web_search_enabled`. The examples show both.
- **`provider_type` derived from `ai_provider_id`.** A model takes its provider via a required `ai_provider_id`, and Coder derives the runtime `provider_type` from it. A plan modifier keeps the prior value while `ai_provider_id` is unchanged and lets it recompute when the binding changes, so it never shows stale plan noise or trips the post-apply consistency check.
- **Default election stays server-side.** Coder elects exactly one default model itself and mutates the flag out-of-band, which a per-model computed boolean can't represent without perpetual plan noise or post-apply inconsistency — so `is_default` is intentionally omitted from this resource, with a follow-up adding a dedicated `coderd_default_agents_model` pointer resource (see *Why `is_default` is omitted* below).

### Why `model_config` is JSON, not typed attributes

`provider_options` is a tagged union (exactly one of `openai`/`anthropic`/`google`/… with non-obvious aliases like Bedrock→`anthropic`), and the config carries `decimal` pricing and free-form `map[string]any` fields — none of which Terraform's type system can express without re-implementing the server's own validation. A single normalized JSON string sidesteps all of that and lets new upstream tuning fields land with a `codersdk` bump instead of schema churn. This mirrors the closest prior art: AWS's `bedrockagent` `additional_model_request_fields`, which models per-provider model overrides the same way.

### Why `is_default` is omitted

`is_default` is a server-enforced singleton: Coder guarantees exactly one default chat model via a partial-unique index and mutates the flag out-of-band — it demotes the previous default when another model is set default, and auto-promotes a replacement when the current default is deleted. A per-model `Optional + Computed` boolean can't represent that safely. It plans as `(known after apply)` on every non-default model (perpetual plan noise), and pinning the planned value — via `UseStateForUnknown` or a plan-time read of the server — reintroduces "Provider produced inconsistent result after apply": a sibling set to default in the same apply demotes this model *after* the plan is frozen, so any known planned value is wrong. The unknown is load-bearing — it's the only value that stays consistent regardless of how the server resolves it.

Rather than ship a flag that's either noisy or flaky, this PR omits `is_default` entirely. Models still apply normally, and the server continues to elect the default as it always has.

A follow-up PR will add a dedicated `coderd_default_agents_model` pointer resource:

```hcl
resource "coderd_default_agents_model" "this" {
  model_id = coderd_agents_model.sonnet.id
}
```

This is the established pattern for a server-enforced "exactly one of N is the default" (cf. `aws_ec2_default_credit_specification`, `aws_ssm_default_patch_baseline`, `github_branch_default`). Create/Update issue a single-field `is_default` PATCH — the server atomically demotes the previous default — Read reflects the current server default, and Delete is a no-op that stops managing the pointer (the server always keeps some default). Moving the pointer to its own resource removes both failure modes by construction: there's no per-member computed flag to go unknown, and nothing for a sibling to mutate underneath it.

## Schema

```hcl
resource "coderd_ai_provider" "anthropic" {
  type     = "anthropic"
  name     = "anthropic"
  base_url = "https://api.anthropic.com"

  api_key_wo         = var.anthropic_api_key
  api_key_wo_version = 1
}

resource "coderd_agents_model" "sonnet" {
  ai_provider_id = coderd_ai_provider.anthropic.id
  model          = "claude-3-5-sonnet-20241022"
  display_name   = "Claude 3.5 Sonnet"
  enabled        = true
  context_limit  = 200000

  model_config = jsonencode({
    max_output_tokens = 8192
    temperature       = 0.7
    cost = {
      input_price_per_million_tokens  = "3"
      output_price_per_million_tokens = "15"
    }
    # Provider-specific controls. Anthropic uses an effort level (low, medium,
    # high, xhigh, max) and/or an extended-thinking token budget, plus common
    # knobs like reasoning visibility, web search, and parallel tool use.
    provider_options = {
      anthropic = {
        effort                    = "high"
        thinking                  = { budget_tokens = 4096 }
        send_reasoning            = true
        web_search_enabled        = true
        disable_parallel_tool_use = false
      }
    }
  })
}

resource "coderd_agents_model" "gpt" {
  ai_provider_id = coderd_ai_provider.openai.id
  model          = "gpt-5"
  display_name   = "GPT-5"
  context_limit  = 400000

  model_config = jsonencode({
    max_output_tokens = 8192
    # OpenAI uses reasoning_effort (none, minimal, low, medium, high, xhigh),
    # plus common knobs like reasoning summary, parallel tool calls, and web search.
    provider_options = {
      openai = {
        reasoning_effort    = "high"
        reasoning_summary   = "detailed"
        parallel_tool_calls = true
        web_search_enabled  = true
      }
    }
  })
}
```

Computed: `id`, `provider_type` (derived from `ai_provider_id`), `display_name`, `enabled`, `compression_threshold`, `created_at`, and `updated_at`.

## Notes

No write-only arguments, so there's no Terraform 1.11+ requirement — the API key lives on the referenced `coderd_ai_provider`. The chat-model endpoints are experimental (`/api/experimental`); since there's no get-by-id route, Read lists model configs and filters by ID.

Follow-up to #368 (`coderd_ai_provider`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants