diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index e187a50f..a8db3f25 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -1,23 +1,38 @@
name: Deploy Documentation
-# IMPORTANT: this file must exist on the DEFAULT branch, not only on `docs`.
+# The published site is built from `develop`, which is the branch every fix PR
+# lands on. That is deliberate: a documentation-only change reaches the site as
+# soon as its PR is squashed, with no release, tag, or changelog entry needed.
#
+# `main` still receives `docs/` through the normal release merge, so README,
+# CONTRIBUTING, and the in-product links that point at `docs/` on the default
+# branch keep resolving.
+#
+# IMPORTANT: this file must exist on the DEFAULT branch, not only on `develop`.
# GitHub Actions resolves workflows for non-`push` events (such as `release`)
-# from the default branch only. While this file lived exclusively on the
-# orphan `docs` branch, the `release` trigger below was inert and never fired,
-# so the published site kept serving stale release metadata until someone ran
-# the workflow by hand.
+# from the default branch only. While this file lived exclusively on the orphan
+# `docs` branch, the `release` trigger below was inert and never fired, so the
+# published site kept serving stale release metadata until someone ran the
+# workflow by hand.
#
-# Keeping a copy here does NOT cause duplicate runs. For `push` events GitHub
-# uses the workflow file from the pushed commit, so pushing to `docs` still
-# runs the copy on `docs`, and pushing to this branch matches no trigger.
-# Every job below checks out `docs` explicitly, so the site content is always
-# built from that branch regardless of which ref started the run.
+# Keeping a copy on the default branch does NOT cause duplicate runs. For `push`
+# events GitHub uses the workflow file from the pushed commit, and the `push`
+# trigger below only matches `develop`. Every job checks out `develop`
+# explicitly, so the site content is always built from that branch regardless of
+# which ref started the run.
on:
push:
branches:
- - docs
+ - develop
+ # Only rebuild when something that affects the rendered site changes.
+ paths:
+ - 'docs/**'
+ - 'mkdocs.yml'
+ - 'main.py'
+ - 'requirements-docs.txt'
+ - 'overrides/**'
+ - '.github/workflows/deploy-docs.yml'
# Rebuild when a release or pre-release is published, since the home page
# shows release metadata baked at build time.
@@ -30,6 +45,12 @@ on:
# Allow triggering a rebuild manually from the Actions UI.
workflow_dispatch:
+# Let a newer commit supersede an in-flight build instead of racing it to the
+# gh-pages branch.
+concurrency:
+ group: deploy-docs
+ cancel-in-progress: true
+
permissions:
contents: write
@@ -42,7 +63,7 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- ref: docs
+ ref: develop
- name: Set up Python
uses: actions/setup-python@v5
@@ -52,6 +73,9 @@ jobs:
- name: Install MkDocs and dependencies
run: pip install -r requirements-docs.txt
+ - name: Build the site
+ run: mkdocs build --strict
+
- name: Deploy to GitHub Pages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 58e2d7e7..15f40733 100644
--- a/.gitignore
+++ b/.gitignore
@@ -464,3 +464,6 @@ e2e-runs/
tmp-artifacts/
tmp/
+
+# MkDocs local build output
+site/
\ No newline at end of file
diff --git a/README.md b/README.md
index 09d72879..e58028ce 100644
--- a/README.md
+++ b/README.md
@@ -155,7 +155,8 @@ Cockpit sections, in display order:
## Documentation
- [Foundry Prompt Agent tutorial](docs/tutorial-prompt-agent.md) - use this when the Foundry target is `agent: name:version`. Walks the sandbox to dev journey with a PR gate.
-- [Hosted or HTTP Agent tutorial](docs/tutorial-hosted-agent-quickstart.md) - use this when the target is a Foundry hosted or HTTP endpoint URL. Same sandbox to dev journey for endpoint-based agents.
+- [Hosted Agent tutorial](docs/tutorial-hosted-agent.md) - use this when Foundry runs your agent code as a managed hosted runtime behind a stable endpoint.
+- [HTTP Agent tutorial](docs/tutorial-http-agent.md) - use this when the target is an HTTP service you operate behind your own URL.
- [End-to-end tutorial](docs/tutorial-end-to-end.md) - extends either of the above with the full sandbox to dev to qa to prod promotion, Foundry red-team scans, and trace-to-regression promotion.
- [Evaluation paths](docs/evaluation.md) - choose static dataset, grey-box HTTP, or telemetry/trace import.
- [Core concepts](docs/concepts.md)
diff --git a/docs/ci-github-actions.md b/docs/ci-github-actions.md
index bc5921ff..9deef55e 100644
--- a/docs/ci-github-actions.md
+++ b/docs/ci-github-actions.md
@@ -1,3 +1,7 @@
+---
+render_macros: false
+---
+
# AgentOps GenAIOps GitFlow on GitHub Actions
This guide shows how to wire AgentOps into a complete GenAIOps CI/CD
@@ -18,38 +22,55 @@ workflow is available separately when you explicitly generate `--kinds doctor`.
| File | Trigger | GitHub Environment | Purpose |
|---|---|---|---|
-| `agentops-pr.yml` | PRs to `develop`, `release/**`, `main` | `dev` | Eval gate + Doctor gate (default blocks on critical findings; configurable via `--doctor-gate`) + PR comment |
+| `agentops-pr.yml` | PRs to `develop`, `release/**` | `sandbox` for prompt-agent PR candidates, `dev` for generic PR gates | Eval PR candidate + Doctor gate (default blocks on critical findings; configurable via `--doctor-gate`) + PR comment |
| `agentops-deploy-dev.yml` | push to `develop` | `dev` | Eval → build → deploy DEV |
| `agentops-deploy-qa.yml` | push to `release/**` | `qa` | Eval → build → deploy QA |
-| `agentops-deploy-prod.yml` | push to `main` | `production` | Safety eval → evidence → build → deploy PROD |
+| `agentops-deploy-prod.yml` | push to `main` | `production` | Deploy PROD → smoke test |
| `agentops-doctor.yml` | daily cron | `dev` | Optional scheduled Doctor + release evidence |
## GitFlow assumed
```mermaid
flowchart LR
- feat["feature/*"] -->|PR| prGate1{{"agentops-pr.yml (gate)"}}
- prGate1 -->|merge| dev["develop"]
- dev --> deployDev["agentops-deploy-dev.yml"]
- deployDev --> DEV(["DEV"])
-
- rel["release/*"] -->|push| deployQa["agentops-deploy-qa.yml"]
- deployQa --> QA(["QA"])
-
- rel -->|PR| prGate2{{"agentops-pr.yml (gate)"}}
- prGate2 -->|merge| main["main"]
- main --> deployProd["agentops-deploy-prod.yml"]
- deployProd --> PROD(["PROD (required reviewers)"])
-
- classDef gate fill:#fff3cd,stroke:#856404,color:#000;
+ feature["feature/*"] --> prDev["PR eval candidate"]
+ prDev --> sandbox["sandbox"]
+ prDev --> develop["develop"]
+ develop --> devDeploy["Eval + deploy agentops-deploy-dev"]
+ devDeploy --> devEnv["dev"]
+
+ develop --> release["release/*"]
+ release --> qaDeploy["Eval + deploy agentops-deploy-qa"]
+ qaDeploy --> qaEnv["qa"]
+
+ release --> prProd["PR: release to main manual approval"]
+ prProd --> main["main"]
+ main --> prodDeploy["Prod release process deploy + smoke test agentops-deploy-prod"]
+ prodDeploy --> prodEnv["production"]
+
+ classDef branch fill:#e7f0fd,stroke:#1f4e79,color:#000;
+ classDef pipeline fill:#ede7f6,stroke:#4527a0,color:#000;
classDef env fill:#d1ecf1,stroke:#0c5460,color:#000;
- class prGate1,prGate2 gate;
- class DEV,QA,PROD env;
+ class feature,develop,release,main branch;
+ class prDev,devDeploy,qaDeploy,prProd,prodDeploy pipeline;
+ class sandbox,devEnv,qaEnv,prodEnv env;
```
+Legend:
+ Git branch
+ PR or workflow gate
+ deployed environment
+
If you are on trunk-based development, generate only the templates you
need: `agentops workflow generate --kinds pr,dev,prod`.
+The PR gate validates candidates before they enter `develop` or `release/**`. It
+is not a dev deployment. HTTP agent tutorials point that candidate at the
+sandbox endpoint; prompt-agent workflows stage and evaluate the candidate prompt
+version in sandbox. The PR from `release/**` to `main` is a manual approval gate
+with static checks only. It does not call agents. After it merges,
+`agentops-deploy-prod` runs the production release process: deploy and smoke
+test.
+
## Quick start
```bash
@@ -253,7 +274,8 @@ az ad app federated-credential list --id "$APP_ID" \
| Error | Cause | Fix |
|---|---|---|
| `AADSTS700213: No matching federated identity record found for presented assertion subject` | The credential `subject` is not byte-identical to the subject GitHub sent. Usually the immutable-ID prefix above. Also caused by the wrong environment name, or a `ref:refs/heads/...` subject on a job that uses `environment:`. | Copy the subject quoted in the error, compare it against `az ad app federated-credential list`, and add the missing credential. |
-| `AADSTS53003: Access has been blocked by Conditional Access policies` | Usually `AZURE_TENANT_ID` points at a tenant that cannot see the app registration, not an actual CA policy. | Set `AZURE_TENANT_ID` to the tenant that owns the app registration and the federated credential, not a subscription `managedByTenants` entry. |
+| `AADSTS53003: Access has been blocked by Conditional Access policies` | A Conditional Access policy blocked the token. Workload identities are in scope of CA, so a policy that requires MFA, a compliant device, or a named location will block a GitHub-hosted runner. | Open the sign-in in Entra ID > Sign-in logs > Service principal sign-ins, read the Conditional Access tab to find the policy that applied, then exclude the workload identity or scope the policy so it does not target it. |
+| `AADSTS700016: Application with identifier '' was not found in the directory` | `AZURE_CLIENT_ID` or `AZURE_TENANT_ID` is wrong. The app registration exists in a different tenant than the one being authenticated against. | Set `AZURE_TENANT_ID` to the tenant that owns the app registration and the federated credential, not a subscription `managedByTenants` entry. Confirm with `az ad app show --id "$AZURE_CLIENT_ID" --query appId`. |
| `AuthorizationFailed` on `azd provision` | The principal has no role at the scope the ARM deployment targets. | Check the template's target scope, then assign at that scope. See below. |
`azd` templates commonly declare `targetScope = 'subscription'` in
@@ -288,6 +310,45 @@ Resource-group scope is enough only when the template is
from the Foundry roles below, which stay scoped to the Foundry project and the
AI Services account.
+#### `azd` in CI
+
+`azure/login@v3` authenticates the Azure CLI. It does not authenticate `azd`,
+which keeps a separate credential store and never falls back to the `az`
+session. CI jobs that call `azd` need three extra things, all of which the
+generated workflows already do:
+
+1. **Install the `azure.ai.agents` extension explicitly and pin it.** azd
+ refuses to auto-install extensions on CI runners. AgentOps pins
+ `1.0.0-beta.9`, the same version the eval gate uses, and reads an override
+ from `AGENTOPS_AZD_AI_AGENTS_EXTENSION_VERSION`.
+
+ ```bash
+ azd extension install azure.ai.agents --version "1.0.0-beta.9"
+ ```
+
+2. **Log azd in on GitHub Actions**, using the same federated credential as the
+ `az` login:
+
+ ```bash
+ azd auth login \
+ --client-id "$AZURE_CLIENT_ID" \
+ --tenant-id "$AZURE_TENANT_ID" \
+ --federated-credential-provider github
+ ```
+
+3. **On Azure DevOps, reuse the `az` session instead.** The steps run inside an
+ `AzureCLI@2` inline script, so the service connection has already produced an
+ authenticated CLI session that azd can borrow:
+
+ ```bash
+ azd config set auth.useAzCliAuth "true"
+ ```
+
+Skipping step 1 surfaces as `ERROR: no extensions found` or
+`Auto-installation is not supported in CI/CD environments`. Skipping step 2 or 3
+surfaces as an azd authentication failure in a job where `az` commands work
+fine.
+
For Foundry prompt-agent gates, the same app registration / service principal
needs **two** Azure RBAC roles before the first workflow run. Both are required
and the eval step fails silently (every metric returns `null`) if only one is
@@ -330,9 +391,6 @@ In Settings → Environments, create three:
- Override env-specific variables for QA infra.
#### `production`
-- **Required reviewers**: at least one. Deploys to PROD pause until
- approved.
-- Optional: **Wait timer** for an extra cool-down.
- Optional: **Deployment branches**: restrict to `main`.
- Override env-specific variables for production infra.
@@ -359,7 +417,7 @@ prompt.
### 4. Choose deployment mode
AgentOps is azd-first for deployment: AgentOps runs the evaluation gate,
-while Azure Developer CLI manages infrastructure, packaging, deployment, and
+while Azure Developer CLI owns infrastructure, packaging, deployment, and
hooks declared in `azure.yaml`.
Before choosing manually, run:
@@ -529,11 +587,11 @@ agentops workflow analyze --format markdown --out agentops-workflow-plan.md
Use the output as the plan for your coding agent:
-1. AgentOps handles repo-side eval gates, Doctor readiness checks, artifacts, and
+1. AgentOps owns repo-side eval gates, Doctor readiness checks, artifacts, and
Cockpit visibility.
-2. `azd` manages `provision`, `deploy`, and hooks for app/infra lifecycle when
+2. `azd` owns `provision`, `deploy`, and hooks for app/infra lifecycle when
`azure.yaml` is present or can be added.
-3. Foundry manages hosted agents, evaluations, traces, and operations.
+3. Foundry owns hosted agents, evaluations, traces, and operations.
4. Project-specific steps such as indexing data, seeding search, building
containers, updating app config, or running private-network post-provision
work stay in the accelerator's azd hooks or existing deployment tooling.
@@ -583,8 +641,8 @@ contract to gate deploys:
| `2` | Eval ran, one or more thresholds failed | ❌ fail (deploy never runs) |
| `1` | Runtime / config error | ❌ fail |
-For prompt-agent cloud eval, Foundry runs the managed evaluation and
-AgentOps enforces the CI exit code. A threshold failure exits `2`, so the PR/deploy
+For prompt-agent cloud eval, Foundry owns the managed evaluation run and
+AgentOps owns the CI exit code. A threshold failure exits `2`, so the PR/deploy
gate fails with the failing threshold rows in `report.md`.
## Artifacts
diff --git a/docs/concepts.md b/docs/concepts.md
index 39ce4efa..f3d170a5 100644
--- a/docs/concepts.md
+++ b/docs/concepts.md
@@ -27,6 +27,35 @@ regression data.
The short version is: **Foundry operates the agent; AgentOps turns that operating
signal into repo-side release proof.**
+## What AgentOps produces
+
+AgentOps Accelerator turns every readiness run into outputs that people and CI
+can both use.
+
+| Output | Use it for |
+|---|---|
+| `report.md` | PR review: what passed, what failed, and what changed from the baseline. |
+| `results.json` | CI automation: stable metrics, thresholds, exit status, and target details. |
+| `evidence.md` / `evidence.json` | Release approval: the proof package for the ship/no-ship decision. |
+| Cockpit | Local review: latest evals, Doctor findings, evidence, and next actions in one view. |
+
+## Reference architecture
+
+Use this as the mental model for the AgentOps loop: build and learn in a
+sandbox, commit the release contract to source control, promote through
+environments with evidence, then feed production learning back into the next
+evaluation set.
+
+{ .agentops-reference-architecture }
+
+| Area | What it owns |
+|---|---|
+| **Sandbox inner loop** | Create, evaluate, and improve the candidate agent in a safe Foundry project before it is promoted. |
+| **AgentOps Accelerator** | Keep release readiness close to the repo: config, datasets, evaluation gates, Doctor diagnostics, Cockpit views, CI workflows, thresholds, and release evidence. |
+| **Foundry** | Hosts managed agent projects, Prompt Agent and Hosted Agent runtime options, traces, operate views, guardrails, and evaluations where applicable. |
+| **Outer loop delivery** | Move the same reviewed candidate through dev, QA or staging, and production. Production release should be gated by reviewable evidence, not memory or a manual spot check. |
+| **Operate and improve** | Watch telemetry, dashboards, alerts, cost, success rate, compliance, quota, security posture, and data governance. Turn production traces into the next regression cases. |
+
## How an Evaluation Works
```mermaid
@@ -96,10 +125,8 @@ Common `agent:` values:
| `"model:gpt-4o-mini"` | Direct model deployment |
HTTP targets can add top-level mapping fields such as `request_field`,
-`response_fields`, `tool_calls_field`, `auth_header_env`, and `extra_fields`.
-Use `response_fields.response` for the final answer and
-`response_fields.context` for retrieved context. Use `response_source: dataset`
-when each dataset row already contains the response to evaluate.
+`response_field`, `tool_calls_field`, `auth_header_env`, and
+`extra_fields`.
### Dataset
@@ -157,7 +184,7 @@ evidence outputs into a release gate.
| Target | Foundry server-side eval through AgentOps | AgentOps local runner | Recommended default |
|---|---|---|---|
| Foundry Prompt Agent (`name:version`) | Yes, with `execution: cloud` | Yes | Use cloud for official Foundry-hosted runs; use local for fast feedback or fallback. |
-| Foundry Hosted Agent URL | No | Yes | Use local runner; optionally publish local metrics to Foundry with `publish: true`. |
+| Foundry Hosted Agent URL | Yes, with `execution: cloud`, when the URL contains `/agents//versions/` | Yes | Use cloud when the endpoint carries the versioned agent path; otherwise use the local runner and optionally `publish: true`. |
| Generic HTTP/JSON endpoint | No | Yes | Use local runner. |
| Raw model deployment (`model:`) | No | Yes | Use local runner. |
@@ -198,17 +225,12 @@ AgentOps auto-selects common evaluation patterns from the dataset:
| **Agent workflow** | `tool_calls` + `tool_definitions` | Tool-use quality |
| **Content safety** | Safety evaluators | Responsible AI checks |
-Use one of the three hands-on tutorials for scenario coverage:
+Use the hands-on tutorials for scenario coverage:
-- [Evaluation paths](evaluation.md) explains when to use a static dataset,
- grey-box HTTP response mapping, or telemetry/trace import.
-- [Foundry Prompt Agent tutorial](tutorial-prompt-agent-quickstart.md) for Foundry
+- [Prompt agent tutorial](tutorial-prompt-agent.md) for Foundry
prompt agents referenced as `name:version`.
-- [Hosted or HTTP Agent tutorial](tutorial-hosted-agent-quickstart.md) for Foundry
- hosted endpoints, generic HTTP agents, RAG services, and code-based workflows.
-- [End-to-end tutorial](tutorial-end-to-end.md) for the complete Foundry +
- AgentOps loop, including CI/CD, observability, red-team follow-through,
- Doctor, release evidence, and trace regression.
+- [HTTP agent tutorial](tutorial-http-agent.md) for HTTP agents,
+ RAG services, and code-based workflows behind a JSON endpoint.
## Configuration Model
@@ -219,13 +241,9 @@ the fields your target needs:
version: 1
agent: "https://api.example.com/chat"
dataset: .agentops/data/support.jsonl
-response_source: agent
-protocol: http-json
request_field: message
-response_fields:
- response: text
- context: retrieved_context
+response_field: text
thresholds:
coherence: ">=3"
diff --git a/docs/doctor-explained.md b/docs/doctor-explained.md
index d8d7ca66..b375fbf4 100644
--- a/docs/doctor-explained.md
+++ b/docs/doctor-explained.md
@@ -2,8 +2,8 @@
A 10-minute read for a platform, observability, or AI engineer (and
the engineering managers who own those teams) who runs
-`agentops doctor` for the first time. For step-by-step setup, see
-the [end-to-end tutorial](tutorial-end-to-end.md).
+`agentops doctor` for the first time. For step-by-step setup, pick a
+[tutorial](tutorials.md).
## 1. What the Doctor is - and isn't
@@ -369,7 +369,7 @@ For thresholds, source configuration, and check toggles, edit
## 10. The WAF knowledge base (editable CSV)
The Doctor ships with a **packaged baseline** at
-[`src/agentops/agent/knowledge/waf-checklist.csv`](../src/agentops/agent/knowledge/waf-checklist.csv).
+[`src/agentops/agent/knowledge/waf-checklist.csv`](https://github.com/Azure/agentops/blob/main/src/agentops/agent/knowledge/waf-checklist.csv).
It maps every Doctor finding id to a row that names its WAF pillar,
area, and a public Microsoft Learn reference link. The reporter
annotates each finding with a `WAF: / ` line when a
@@ -410,8 +410,8 @@ members and CI.
## 11. Next steps
-- Walk through a full setup with Azure resources:
- [end-to-end tutorial](tutorial-end-to-end.md).
+- Walk through a full setup with Azure resources: pick a
+ [tutorial](tutorials.md).
- Open the workspace command center: `agentops cockpit` shows eval
history, Doctor findings, CI/CD status, telemetry readiness, and
Foundry/Azure navigation.
diff --git a/docs/e2e-live-setup.md b/docs/e2e-live-setup.md
index 8e24cd68..fda0e674 100644
--- a/docs/e2e-live-setup.md
+++ b/docs/e2e-live-setup.md
@@ -1,7 +1,7 @@
# Live Azure E2E - one-time setup
This guide walks through the human-only steps needed to enable the **live**
-jobs in [`.github/workflows/e2e.yml`](../.github/workflows/e2e.yml). Once
+jobs in [`.github/workflows/e2e.yml`](https://github.com/Azure/agentops/blob/main/.github/workflows/e2e.yml). Once
completed, anyone with `Run workflow` permission can dispatch the workflow
and pick which scenario(s) to execute against real Azure resources.
@@ -136,7 +136,7 @@ Save the three values printed at the end - you'll add them as
> use its `sub_claim_prefix` value, not the booleans beside it. Creating both
> subjects as separate credentials on the same app works on either kind of
> account. See
-> [`docs/ci-github-actions.md`](ci-github-actions.md#federated-credential-subject-check-sub_claim_prefix-first)
+> [`ci-github-actions.md`](ci-github-actions.md#federated-credential-subject-check-sub_claim_prefix-first)
> for the full walkthrough.
---
diff --git a/docs/evaluation.md b/docs/evaluation.md
index 44403b8b..ab2bcd83 100644
--- a/docs/evaluation.md
+++ b/docs/evaluation.md
@@ -1,282 +1,498 @@
# Evaluation
-Use this page when you need to choose how AgentOps should evaluate a RAG or
-agent workflow. The goal is simple: pick the path that matches where your
-evidence comes from, run the evaluation, and keep the result in a format that
-reviewers can trust.
+This is the canonical page for how evaluation works in AgentOps. An evaluation
+runs a dataset against a target agent, scores the responses, and gates the
+result against thresholds. Foundry operates the agent at runtime; AgentOps turns
+that run into repo-side release proof.
-AgentOps supports three evaluation paths:
+If you want a hands-on walkthrough instead of a reference, pick a
+[tutorial](tutorials.md) and follow it end to end.
-1. **Static dataset**: use a JSONL file that already contains the prompt,
- expected answer, and optional retrieval context.
-2. **Grey-box HTTP**: call an HTTP endpoint and extract both the answer and
- retrieval context from the live response.
-3. **Telemetry/trace import**: import production traces into a reviewable
- dataset so real traffic can become future regression coverage.
+## What an evaluation is
-## Choose a path
+An evaluation is defined by one flat file, `agentops.yaml`. It connects three
+things: the **agent** (the target to evaluate), the **dataset** (the rows to
+send), and the **thresholds** (the quality gates that decide pass or fail).
-| Path | Use it when | Best first step |
-|---|---|---|
-| Static dataset | You already know the test cases, expected answers, and optionally the target responses. | Create or edit `.agentops/data/*.jsonl`. |
-| Grey-box HTTP | Your endpoint can return the answer plus retrieval details for the same request. | Configure `request_field` and `response_fields`. |
-| Telemetry/trace import | You want to learn from production traffic before adding new regression rows. | Configure `telemetry_imports`, then run `agentops telemetry preview`. |
-
-The paths build on each other. Most teams start with a static dataset, add
-grey-box HTTP when they need retrieval telemetry, then use telemetry import after
-the agent is running in production.
+The minimum config is three lines:
-```mermaid
-flowchart LR
- Static[Static dataset] --> HTTP[Grey-box HTTP]
- HTTP --> Traces[Telemetry import]
- Traces --> Static
+```yaml
+version: 1
+agent: "travel-agent:1"
+dataset: .agentops/data/smoke.jsonl
```
-## Static dataset
+The AgentOps runner reads that config, sends each dataset row to the target,
+collects responses, scores them with evaluators, and checks the scores against
+your thresholds. It writes two outputs every run: `results.json` for automation
+and `report.md` for human review.
-Choose this path when the data you need is already in the dataset file. Each row
-is a test case. AgentOps sends `input` to the target, compares the target
-response with `expected`, and uses `context` when present to select RAG
-evaluators.
+## Where evaluations run
-By default, `response_source: agent` means AgentOps calls the configured target.
-Use `response_source: dataset` only when the dataset already includes the answer
-you want to evaluate in a `response`, `prediction`, `output`, or `answer` field.
-That is useful for offline review or imported trace rows that should not call a
-live endpoint again.
+By default, `agentops eval run` is a local runner. It runs wherever you execute
+the command: your laptop, a dev container, GitHub Actions, or another CI host.
+The output is written to that workspace under `.agentops/results/latest/`.
-Minimal RAG row:
+Foundry visibility is opt-in:
-```json
-{"id":"refund-001","input":"What is the refund window?","expected":"Customers can request a refund within 30 days.","context":"Refunds are available for 30 days after purchase."}
+| Config | What happens | Foundry surface |
+|---|---|---|
+| `execution: local` or omitted | AgentOps invokes the target and scores rows locally. | Local `results.json` and `report.md` only. |
+| `execution: local` plus `publish: true` | AgentOps keeps the local run as source of truth, then uploads metrics and row results. | Classic Foundry Evaluations. |
+| `execution: cloud` | Foundry runs the agent and evaluators server-side. | New Foundry Evaluations. |
+
+`execution: cloud` needs a target Foundry can resolve on its own: a prompt agent
+declared as `name:version`, or a hosted agent endpoint whose URL contains
+`/agents//versions/`. AgentOps parses the name and version out of
+that URL and builds the server-side target from it, so you do not have to
+duplicate the reference. Only the name and version are used. The run is
+submitted against the project in `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT`, so that
+endpoint must point at the project that holds the agent version. A hosted
+endpoint without the versioned path is rejected with a message telling you to
+add it or set `agent: :`.
+
+Generic HTTP endpoints and raw model deployments always use the local runner; to
+make those results visible in Foundry, use `publish: true`, which targets the
+Classic Foundry Evaluations upload path.
+
+If you configure Application Insights, AgentOps also emits telemetry spans so
+the run can be inspected through Foundry tracing or Azure Monitor Logs. That is
+separate from the Evaluations page.
+
+!!! info "Exit codes are the CI contract"
+ The runner returns `0` when every threshold passes, `2` when the run
+ succeeded but one or more thresholds failed, and `1` for a runtime or
+ configuration error. These three codes are the public gate contract. CI
+ treats `2` as a hard fail so a deploy never runs on a regression.
+
+```mermaid
+graph TD
+ A[agentops.yaml target dataset thresholds]
+ B[JSONL dataset rows]
+ C[AgentOps runner]
+ D[Foundry target]
+ E[HTTP target]
+ F[Model target]
+ G[Evaluators and thresholds]
+ H[results.json]
+ I[report.md]
+
+ A --> C
+ B --> C
+ C --> D
+ C --> E
+ C --> F
+ D --> G
+ E --> G
+ F --> G
+ G --> H
+ G --> I
```
-Minimal config:
+## Target kinds
+
+AgentOps resolves the `agent:` value into one of four target kinds by its shape.
+You do not choose a backend by hand; the shape of `agent:` selects both the kind
+and the fields that make sense for it.
+
+| `agent:` value | Target kind | Use case |
+|---|---|---|
+| `"travel-agent:1"` (`name:version`) | Foundry prompt agent | Foundry Agent Service agents |
+| `"https://...services.ai.azure.com/.../agents/"` | Foundry hosted agent | A deployed agent endpoint on a Foundry domain |
+| `"https://api.example.com/chat"` | HTTP/JSON endpoint | LangGraph, Agent Framework, ACA, AKS, custom REST |
+| `"model:gpt-4o-mini"` | Model-direct | Raw model deployment checks |
+
+!!! note "HTTP targets need request and response mapping"
+ A custom HTTP endpoint rarely matches AgentOps defaults exactly, so you map
+ its request and response shape with top-level fields. Use `request_field`
+ and `response_field` (dot-paths) to point at the right JSON keys,
+ `tool_calls_field` for tool output, `auth_header_env` to name an env var
+ holding a Bearer token, and `extra_fields` for any static body fields.
```yaml
version: 1
-agent: "support-agent:3"
-dataset: .agentops/data/rag-smoke.jsonl
-response_source: agent
-
-thresholds:
- groundedness: ">=3"
- retrieval: ">=3"
- response_completeness: ">=3"
+agent: https://my-aca-app.eastus2.azurecontainerapps.io/chat
+dataset: .agentops/data/qa.jsonl
+request_field: message # default is "message"
+response_field: text # dot-path; default is "text"
+auth_header_env: APP_API_TOKEN # value is sent as a Bearer token
```
-Run it:
+## Configure an HTTP target
+
+For HTTP agents, fill `agentops.yaml` from the shape of the request and response.
+Start with the defaults, then add only the fields your endpoint needs.
-```powershell
-agentops eval analyze
-agentops eval run
+```yaml
+version: 1
+agent: https://api.example.com/chat
+dataset: .agentops/data/qa.jsonl
+protocol: http-json
+request_field: message
+response_field: text
```
-Use this path for:
+| If the endpoint response is... | Use this config |
+|---|---|
+| JSON, for example `{"text": "answer"}` | `response_mode: json` or omit it. Set `response_field: text` if needed. |
+| Plain text, returned all at once | `response_mode: text`. Do not add `stream:`. |
+| Plain text, streamed in chunks | `response_mode: text`. Do not add `stream:` unless the first chunk is not part of the answer. |
+| Plain text stream with a leading id or token | `response_mode: text` plus `stream.strip_leading_token: true`. |
+| Server-Sent Events with `data:` lines | `response_mode: sse`. |
+| Server-Sent Events where each `data:` line is JSON | `response_mode: sse` plus `stream.text_field`, for example `stream.text_field: choices.0.delta.content`. |
+| Server-Sent Events with a final marker | `response_mode: sse` plus `stream.done_marker`, for example `stream.done_marker: "[DONE]"`. |
+
+Examples:
+
+```yaml
+# JSON response: {"answer": "..."}
+response_mode: json
+response_field: answer
+```
-- Fast local checks before opening a PR.
-- CI gates with stable examples.
-- Baseline comparison with `agentops eval run --baseline`.
-- Manual review of newly written or newly labeled examples.
+```yaml
+# Plain text response, streamed or not.
+response_mode: text
+```
-## Grey-box HTTP
+```yaml
+# GPT-RAG orchestrator: text stream where the first token is a conversation id.
+response_mode: text
+stream:
+ strip_leading_token: true
+```
-Choose this path when the endpoint can return more than final text. This is the
-best path for RAG services because the evaluator can see what the agent actually
-retrieved for the request.
+```yaml
+# SSE response with JSON data frames.
+response_mode: sse
+stream:
+ text_field: choices.0.delta.content
+ done_marker: "[DONE]"
+```
-The endpoint response should include:
+### Grey-box: score the live retrieved context
-- the final answer;
-- retrieval context, citations, or document chunks;
-- optional tool calls or workflow metadata.
+`response_field` extracts the final answer. When the endpoint also returns the
+chunks it retrieved, capture them with `response_fields` (plural) so RAG
+evaluators can score what the agent actually grounded on for that request. Each
+entry maps a name to a dot-path into the JSON body, and the captured value
+becomes available to evaluator `input_mapping` as `$response.`.
-Example endpoint response:
+Given an endpoint that answers like this:
```json
{
"answer": "Customers can request a refund within 30 days.",
- "context": [
- "Refunds are available for 30 days after purchase.",
- "Refunds require the original order number."
- ],
+ "context": ["Refunds are available for 30 days after purchase."],
"citations": ["refund-policy.md"]
}
```
-Example config:
+Capture the extra fields and point the evaluators at them:
```yaml
version: 1
-agent: "https://support-dev.example.com/chat"
+agent: https://support-dev.example.com/chat
dataset: .agentops/data/rag-smoke.jsonl
-
protocol: http-json
request_field: message
+response_field: answer
+
response_fields:
- response: answer
context: context
citations: citations
-thresholds:
- groundedness: ">=3"
- retrieval: ">=3"
- relevance: ">=3"
+evaluators:
+ - name: GroundednessEvaluator
+ input_mapping:
+ query: $prompt
+ response: $prediction
+ context: $response.context
+ - name: RetrievalEvaluator
+ input_mapping:
+ query: $prompt
+ context: $response.context
```
-What happens:
+`response_fields` only applies when `response_mode` is `json`. The primary
+answer still comes from `response_field`. `input_mapping` is merged onto the
+preset defaults, so list only the keys you want to change.
-1. AgentOps reads each row from the dataset.
-2. It sends `row.input` as the HTTP request field named by `request_field`.
-3. It extracts the final answer from `response_fields.response`.
-4. It extracts retrieval context from `response_fields.context`.
-5. RAG evaluators can use the extracted context through `$response.context`,
- `$retrieved_context`, or `$retrieved_context_items`.
+This is the path to use when a groundedness or retrieval score moves and you
+need to see whether the agent retrieved the wrong chunks or reasoned badly over
+the right ones.
-Use dot paths when fields are nested:
+## Datasets and scenarios
-```yaml
-response_fields:
- response: output.text
- context: output.retrieval.chunks
-```
+A dataset is a plain JSONL file, one evaluation row per line. Each row has an
+`input` prompt and usually an `expected` reference answer. Optional fields drive
+which evaluators run.
-Use this path for:
+```json
+{"id": "1", "input": "What is the refund policy?", "expected": "Refunds within 30 days.", "context": "Our policy: refunds are available within 30 days."}
+```
-- RAG services where the retrieved chunks matter.
-- Debugging why a groundedness or retrieval score changed.
-- Endpoint-based agents hosted in Azure Container Apps, AKS, Foundry Hosted
- Agents, or another HTTP host.
+The presence of optional fields tells AgentOps which evaluation scenario you are
+running. You do not declare the scenario; the row shape implies it.
-## Telemetry import
+| Scenario | Signal in the row | Purpose |
+|---|---|---|
+| Model quality | `model:` target plus `expected` | Direct model checks |
+| RAG | `context` | Grounding and retrieval checks |
+| Conversational | `input` plus `expected` | Chatbot and Q&A quality |
+| Agent workflow | `tool_calls` plus `tool_definitions` | Tool-use quality |
+| Content safety | Safety evaluators | Responsible AI checks |
+
+## Evaluators
+
+An evaluator is a scoring function that measures one aspect of a response. They
+come in two flavors. **AI-assisted** evaluators use a judge model to score
+qualities like coherence, similarity, or groundedness. **Local metrics** are
+computed without a judge, such as `avg_latency_seconds` or `F1ScoreEvaluator`
+for exact-reference checks.
+
+AgentOps auto-selects evaluators from the target kind and the dataset shape, so a
+three-line config still scores the right things. Prompt and hosted agents get
+answer-quality judges, `context` rows add the RAG set, and tool rows add the
+tool-use set.
+
+Run `agentops eval init` after you create the dataset to see the recommendation.
+For HTTP, model, and other local targets, this is recommendation-only: AgentOps
+does not call `azd` or create `eval.yaml`. For Foundry prompt agents, the same
+command can also delegate to `azd ai agent eval init` to create Foundry-native
+eval assets.
+
+!!! note "Override only when you must"
+ Set the `evaluators:` list in `agentops.yaml` only when you need to replace
+ the auto-selection. It is an escape hatch, not the normal path. For the full
+ catalog of evaluator names and their required inputs, see
+ [Built-in Evaluators](foundry-evaluation-sdk-built-in-evaluators.md).
+
+## Where the run executes
+
+The `execution:` field decides where the evaluation actually runs. Local is the
+default and works for every target. Cloud runs a Foundry agent server-side. The
+azd recipe path delegates to an existing `azd ai agent eval` flow.
+
+| Target | Cloud (`execution: cloud`) | Local runner | Recommended default |
+|---|---|---|---|
+| Foundry prompt agent (`name:version`) | Yes | Yes | Cloud for official Foundry runs; local for fast feedback |
+| Foundry hosted agent URL | Yes, when the URL contains `/agents//versions/` | Yes | Cloud when the endpoint carries the versioned path; otherwise local, optionally `publish: true` |
+| Generic HTTP/JSON endpoint | No | Yes | Local runner; optionally `publish: true` |
+| Raw model deployment (`model:`) | No | Yes | Local runner |
+
+For prompt-agent CI pipelines that need a merge or deploy gate, prefer cloud
+eval. Foundry executes the managed evaluation and AgentOps enforces thresholds,
+baselines, Doctor readiness, and release evidence.
+
+!!! info "Reusing an azd eval recipe"
+ If a Foundry project already uses the public-preview `azd ai agent eval`
+ recipe, set `execution: azd` and `eval_recipe: eval.yaml`. AgentOps
+ delegates execution to azd, normalizes the metrics, binds thresholds, writes
+ `results.json`, and fails closed for any threshold that has no emitted
+ metric. Rubric evaluator dimensions are treated as first-class metric names.
-Choose this path when production traffic has useful examples that are not yet in
-your test set. Telemetry import does not make production responses automatically
-correct. It creates reviewable dataset candidates.
+## Input mapping
-Configure a named telemetry import in `agentops.yaml`:
+Every evaluator receives a fixed set of named inputs. `input_mapping` decides
+which part of the dataset row or the target response feeds each input. AgentOps
+provides a preset per evaluator, so you only list the keys you want to override.
+
+| Token | Resolves to |
+|---|---|
+| `$prompt` or `$row.input` | The `input` column of the dataset row |
+| `$expected` or `$row.expected` | The `expected` column of the dataset row |
+| `$prediction` or `$response.response` | The primary answer, read via `response_field` |
+| `$response.` | An extra field captured by the target's `response_fields` |
+| `$retrieved_context` or `$response.context` | Live retrieved chunks returned by the same call |
+| `$retrieved_context_items` | The same chunks as a list, for evaluators that expect items |
+| `$context` or `$row.context` | Static context stored in the dataset row |
+| `$telemetry.trace_id` | The trace ID of the invocation, when telemetry is available |
+
+Use `$row.context` when the ground truth is fixed and lives in the dataset. Use
+`$response.context` when you want to score what the agent retrieved at request
+time. Mixing them silently is the most common reason a groundedness score looks
+fine while retrieval is broken.
+
+## Import production traces into a dataset
+
+Real traffic is the best source of eval cases, because it contains the questions
+users actually ask. `telemetry_imports` declares a named import that reads
+Azure Monitor telemetry and writes an AgentOps JSONL dataset. AgentOps generates
+the KQL, so you never pass raw query text.
```yaml
+version: 1
+agent: support-agent:3
+dataset: .agentops/data/prod-candidates.jsonl
+
telemetry_imports:
- - name: prod-rag
+ - name: prod-candidates
+ source: azure-monitor
target: application-insights
- resource_id: $APPINSIGHTS_RESOURCE_ID
+ resource_id: /subscriptions//resourceGroups//providers/microsoft.insights/components/
time_range:
lookback_days: 7
filters:
- customDimensions.agent: support-agent
+ agent: support-agent
fields:
- input: customDimensions.question
- response: customDimensions.answer
- context: customDimensions.retrieved_context
- trace_id: operation_Id
+ input: customDimensions.prompt
+ response: customDimensions.completion
+ privacy:
+ redact_fields: [authorization, api_key, token, password, secret]
+ max_field_length: 4000
output:
- path: .agentops/data/prod-rag-candidates.jsonl
+ path: .agentops/data/prod-candidates.jsonl
label_mode: pending
+ max_rows: 200
```
-Validate the import without querying Azure:
+Point `target` at `application-insights` (needs `resource_id`,
+`application_id`, or `connection_string`) or `log-analytics` (needs
+`workspace_id`). Use either `lookback_days` (1 to 90) or an explicit
+`from`/`to` pair, never both.
-```powershell
-agentops telemetry validate prod-rag
-```
+`fields` overrides the auto-detection for a column. AgentOps already probes the
+common shapes (`input`, `prompt`, `customDimensions.prompt`, and so on), so set
+it only when your telemetry uses a name AgentOps cannot infer.
-Preview rows from Azure Monitor:
+Then work the import in three steps, so nothing lands in your dataset unseen:
-```powershell
-agentops telemetry preview prod-rag --rows 10
+```bash
+agentops telemetry validate prod-candidates # check config and connectivity
+agentops telemetry preview prod-candidates # show the generated KQL and sample rows
+agentops telemetry import prod-candidates --apply # write the JSONL dataset and manifest
```
-Write the candidate dataset and manifest:
+`agentops telemetry import` is a dry run without `--apply`. It prints what it
+would write and touches nothing, which makes it safe to run on a shared machine.
-```powershell
-agentops telemetry import prod-rag --apply
-```
+### Choose a label mode
-Label modes:
+`output.label_mode` decides what goes into the `expected` column, and it changes
+what the resulting dataset can be used for.
-| Mode | What it writes | Use it when |
+| Mode | `expected` is set to | Use it for |
|---|---|---|
-| `pending` | Empty `expected` values with review metadata. | A human must write the correct answer before the row can gate a release. |
-| `self-similarity` | The production response becomes `expected`. | You want drift detection against known production behavior. |
+| `self-similarity` (default) | The production response | Drift detection: catch when new behavior diverges from known production behavior |
+| `pending` | Empty, every row flagged for review | Building human-verified ground truth before gating a release |
+
+`self-similarity` is not human-verified ground truth. It answers "did the answer
+change?", not "was the answer correct?". If production was already wrong, the
+eval will happily certify the wrong answer. Use `pending` and fill the rows in
+before you make the dataset a blocking gate.
+
+Every imported row carries a `telemetry` block (trace ID, turn ID, timestamp,
+source, target, and the import name) so you can jump from a failing eval row
+back to the original production trace in Foundry or App Insights.
+
+### Safety notes
+
+- Do not treat production output as ground truth without review.
+- Do not import payloads that contain personal or regulated data. `privacy.redact_fields`
+ redacts on field-name fragments only, so it will not catch secrets embedded in
+ free text. Read the `preview` output before you run with `--apply`.
+- `privacy.include_raw` is `false` by default. Leave it off unless you have a
+ specific reason, because it writes the untouched telemetry record.
+- Keep credentials in environment variables. `agentops.yaml` is committed.
+- `max_rows` caps the import (1 to 5000). Start small and inspect the result.
+- Imported rows carry `metadata.needs_review: true`. Clear that flag deliberately, not in bulk.
+
+## Mini-glossary
+The tutorials defer to these definitions, so they live here once.
+
+!!! note "Dimension"
+ A dimension is a single named axis a rubric or evaluator scores. A Travel
+ Agent rubric might score the dimensions `helpfulness`, `safety`, and
+ `format_adherence` separately, so one response produces one score per
+ dimension rather than a single blended number.
+
+!!! note "Rubric"
+ A rubric is an evaluator that scores responses against a written scoring
+ guide, usually one score per dimension. For example, a rubric can define
+ `helpfulness: 1 to 5` with a short description of what a 1 and a 5 look like,
+ and the judge model applies that guide to each row. Rubric dimensions become
+ metric names you can put thresholds on.
+
+!!! note "smoke-core"
+ A smoke-core is a small, fast smoke dataset plus the minimal evaluator set
+ that gates it. It is the quick check you run on every change to catch obvious
+ breakage in seconds, before the larger scenario datasets run. Think of it as
+ the few rows and one or two evaluators that must always pass.
+
+## Configuration model
+
+`agentops.yaml` is the single source of truth. Keep it small and add only the
+fields your target needs. For the complete schema, every top-level field, and
+more examples, see [Built-in Evaluators](foundry-evaluation-sdk-built-in-evaluators.md)
+for evaluator config and the tutorials for end-to-end setups.
+
+```yaml
+version: 1
+agent: "https://api.example.com/chat"
+dataset: .agentops/data/support.jsonl
-Telemetry import keeps lineage metadata such as trace ID, timestamp, replay URL,
-and source system when those values exist in the export. If the trace includes
-retrieval context, AgentOps writes it as `context` so RAG evaluators can use it
-later. Evaluator mappings can also use `$telemetry.trace_id` when a trace ID is
-needed for reporting or troubleshooting.
+request_field: message
+response_field: text
-If you already have a local trace export file, `agentops eval promote-traces`
-still works. Use `agentops telemetry` when the source is Azure Monitor or
-Application Insights.
+thresholds:
+ coherence: ">=3"
+ avg_latency_seconds: "<=2"
+```
-Use this path for:
+## Try it
-- Turning incidents or surprising production answers into regression tests.
-- Sampling real traffic for future review.
-- Building a trace-to-dataset flywheel without skipping human judgment.
+Run these five commands in order to go from an empty repo to a gated result.
-## Input mapping
+1. Bootstrap the workspace and a starter `agentops.yaml` with the init wizard.
-Evaluator inputs come from three places:
+ ```bash
+ agentops init
+ ```
-| Source | Placeholder | Example |
-|---|---|---|
-| Dataset prompt | `$row.input` or `$prompt` | User question sent to the agent. |
-| Dataset expected answer | `$row.expected` or `$expected` | Ground truth or acceptance criteria. |
-| Agent response | `$response.response` or `$prediction` | Final answer returned by the target. |
-| Any response field | `$response.` | Any field extracted through `response_fields`. |
-| Extracted retrieval context | `$response.context`, `$retrieved_context`, or `$retrieved_context_items` | Chunks, citations, or grounding text from the live response. |
-| Dataset retrieval context | `$row.context` | Static context stored in JSONL. |
-| Trace ID | `$telemetry.trace_id` | Azure Monitor or Application Insights operation ID. |
+2. Inspect the repo and get an evaluator recommendation for your target and dataset.
-For beginners, the easiest rule is:
+ ```bash
+ agentops eval analyze
+ ```
-- Put known test data in the dataset.
-- Put live endpoint outputs under `response_fields`.
-- Let AgentOps map the common fields to evaluators.
+3. Write the recommended eval assets once the plan looks right.
-Only customize evaluator selection when the automatic choice is not enough:
+ ```bash
+ agentops eval init
+ ```
-```yaml
-evaluators:
- - GroundednessEvaluator
- - RetrievalEvaluator
- - RelevanceEvaluator
+4. Send the dataset to the target, score the responses, and gate them against thresholds.
+
+ ```bash
+ agentops eval run
+ ```
+
+5. Regenerate the human-readable report from the latest results.
+
+ ```bash
+ agentops report generate
+ ```
+
+## Run from your coding agent
+
+Install the AgentOps skills so your coding agent can run these steps for you.
+
+```bash
+agentops skills install --platform copilot
```
-## Safety notes
-
-- Do not treat production responses as ground truth without review.
-- Do not import sensitive trace payloads into a repository dataset.
-- Keep secrets in environment variables or `.agentops/.env`, not in JSONL files.
-- Prefer `--label-mode pending` when correctness matters.
-- Use `self-similarity` only for drift detection.
-- Keep trace replay links in metadata so reviewers can investigate the original
- runtime behavior.
-
-## View Foundry trace-evaluation results in the workbook
-
-Deploy or open the Azure Monitor workbook with
-`agentops telemetry dashboard deploy` and `agentops telemetry dashboard open`,
-then select **Agent behavior**. The tab reads Microsoft Foundry-owned
-compatible `gen_ai.evaluation.result` events and observed `invoke_agent` spans
-from the selected Log Analytics workspace. Official Foundry documentation
-verifies this event for
-[human trace annotations](https://learn.microsoft.com/azure/foundry/observability/how-to/trace-annotations#log-end-user-feedback-as-trace-annotations);
-automated trace-evaluation export through the same schema is
-validation-dependent and must be proven in the target workspace. The tab shows
-data status and freshness first,
-then separate invocation, evaluated-trace, and evaluation-event counts,
-per-evaluator pass-rate / volume trends, raw scores grouped by evaluator, and
-recent trace IDs for investigation in Foundry Tracing.
-
-Foundry trace evaluation is a preview, platform-owned feature. The workbook is
-read-only: it does not schedule evaluations, change rules, add release gates, or
-replace the Foundry trace view. Filters work when environment, agent, version,
-and evaluator properties are present; missing versions are shown as **Version
-not reported**. Trace-ID evaluation and correlation do not require an emitted
-`gen_ai.agent.id`. For supported table shapes, verified producers, state
-meanings, schema assumptions, and trace-correlation instructions, see the packaged
-[workbook authoring guide](../src/agentops/templates/workbooks/README.md#agent-behavior-tab).
+The skills that map to evaluation are:
+
+| Skill | What it helps with |
+|---|---|
+| `agentops-config` | Generate and edit `agentops.yaml`. |
+| `agentops-dataset` | Create JSONL datasets and pick the right scenario. |
+| `agentops-eval` | Run evaluations, benchmark, and compare runs. |
+| `agentops-report` | Interpret results and regenerate the report. |
+
+## Next
+
+Continue with the [Built-in Evaluators](foundry-evaluation-sdk-built-in-evaluators.md)
+catalog, wire the gate into CI on the [Ship](ship.md) page, or pick a
+[tutorial](tutorials.md) and follow it end to end.
diff --git a/docs/foundry-ops-workbook-kql.md b/docs/foundry-ops-workbook-kql.md
new file mode 100644
index 00000000..3b7c9212
--- /dev/null
+++ b/docs/foundry-ops-workbook-kql.md
@@ -0,0 +1,175 @@
+# Foundry operations workbook: KQL library
+
+This page lists the Kusto queries behind the [Foundry operations
+workbook](foundry-ops-workbook.md). Each query maps to one derived metric in the
+workbook, so you can run it directly in Log Analytics, adapt it for an alert, or
+paste it into your own dashboard.
+
+!!! note "Column names depend on the diagnostic mode"
+ These queries target `AzureDiagnostics`, which is the Azure diagnostics
+ collection mode. If your resource uses resource-specific tables instead, the
+ table and column names differ, so adjust the `where Category` filters and the
+ parsed field names to match your workspace schema. Both diagnostic categories
+ from the [prerequisites](foundry-ops-workbook.md#prerequisites) must be
+ enabled for every query below to return rows.
+
+## Request volume over time
+
+Counts requests per five-minute bin so you can see traffic peaks and drops.
+
+Columns returned: `TimeGenerated`, `Requests`.
+
+```kusto
+AzureDiagnostics
+| where Category == "RequestResponse"
+| where ResourceProvider == "MICROSOFT.COGNITIVESERVICES"
+| summarize Requests = count() by bin(TimeGenerated, 5m)
+| order by TimeGenerated asc
+```
+
+## Success rate
+
+Reports the share of requests that returned a `2xx` status in each bin.
+
+Columns returned: `TimeGenerated`, `SuccessRate`.
+
+```kusto
+AzureDiagnostics
+| where Category == "RequestResponse"
+| extend Status = toint(ResultSignature)
+| summarize Total = count(), Success = countif(Status between (200 .. 299))
+ by bin(TimeGenerated, 15m)
+| extend SuccessRate = round(100.0 * Success / Total, 2)
+| project TimeGenerated, SuccessRate
+| order by TimeGenerated asc
+```
+
+## Error rate by status code
+
+Breaks failures down by HTTP status code so you can tell a client error from a
+server error.
+
+Columns returned: `Status`, `Errors`.
+
+```kusto
+AzureDiagnostics
+| where Category == "RequestResponse"
+| extend Status = toint(ResultSignature)
+| where Status >= 400
+| summarize Errors = count() by Status
+| order by Errors desc
+```
+
+## Throttled requests
+
+Tracks `429` responses over time, the first sign that you are hitting a rate or
+quota limit.
+
+Columns returned: `TimeGenerated`, `Throttled`.
+
+```kusto
+AzureDiagnostics
+| where Category == "RequestResponse"
+| where toint(ResultSignature) == 429
+| summarize Throttled = count() by bin(TimeGenerated, 5m)
+| order by TimeGenerated asc
+```
+
+## Latency percentiles
+
+Computes p50, p95, and p99 request latency in milliseconds per bin.
+
+Columns returned: `TimeGenerated`, `p50`, `p95`, `p99`.
+
+```kusto
+AzureDiagnostics
+| where Category == "RequestResponse"
+| where isnotnull(DurationMs)
+| summarize
+ p50 = percentile(DurationMs, 50),
+ p95 = percentile(DurationMs, 95),
+ p99 = percentile(DurationMs, 99)
+ by bin(TimeGenerated, 15m)
+| order by TimeGenerated asc
+```
+
+## Token consumption
+
+Sums prompt, completion, and total tokens from the usage logs per bin.
+
+Columns returned: `TimeGenerated`, `PromptTokens`, `CompletionTokens`, `TotalTokens`.
+
+```kusto
+AzureDiagnostics
+| where Category == "AzureOpenAIRequestUsage"
+| extend props = parse_json(properties_s)
+| extend
+ PromptTokens = tolong(props.promptTokens),
+ CompletionTokens = tolong(props.completionTokens),
+ TotalTokens = tolong(props.totalTokens)
+| summarize
+ PromptTokens = sum(PromptTokens),
+ CompletionTokens = sum(CompletionTokens),
+ TotalTokens = sum(TotalTokens)
+ by bin(TimeGenerated, 15m)
+| order by TimeGenerated asc
+```
+
+## Tokens per minute
+
+Turns total token usage into a tokens-per-minute rate for capacity planning.
+
+Columns returned: `TimeGenerated`, `TokensPerMinute`.
+
+```kusto
+AzureDiagnostics
+| where Category == "AzureOpenAIRequestUsage"
+| extend props = parse_json(properties_s)
+| extend TotalTokens = tolong(props.totalTokens)
+| summarize TokensPerMinute = sum(TotalTokens) by bin(TimeGenerated, 1m)
+| order by TimeGenerated asc
+```
+
+## Normalized provisioned throughput usage
+
+Derives `PTU_Normalizado` by dividing tokens per minute by the provisioned
+throughput of the deployment, so utilization is comparable across deployments.
+
+Columns returned: `TimeGenerated`, `DeploymentName`, `PTU_Normalizado`.
+
+```kusto
+// Set this to the provisioned throughput units of the deployment you measure.
+let provisioned_ptu = 100.0;
+AzureDiagnostics
+| where Category == "AzureOpenAIRequestUsage"
+| extend props = parse_json(properties_s)
+| extend
+ DeploymentName = tostring(props.modelDeploymentName),
+ TotalTokens = tolong(props.totalTokens)
+| summarize TokensPerMinute = sum(TotalTokens) by bin(TimeGenerated, 1m), DeploymentName
+| extend PTU_Normalizado = round(TokensPerMinute / provisioned_ptu, 3)
+| project TimeGenerated, DeploymentName, PTU_Normalizado
+| order by TimeGenerated asc
+```
+
+## Top deployments by usage
+
+Ranks deployments by total tokens so you can see which one drives consumption.
+
+Columns returned: `DeploymentName`, `TotalTokens`.
+
+```kusto
+AzureDiagnostics
+| where Category == "AzureOpenAIRequestUsage"
+| extend props = parse_json(properties_s)
+| extend
+ DeploymentName = tostring(props.modelDeploymentName),
+ TotalTokens = tolong(props.totalTokens)
+| summarize TotalTokens = sum(TotalTokens) by DeploymentName
+| order by TotalTokens desc
+```
+
+## Next
+
+Return to the [Foundry operations workbook](foundry-ops-workbook.md) overview, or
+see the readiness loop these signals feed on the [Operate](operate.md) page.
diff --git a/docs/foundry-ops-workbook.md b/docs/foundry-ops-workbook.md
new file mode 100644
index 00000000..21412048
--- /dev/null
+++ b/docs/foundry-ops-workbook.md
@@ -0,0 +1,143 @@
+# Foundry operations workbook
+
+The Foundry operations workbook is an Azure Monitor workbook that AgentOps
+deploys into your Log Analytics workspace. It turns the raw Azure OpenAI
+diagnostic logs behind a Foundry project into operational charts for traffic,
+latency, token consumption, and throttling. You deploy it once with a single
+command and then open it in the Azure portal alongside the rest of your
+monitoring.
+
+!!! note "What you need installed"
+ These commands ship in every published `agentops-accelerator` release, so
+ there is nothing extra to install for the CLI itself. `deploy` is the one
+ that shells out to the Azure CLI, so `az` must be on `PATH` and logged in
+ (`az login`). Without it, `deploy` stops with *The Azure CLI ('az') was not
+ found on PATH*. Use `deploy --dry-run` to emit the ARM template instead.
+ `open` and `export` do not need the Azure CLI.
+
+## When to use it
+
+The workbook, the Cockpit, and Foundry each answer a different question, so pick
+the surface that matches what you need.
+
+| Surface | Best for | Scope |
+|---|---|---|
+| Foundry operations workbook | Usage, cost, token, and throttling trends over days and weeks. | Azure OpenAI platform metrics across every caller of the resource. |
+| [Cockpit](operate.md#cockpit) | Reviewing Doctor findings and jumping to the traces behind them. | The AgentOps workspace and its release readiness signals. |
+| Foundry portal | Reading a single conversation, trace, or evaluation run. | One agent run at a time inside the Foundry project. |
+
+Reach for the workbook when someone asks how much the agent is being used, where
+latency is coming from, or whether you are close to a throughput limit. Reach for
+the Cockpit or Foundry when you need to inspect a specific finding or trace.
+
+## Prerequisites
+
+The workbook reads Azure OpenAI diagnostic logs, so those logs must be flowing
+into a Log Analytics workspace before any chart has data.
+
+Turn on both diagnostic categories on the Azure OpenAI resource that backs the
+Foundry project:
+
+| Diagnostic setting | Why it is needed |
+|---|---|
+| `RequestResponse` | Per-request logs for traffic, latency, status codes, and throttling. |
+| `AzureOpenAIRequestUsage` | Token and usage logs for prompt, completion, and total tokens. |
+
+Route both categories to the same Log Analytics workspace you plan to point the
+workbook at. Allow time for the first logs to arrive, since diagnostic ingestion
+can lag the first requests by a few minutes.
+
+You also need the right Azure role for what you are doing:
+
+| Action | Minimum role | Scope |
+|---|---|---|
+| View the workbook and its charts | Log Analytics Reader | The Log Analytics workspace. |
+| Deploy or update the workbook | Workbook Contributor | The resource group or subscription that holds the workbook. |
+
+## Deploy, open, and export
+
+Deploy the workbook with one command from a configured workspace. AgentOps reads
+the Foundry project endpoint, discovers the linked Log Analytics workspace, and
+creates or updates the workbook in place.
+
+```bash
+agentops telemetry dashboard deploy
+```
+
+Open the deployed workbook directly in the Azure portal without hunting for it in
+the resource list.
+
+```bash
+agentops telemetry dashboard open
+```
+
+If you cannot deploy because you lack Workbook Contributor, export the workbook
+definition instead and hand it to someone who can import it.
+
+```bash
+agentops telemetry dashboard export
+```
+
+The export writes the workbook JSON to your workspace so a portal admin can
+create the workbook manually, or so you can commit it and deploy it through your
+own infrastructure pipeline.
+
+## A tour of the four sections
+
+The workbook is organized into four sections that read top to bottom, from
+"how much traffic" down to "what is failing".
+
+### 1. Traffic and usage
+
+This section answers how much the agent is being called. It charts request volume
+over time and breaks it down by deployment, so a spike or a drop is obvious at a
+glance.
+
+### 2. Latency and reliability
+
+This section shows how the agent is performing. It plots p50, p95, and p99
+latency and the success rate, so you can separate a slow tail from a broad
+slowdown.
+
+### 3. Tokens and throughput
+
+This section covers consumption and capacity. It charts prompt, completion, and
+total tokens, tokens per minute, and the normalized provisioned throughput usage
+described below.
+
+### 4. Errors and throttling
+
+This section highlights what is failing. It counts errors by status code and
+tracks throttled `429` responses, which is the first signal that you are hitting a
+rate or quota limit.
+
+!!! info "The PTU_Normalizado column"
+ `PTU_Normalizado` is a derived column, not a raw platform metric. It
+ normalizes token consumption against the provisioned throughput units of a
+ deployment so that utilization is comparable across models and deployments of
+ different sizes. Read a value near the top of its range as a deployment that
+ is close to its provisioned capacity, and treat it as a planning signal rather
+ than a hard limit.
+
+## Troubleshooting empty charts
+
+An empty workbook almost always means the underlying logs are missing, not that
+the workbook is broken. Work through these causes from most to least common.
+
+| Symptom | Likely cause | Fix |
+|---|---|---|
+| Every chart is empty | Diagnostic settings are off, or point at a different workspace. | Enable `RequestResponse` and `AzureOpenAIRequestUsage` on the Azure OpenAI resource and route both to the workbook's workspace. |
+| Traffic and latency show data, tokens do not | Only `RequestResponse` is enabled. | Add the `AzureOpenAIRequestUsage` category. |
+| Charts are empty only for recent time | Ingestion lag or no traffic in the window. | Widen the time range and re-run some requests, then wait a few minutes. |
+| Charts load but you see a permissions error | Missing read access. | Grant Log Analytics Reader on the workspace. |
+
+If data exists in the workspace but the workbook still looks wrong, confirm the
+column names in your logs match the queries. The exact names depend on whether
+the resource uses Azure diagnostics or resource-specific tables, which the
+[KQL library](foundry-ops-workbook-kql.md) explains.
+
+## Next
+
+Browse the [KQL library](foundry-ops-workbook-kql.md) behind these charts, return
+to [Operate](operate.md) for the readiness loop, or see where the signal comes
+from on the [Observe](observe.md) page.
diff --git a/docs/how-it-works.md b/docs/how-it-works.md
index f397ac01..dd53523a 100644
--- a/docs/how-it-works.md
+++ b/docs/how-it-works.md
@@ -14,17 +14,12 @@ is the proof?** It:
4. Returns CI-friendly exit codes: `0` pass, `2` threshold failure, `1` error.
5. Writes release evidence with `agentops doctor --evidence-pack`.
-Foundry manages agent creation, deployment, runtime, traces, monitoring,
+Foundry owns agent creation, deployment, runtime, traces, monitoring,
red-teaming, datasets, and Microsoft-hosted evaluation drilldown. AgentOps
references the candidate those tools produced and adds the repo-controlled
release proof:
config, gates, artifacts, PR reports, Doctor diagnostics, release evidence,
trace-to-regression promotion, and Cockpit links back to Foundry/Azure Monitor.
-The additive **Agent behavior** tab in the Foundry operations Azure Monitor
-workbook reads compatible platform-owned evaluation events for cross-evaluator
-trends and trace correlation while leaving evaluation and trace drilldown in
-Foundry. Human trace annotations are a documented event producer; automated
-trace-evaluation event export remains validation-dependent.
### Key Principles
@@ -234,10 +229,20 @@ flowchart LR
| `agentops init show` | Inspect resolved config (`agentops.yaml` + local env values) |
| `agentops init explain` | Long-form `init` manual |
| `agentops eval analyze` | Inspect eval setup and recommend direct run vs skill-assisted configuration |
+| `agentops eval init` | Initialize Foundry-native eval assets with `azd` |
| `agentops eval run` | Run an evaluation; the main command |
| `agentops eval run --baseline ` | Run an eval and add a baseline comparison section to the report |
| `agentops eval promote-traces` | Convert local trace exports into reviewable regression dataset rows |
| `agentops report generate` | Regenerate `report.md` from a `results.json` |
+| `agentops assert run` | Invoke the ASSERT (`assert-ai`) CLI and normalize its results |
+| `agentops redteam run` | Invoke the Foundry / PyRIT AI Red Teaming agent and normalize its results |
+| `agentops prompt pull` | Pull Foundry prompt-agent instructions into a prompt file |
+| `agentops telemetry validate ` | Validate a named telemetry import without querying Azure |
+| `agentops telemetry preview ` | Query Azure Monitor and print a small dataset preview |
+| `agentops telemetry import ` | Import telemetry into the configured JSONL output path |
+| `agentops telemetry dashboard deploy` | Deploy the Foundry operations workbook to Azure Monitor |
+| `agentops telemetry dashboard open` | Open the Foundry operations workbook in the Azure portal |
+| `agentops telemetry dashboard export` | Export the packaged workbook JSON to a local path |
| `agentops doctor [--evidence-pack]` | Run the AgentOps Doctor and optionally write release evidence |
| `agentops doctor explain` | Long-form Doctor manual |
| `agentops cockpit` | Local read-only Cockpit UI (FastAPI) that links out to Foundry |
@@ -297,7 +302,7 @@ continue to use `.azure//.env`.
The legacy layered layout (`.agentops/config.yaml` + `bundles/` +
`datasets/*.yaml` + `run.yaml`) **no longer exists**. The new schema is
-declared by [src/agentops/core/agentops_config.py](../src/agentops/core/agentops_config.py)
+declared by [src/agentops/core/agentops_config.py](https://github.com/Azure/agentops/blob/main/src/agentops/core/agentops_config.py)
and rejects any of the legacy top-level keys (`target`, `bundle`,
`execution`, `output`, `scenario`, `backend`, `run`) at parse time with
an actionable error.
@@ -331,12 +336,11 @@ That's a complete config. AgentOps:
| `thresholds` | no | Metric gates such as `">=3"` or `"<=10"`. |
| `protocol` | no | URL protocol: `responses`, `invocations`, or `http-json`. |
| `request_field` / `response_field` / `tool_calls_field` | no | Request/response JSON keys or dot-paths. |
-| `response_fields` | no | Map of `name -> dot-path` capturing extra fields from a JSON response. Each captured value is exposed to evaluator `input_mapping` as `$response.`. Only used when `response_mode` is `json`. |
| `headers` | no | Static HTTP headers (dict). |
| `auth_header_env` | no | Env var name holding a Bearer token. |
| `evaluators` | no | Escape-hatch list of evaluator names that overrides auto-selection. |
| `publish` | no | Boolean. With `execution: local`, `true` uploads local metrics to Classic Foundry. With `execution: cloud`, publishing is implicit. |
-| `execution` | no | `local` (default) runs through AgentOps locally. `cloud` runs a Foundry agent server-side through the OpenAI Evals API — either a prompt agent (`name:version`) or a hosted agent URL containing `/agents//versions/`. |
+| `execution` | no | `local` (default) runs through AgentOps locally. `cloud` runs a Foundry prompt agent server-side through the OpenAI Evals API. |
| `project_endpoint` | no | Foundry project URL used by Foundry invocation and publishing. Falls back to `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT`. |
| `dataset_sync` | no | Cloud-evaluation dataset policy: `auto`, `foundry`, or `inline`. |
@@ -386,35 +390,6 @@ response_field: text # dot-path; default is "text"
auth_header_env: APP_API_TOKEN # value used as Bearer token
```
-**HTTP-deployed agent with grey-box retrieval capture (RAG evaluators):**
-
-When the endpoint can return its retrieval alongside the answer (for example a
-JSON body `{"answer": ..., "context": ..., "retrieved_documents": [...]}`),
-capture the extra fields with `response_fields` and reference them in evaluator
-`input_mapping` via `$response.`. This scores the retrieval actually used
-at eval time instead of static dataset context.
-
-```yaml
-version: 1
-agent: https://my-aca-app.eastus2.azurecontainerapps.io/orchestrator
-dataset: .agentops/data/qa.jsonl
-response_mode: json
-request_field: ask
-response_field: answer # primary prediction (dot-path)
-response_fields: # extra fields captured per row
- context: context
- retrieved_documents: retrieved_documents
-bundle:
- evaluators:
- - name: groundedness
- config:
- kind: builtin
- class_name: GroundednessEvaluator
- input_mapping:
- response: $response.answer
- context: $response.context
-```
-
**Raw model deployment:**
```yaml
@@ -430,27 +405,12 @@ thresholds:
```yaml
version: 1
-agent: my-rag:3 # prompt agent
+agent: my-rag:3 # name:version is required for cloud mode
dataset: .agentops/data/qa.jsonl
execution: cloud
# project_endpoint: "https://.services.ai.azure.com/api/projects/
"
```
-A hosted agent works the same way, as long as the URL carries the agent
-name and version. That is the pair Foundry needs to resolve the target:
-
-```yaml
-version: 1
-agent: https://.services.ai.azure.com/api/projects/
/agents/helpdeskbot/versions/11
-dataset: .agentops/data/qa.jsonl
-protocol: responses
-execution: cloud
-```
-
-A hosted URL without a `/versions/` segment cannot be cloud-executed,
-because there is no version to pin the run to. Add the version to the URL, or
-keep `execution: local` for that target.
-
## Datasets
A dataset is a plain JSONL file. One row per line. No companion YAML.
@@ -530,7 +490,7 @@ have permission to create Foundry datasets.
## Evaluator auto-selection
-The catalog is defined in [src/agentops/core/evaluators.py](../src/agentops/core/evaluators.py).
+The catalog is defined in [src/agentops/core/evaluators.py](https://github.com/Azure/agentops/blob/main/src/agentops/core/evaluators.py).
Selection rules (in order):
1. If `evaluators:` is set in `agentops.yaml`, use it verbatim (escape hatch).
@@ -583,7 +543,7 @@ link.
|---|---|---|---|
| `execution: local`, `publish: false` | AgentOps invokes target and evaluators locally | None; local artifacts only | Any target |
| `execution: local`, `publish: true` | AgentOps local run, then metric upload | Classic Foundry Evaluations panel | Any target |
-| `execution: cloud` | Foundry runs agent + evaluators server-side through the OpenAI Evals API | New Foundry Evaluations panel; publish is implicit | Foundry Prompt Agent (`name:version`) or Foundry Hosted Agent URL with `/agents//versions/` |
+| `execution: cloud` | Foundry runs agent + evaluators server-side through the OpenAI Evals API | New Foundry Evaluations panel; publish is implicit | Foundry Prompt Agent (`name:version`) or Foundry Hosted Agent URL containing `/agents//versions/` |
Foundry-visible modes:
@@ -611,17 +571,17 @@ pack. The standalone Microsoft Foundry AI Agent Evaluation GitHub Action or
Azure DevOps extension remains useful for platform-native validation outside the
AgentOps release-readiness flow.
-Implementation lives in [src/agentops/pipeline/publisher.py](../src/agentops/pipeline/publisher.py)
-(Classic) and [src/agentops/pipeline/cloud_runner.py](../src/agentops/pipeline/cloud_runner.py)
+Implementation lives in [src/agentops/pipeline/publisher.py](https://github.com/Azure/agentops/blob/main/src/agentops/pipeline/publisher.py)
+(Classic) and [src/agentops/pipeline/cloud_runner.py](https://github.com/Azure/agentops/blob/main/src/agentops/pipeline/cloud_runner.py)
(New Foundry). Dispatch happens in
-[src/agentops/pipeline/orchestrator.py](../src/agentops/pipeline/orchestrator.py).
+[src/agentops/pipeline/orchestrator.py](https://github.com/Azure/agentops/blob/main/src/agentops/pipeline/orchestrator.py).
## Pre-flight checks
-Before any agent invocation, [services/preflight.py](../src/agentops/services/preflight.py)
-runs a short series of checks and reports **all** rows at once instead of
-stopping at the first problem. It is wired into `agentops doctor` and
-`agentops cockpit`:
+`agentops doctor` and `agentops cockpit` run a short pre-flight before doing
+real work. It lives in
+[src/agentops/services/preflight.py](https://github.com/Azure/agentops/blob/main/src/agentops/services/preflight.py)
+and reports **all** rows at once instead of stopping at the first problem:
* **Workspace** — the target directory is a usable AgentOps workspace.
* **Azure authentication** — `DefaultAzureCredential` acquires an ARM token
@@ -766,9 +726,9 @@ Azure SDK dependencies are kept separate so the CLI stays lightweight and tests
## Quick Reference for New Contributors
-1. **Install in dev mode**: `uv sync --group dev`. This installs the package in editable mode together with the `dev` dependency group, which is exactly what CI runs. `dev` is a PEP 735 dependency group rather than an extra, so `pip install -e ".[dev]"` silently installs nothing extra and leaves you without `pytest`.
-2. **Run tests**: `uv run pytest tests/ -x -q`
-3. **Try it out**: `agentops init` then explore `.agentops/`
+1. **Install in dev mode**: `uv sync --group dev`. `dev` is a PEP 735 dependency group, not an extra, so `pip install -e ".[dev]"` does not install it: pip warns about an unknown extra and installs the project without the dev tools. With pip, use `pip install -e ".[agent,mcp]"` and add test tools yourself. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
+2. **Run tests**: `uv run python -m pytest tests/ -x -q`. `uv sync` populates `.venv` but does not activate it, so a bare `python` may be the system interpreter. `uv run` is what CI uses. If you prefer, activate `.venv` first and then call `python` directly.
+3. **Try it out**: `uv run agentops init` then explore `.agentops/`
4. **Read the models**: `core/models.py` is the best single file to understand all data structures
5. **Follow the flow**: `cli/app.py` → `services/runner.py` → `backends/` → `core/`
6. **Keep CLI thin**: never put logic in `cli/app.py` - delegate to `services/`
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..b53709bc
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,114 @@
+---
+hide:
+ - navigation
+ - toc
+---
+
+
+
+
+
+
+# AgentOps Accelerator
+
+
+
The open-source AgentOps jumpstart for continuous evaluation, safety testing, observability, and release readiness of Microsoft Foundry agents.
+
+## What AgentOps does
+
+AgentOps turns Foundry evaluation, safety, and observability signals into a
+repeatable ship or no-ship workflow. It connects Foundry Evaluations, the ASSERT
+safety framework, the PyRIT-backed AI Red Teaming agent, Azure Monitor, and your
+CI/CD platform into one release loop. Every result is packaged into a stable
+evidence pack that proves a release is ready for production.
+
+!!! tip "Install in 60 seconds"
+ Install the package, bootstrap a workspace, and drop the skills into your
+ coding agent.
+
+ ```bash
+ pip install "agentops-accelerator[agent]"
+ agentops init
+ agentops skills install --platform copilot
+ ```
+
+ The `[agent]` extra pulls in the FastAPI stack behind `agentops cockpit` and
+ `agentops agent serve`. Plain `pip install agentops-accelerator` gives you
+ the eval and Doctor commands but raises an `ImportError` on those two.
+
+
+
+
+
+
+
+
+### :material-clipboard-check: Evaluate
+Read [Evaluation](evaluation.md) to learn how datasets, evaluators, thresholds,
+and rubrics turn an agent into a pass or fail gate. Start with `agentops eval run`
+and the `agentops-eval` skill.
+
+
+
+### :material-source-branch: Ship
+[Ship](ship.md) explains the generated PR gate and dev deploy workflows, and how
+candidate versions become a release. Start with
+`agentops workflow generate --kinds pr` and the `agentops-workflow` skill.
+
+
+
+### :material-radar: Observe
+[Observe](observe.md) covers Foundry traces and Azure Monitor, and how
+production signals feed continuous evaluation. Start with
+`agentops telemetry validate` and the `agentops-agent` skill.
+
+
+
+### :material-stethoscope: Operate
+[Operate](operate.md) shows how Doctor scores readiness and packages an evidence
+pack so you can make the ship or no-ship call. Start with `agentops doctor` and
+the `agentops-governance` skill.
+
+
+
+
+## Reference architecture
+
+Use this as the mental model for the AgentOps loop: build in a sandbox, commit
+the release contract to source control, promote through environments with
+evidence, then feed production learning back into the next evaluation set.
+
+{ .agentops-reference-architecture }
+
+## Where to go next
+
+
+Pick a tutorial to learn the sandbox to PR gate flow end to end, or jump straight
+to the evaluation reference.
+
+[Tutorials :material-rocket-launch:](tutorials.md){ .md-button--pill }
+[Evaluation reference :material-book-open-variant:](evaluation.md){ .md-button--pill }
+
+
+Contributions are welcome. See the
+[repository](https://github.com/Azure/agentops) for guidelines, issues, and the
+contribution process.
diff --git a/docs/javascripts/mermaid.js b/docs/javascripts/mermaid.js
new file mode 100644
index 00000000..7a3f946e
--- /dev/null
+++ b/docs/javascripts/mermaid.js
@@ -0,0 +1,25 @@
+document$.subscribe(function () {
+ if (typeof mermaid === "undefined") {
+ return;
+ }
+
+ document.querySelectorAll("pre.mermaid > code").forEach(function (code) {
+ const pre = code.parentElement;
+ const diagram = document.createElement("div");
+ diagram.className = "mermaid";
+ diagram.textContent = code.textContent;
+ pre.replaceWith(diagram);
+ });
+
+ mermaid.initialize({
+ startOnLoad: false,
+ theme: document.body.getAttribute("data-md-color-scheme") === "slate" ? "dark" : "default",
+ themeVariables: {
+ fontSize: "18px"
+ }
+ });
+
+ mermaid.run({
+ querySelector: ".mermaid"
+ });
+});
diff --git a/docs/media/agentops-architecture.png b/docs/media/agentops-architecture.png
new file mode 100644
index 00000000..a88742ca
Binary files /dev/null and b/docs/media/agentops-architecture.png differ
diff --git a/docs/media/foundry.svg b/docs/media/foundry.svg
new file mode 100644
index 00000000..4bab104e
--- /dev/null
+++ b/docs/media/foundry.svg
@@ -0,0 +1,71 @@
+
+
\ No newline at end of file
diff --git a/docs/media/logo.png b/docs/media/logo.png
new file mode 100644
index 00000000..ad1ad1f7
Binary files /dev/null and b/docs/media/logo.png differ
diff --git a/docs/media/vw-fuel-system.pdf b/docs/media/vw-fuel-system.pdf
new file mode 100644
index 00000000..932170ce
Binary files /dev/null and b/docs/media/vw-fuel-system.pdf differ
diff --git a/docs/observe.md b/docs/observe.md
new file mode 100644
index 00000000..20b4e49a
--- /dev/null
+++ b/docs/observe.md
@@ -0,0 +1,155 @@
+# Observe
+
+This page explains how AgentOps uses agent observability. Foundry and Azure
+Monitor produce the runtime signal; AgentOps reads that signal so release
+readiness reflects what is actually happening in production, not just what
+passed in CI.
+
+Observability is conceptual here. For the hands-on portal and KQL walkthrough,
+see step 18 of the [Foundry Prompt Agent tutorial](tutorial-prompt-agent.md).
+
+## Where the signal comes from
+
+Foundry gives you the runtime view of an agent: traces, conversations, spans,
+latency, and model calls per run. Behind that view, Foundry emits telemetry to
+**Azure Monitor / Application Insights**, where requests, errors, and evaluation
+events are stored and queryable.
+
+AgentOps does not replace either surface. It reads them so the same runtime
+truth feeds the readiness story alongside eval results and Doctor findings.
+
+## What AgentOps reads
+
+AgentOps connects to Application Insights through
+`APPLICATIONINSIGHTS_CONNECTION_STRING`. When a Foundry project endpoint is set,
+AgentOps first tries to auto-discover the project's App Insights resource and
+falls back to that connection string when discovery is not available.
+
+!!! info "Telemetry from CI runs"
+ Generated eval and Doctor workflows install AgentOps telemetry support.
+ Eval runs emit `agentops.eval.*` spans and scheduled Doctor runs emit
+ `agentops.agent.finding.*` spans, both of which the Cockpit can deep-link
+ into Azure Monitor Logs.
+
+## Operations dashboard
+
+Traces answer "what did this run do." Operational metrics answer "is the
+deployment healthy." AgentOps ships an Azure Monitor workbook for the Foundry /
+Azure OpenAI deployments behind your agent, so operators read PTU utilization,
+PAYG spillover, throughput, latency percentiles, and error and throttling rates
+in one place.
+
+The workbook is scoped per Azure OpenAI resource and per Log Analytics
+workspace, with tabs for capacity, traffic and tokens, latency, and errors and
+throttling. You can deploy it, open it, or export the JSON with the CLI, or
+import it by hand into Azure Monitor.
+
+!!! info "Doctor checks the diagnostic settings"
+ The dashboard needs the Azure OpenAI resource to send `RequestResponse` and
+ `AzureOpenAIRequestUsage` logs to Log Analytics. `agentops doctor` flags
+ when they are missing (rule `waf.observability.aoai_diagnostic_categories`)
+ and prints the exact `az monitor diagnostic-settings` command to fix it.
+
+## Traces as evaluation signal
+
+A single trace shows what one request did. The value for release readiness comes
+from reading many traces at once: latency percentiles, error rates, and the
+evaluation results Foundry records as `gen_ai.evaluation.result` events.
+
+The Doctor turns this into findings. It reads App Insights for p95 latency and
+error rate, and it reports when telemetry is connected but silent, so a project
+with no monitoring does not look healthy simply because nothing is being graded.
+
+!!! note "Real telemetry produces honest findings"
+ Because the Doctor reads live runtime data, it can surface latency or error
+ findings from your own production traffic, separate from the eval gate. That
+ is intended: a real release should investigate latency and errors before
+ promoting, even when the candidate's eval scores pass.
+
+## Trace-to-regression promotion
+
+The strongest use of observability is turning real production behavior into new
+evaluation coverage. Reviewed production traces become new dataset rows, so the
+cases your agent actually sees keep getting evaluated on every future run.
+
+In Foundry, this is the trace-to-dataset flow: sample recent traces, let
+intelligent sampling deduplicate and select a representative set, and create an
+evaluation dataset from them. AgentOps then promotes that into reviewable
+regression rows with `agentops eval promote-traces`.
+
+!!! warning "Promotion is review-first"
+ Trace-derived rows are candidates, not ground truth. Self-similarity labels
+ are useful for drift detection, not human-verified correctness, so a person
+ should confirm or fill the expected answers before those rows gate a
+ release. This keeps regression data trustworthy as it grows.
+
+The loop is the point: traces become datasets, datasets gate the next release,
+and the agent keeps getting evaluated on the behavior that matters in
+production.
+
+## Try it
+
+Confirm the signal is flowing, then turn real traces into regression coverage.
+
+1. Check that AgentOps can reach Application Insights before you rely on the signal.
+
+ ```bash
+ agentops telemetry validate
+ ```
+
+2. Preview the traces and evaluation events AgentOps can currently see.
+
+ ```bash
+ agentops telemetry preview
+ ```
+
+3. Import a trace export so it can become regression coverage.
+
+ ```bash
+ agentops telemetry import
+ ```
+
+4. Promote reviewed production traces into regression dataset rows.
+
+ ```bash
+ agentops eval promote-traces --source .agentops/traces/export.jsonl
+ ```
+
+5. Preview the operations dashboard as an ARM template, without touching Azure.
+
+ ```bash
+ agentops telemetry dashboard deploy --dry-run
+ ```
+
+6. Deploy the workbook, then open it in the Azure portal.
+
+ ```bash
+ agentops telemetry dashboard deploy
+ agentops telemetry dashboard open
+ ```
+
+To browse this signal interactively and deep-link into Foundry and Azure
+Monitor, run `agentops cockpit`. That local command center is covered on the
+[Operate](operate.md#cockpit) page.
+
+## Run from your coding agent
+
+Install the AgentOps skills so your coding agent can read telemetry and grow the
+regression set for you.
+
+```bash
+agentops skills install --platform copilot
+```
+
+The skills that map to observability are:
+
+| Skill | What it helps with |
+|---|---|
+| `agentops-agent` | Watchdog analysis of production health and latency spikes. |
+| `agentops-eval` | Promote traces and re-evaluate against the hardened dataset. |
+
+## Next
+
+Act on the signal over time on the [Operate](operate.md) page, feed passing
+evidence back into the gate on the [Ship](ship.md) page, or harden the dataset
+on the [Evaluation](evaluation.md) page.
diff --git a/docs/operate.md b/docs/operate.md
new file mode 100644
index 00000000..505f5c24
--- /dev/null
+++ b/docs/operate.md
@@ -0,0 +1,183 @@
+# Operate
+
+This page is about operating an agent over time, not just shipping it once.
+Operating is the loop of scoring readiness, proving the ship decision with
+evidence, and feeding production learning back into the next evaluation. The
+Doctor and the evidence pack are the two tools that make that loop concrete.
+
+For the full check inventory, see the [Doctor checks reference](doctor-checks.md).
+For a narrative walkthrough of what the Doctor is and how it reasons, see
+[The Doctor, explained](doctor-explained.md).
+
+## Doctor as the readiness scorer
+
+The Doctor is a regular check-up for an agent project. It reads signals that are
+already there, eval history, App Insights telemetry, Foundry metadata, and Azure
+resource configuration, and emits **findings**: severity-ranked observations
+with a recommendation attached.
+
+It does not fix anything and it does not replace Foundry's compliance surface. It
+is the complementary half that scores runtime telemetry, identity scope, eval
+discipline, and pipeline hygiene.
+
+```
+agentops doctor
+```
+
+!!! info "Findings, severities, and exit codes"
+ Findings are grouped into categories like quality, performance, reliability,
+ security, responsible AI, and operational excellence. Severity is
+ independent of category, so a quality finding can be critical, warning, or
+ info. The Doctor exits `0` when nothing meets the configured
+ `--severity-fail` floor, `2` when something does, and `1` if the analyzer
+ itself errored.
+
+## Ship/no-ship evidence pack
+
+Adding `--evidence-pack` turns a Doctor run into a release decision artifact:
+
+```bash
+agentops doctor --evidence-pack
+```
+
+This writes `.agentops/release/latest/evidence.json` and `evidence.md`. The
+evidence pack projects signals you already produce, eval results, baselines,
+Doctor findings, workflow files, Foundry continuous-eval, monitoring, and
+trace-regression manifests, into one readiness summary.
+
+| Artifact | Use it for |
+|---|---|
+| `evidence.json` | The stable machine-readable contract (`version: 1`) for automation. |
+| `evidence.md` | The PR and release-review summary, including the Doctor finding rollup. |
+
+!!! note "Evidence does not add a new gate"
+ The readiness states `ready`, `ready_with_warnings`, and `blocked` are
+ projections of existing signals. They do not create a second exit-code
+ contract: eval and Doctor exit codes stay exactly as they are. A `blocked`
+ status tells a reviewer to stop; the underlying Doctor exit code still
+ depends only on `--severity-fail`.
+
+## Release readiness
+
+Release readiness is the question the evidence pack answers: is there current,
+passing eval evidence, a baseline to judge regressions against, promoted
+production traces where they exist, and continuous evaluation wired up. The
+Doctor emits operational-excellence findings for each of these so gaps are
+visible before a release review, not after.
+
+Generated production workflows append the evidence report to the run summary, so
+when a release blocks you can start from the critical and warning finding ids
+before opening the full artifact.
+
+## Cockpit
+
+The Cockpit is a local web UI for operating an agent day to day. It browses the
+Doctor findings that AgentOps owns end to end, and it deep-links out to Foundry
+and Azure Monitor for the runtime views those surfaces own.
+
+```bash
+agentops cockpit
+```
+
+Start the Cockpit from a configured workspace to review findings, open the
+evidence pack, and jump into the traces behind a finding. It reads the same
+signals as the Doctor, so what you see matches the gate.
+
+## Assurance and governance
+
+Readiness is not only quality and latency. A production agent also needs safety
+and adversarial assurance, so AgentOps runs two checks you can gate on and attach
+to the evidence pack.
+
+| Command | What it does |
+|---|---|
+| `agentops assert run` | Runs the ASSERT safety framework against the agent. |
+| `agentops redteam run` | Runs the PyRIT-backed AI Red Teaming agent for adversarial probing. |
+
+```bash
+agentops assert run
+agentops redteam run
+```
+
+Use the `agentops-governance` skill when you want a coding agent to set up
+ASSERT, Azure Content Safety, guardrails, and red-team readiness for you.
+
+## The operating loop
+
+Operating an agent means running this loop, not a one-time checklist.
+
+```mermaid
+flowchart LR
+ M["Monitor traces + telemetry"] --> R["Regress promote traces to dataset"]
+ R --> E["Re-evaluate eval run + Doctor"]
+ E --> P["Prove evidence pack"]
+ P --> M
+```
+
+You monitor production behavior, promote reviewed traces into regression rows,
+re-evaluate against the hardened dataset, and produce fresh evidence for the next
+decision. Each pass makes the gate reflect more of what the agent actually does.
+
+When re-evaluation shows weak grounding or off-topic answers, the cause is often
+retrieval. To measure and tune search quality directly, see
+[Retrieval optimization](retrieval-optimization.md).
+
+To see the monitoring half of this loop in depth, read [Observe](observe.md).
+To see how the gate runs in CI, read [Ship](ship.md).
+
+## Try it
+
+Score readiness, prove the decision, and add assurance before you promote.
+
+1. Score readiness across quality, performance, reliability, security, and OpEx.
+
+ ```bash
+ agentops doctor
+ ```
+
+2. Turn the run into a ship or no-ship evidence pack.
+
+ ```bash
+ agentops doctor --evidence-pack
+ ```
+
+3. Read what a finding means and how the Doctor reasons about it.
+
+ ```bash
+ agentops doctor explain
+ ```
+
+4. Open the local Cockpit to browse findings and deep-link into Foundry and Azure Monitor.
+
+ ```bash
+ agentops cockpit
+ ```
+
+5. Add safety and red-team assurance before you promote.
+
+ ```bash
+ agentops assert run
+ agentops redteam run
+ ```
+
+## Run from your coding agent
+
+Install the AgentOps skills so your coding agent can triage findings and set up
+governance for you.
+
+```bash
+agentops skills install --platform copilot
+```
+
+The skills that map to operating are:
+
+| Skill | What it helps with |
+|---|---|
+| `agentops-agent` | Watchdog analysis of production health and latency spikes. |
+| `agentops-governance` | ASSERT, Azure Content Safety, guardrails, and red-team readiness. |
+
+## Next
+
+Browse the full [Doctor checks reference](doctor-checks.md), watch usage and cost
+in the [Foundry operations workbook](foundry-ops-workbook.md), or return to
+[Observe](observe.md) for the signal side of the loop.
diff --git a/docs/release-process.md b/docs/release-process.md
index a2beeecb..026593eb 100644
--- a/docs/release-process.md
+++ b/docs/release-process.md
@@ -1,969 +1,969 @@
-# GitOps Guide: Building and Releasing AgentOps Toolkit
-
-This guide is a comprehensive instruction manual for engineers working on the **agentops-accelerator** project. It covers the full GitOps lifecycle - from setting up your development environment, through the branching model and CI pipeline, to staging and production releases.
-
-## Table of Contents
-
-- [1. GitOps Principles](#1-gitops-principles)
-- [2. Branching Model](#2-branching-model)
-- [3. Development Environment Setup](#3-development-environment-setup)
-- [4. Development Workflow](#4-development-workflow)
-- [5. CI Pipeline (Continuous Integration)](#5-ci-pipeline-continuous-integration)
-- [6. Versioning with setuptools-scm](#6-versioning-with-setuptools-scm)
-- [7. Staging Pipeline (TestPyPI)](#7-staging-pipeline-testpypi)
-- [8. End-to-End Pipeline Testing](#8-end-to-end-pipeline-testing)
-- [9. Production Release Pipeline (PyPI)](#9-production-release-pipeline-pypi)
-- [10. Infrastructure Setup](#10-infrastructure-setup)
-- [11. Workflow File Reference](#11-workflow-file-reference)
-- [12. Release Checklist](#12-release-checklist)
-- [13. Troubleshooting](#13-troubleshooting)
-
-## 1. GitOps Principles
-
-AgentOps follows GitOps practices where **git is the single source of truth** for both code and operational state:
-
-- **Declarative configuration** - All pipeline behavior is defined in YAML workflow files checked into the repository.
-- **Version-controlled releases** - Every release is traceable to a git tag. No manual version edits.
-- **Automated pipelines** - Pushing branches or tags triggers the corresponding workflow automatically.
-- **Keyless publishing** - PyPI uploads use Trusted Publishing (OIDC). There is no PyPI API token to store or rotate.
-- **Immutable artifacts** - Built packages are uploaded once and reused across pipeline stages (no rebuilds between TestPyPI and PyPI).
-
-## 2. Branching Model
-
-AgentOps uses a modified [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) strategy:
-
-```
-main ← always production-ready, receives merges from release/* branches
- │
-develop ← integration branch, all feature PRs target here
- │
- ├── feature/* ← individual features branched from develop
- │
- └── release/* ← release preparation, branched from develop when ready to ship
-```
-
-### Branch Purposes
-
-| Branch | Purpose | Who creates | Merges into |
-| ---------------- | -------------------------------------------------------------------- | ---------------- | ----------------------------- |
-| `main` | Production-ready code. Every commit here should be a tagged release. | Maintainers only | - |
-| `develop` | Integration branch. All feature work flows through here. | - | `main` (via release branches) |
-| `feature/*` | Individual features, bug fixes, or improvements. | Any contributor | `develop` |
-| `release/v0.X.Y` | Release stabilization and staging. Triggers TestPyPI pipeline. | Maintainers | `main` |
-
-### Branch Lifecycle
-
-```
-1. feature/my-change ──PR──→ develop (contributor)
-2. develop ──branch──→ release/v0.2.0 (maintainer, when ready to release)
-3. release/v0.2.0 ──PR──→ main (maintainer, after staging validates)
-4. main ──tag──→ v0.2.0 (maintainer, publishes to PyPI immediately)
-5. main ──merge──→ develop (maintainer, REQUIRED, same sitting as step 4)
-6. release/v0.2.0 ──delete── (maintainer, cleanup)
-```
-
-Steps 4 and 5 are a single unit of work. Leaving `develop` behind `main` corrupts
-the next release's CHANGELOG. See
-[Step 5: Tag the release and sync develop](#step-5-tag-the-release-and-sync-develop).
-
-### Branch Protection Rules (Recommended)
-
-Configure these in **Settings → Branches → Branch protection rules**:
-
-| Branch | Rules |
-| ----------- | ------------------------------------------------------------------------ |
-| `main` | Require PR, require status checks (CI), require approvals, no force push |
-| `develop` | Require PR, require status checks (CI), no force push |
-| `release/*` | Require status checks (Staging pipeline), no force push |
-
-## 3. Development Environment Setup
-
-### Prerequisites
-
-- Python 3.11 or later
-- [uv](https://docs.astral.sh/uv/) (recommended) or pip
-- Git with access to the repository
-
-### First-Time Setup
-
-```bash
-# 1. Clone the repository
-git clone https://github.com/Azure/agentops.git
-cd agentops
-
-# 2. Install uv (if not already installed)
-# macOS/Linux:
-curl -LsSf https://astral.sh/uv/install.sh | sh
-# Windows:
-powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
-
-# 3. Install the project and dev dependencies
-uv sync --group dev
-
-# 4. Verify the installation
-uv run agentops --version
-uv run pytest tests/ -x -q
-```
-
-### Alternative Setup (pip)
-
-```bash
-python -m venv .venv
-# Windows:
-.venv\Scripts\Activate.ps1
-# macOS/Linux:
-source .venv/bin/activate
-
-pip install -e .
-pip install pytest
-agentops --version
-python -m pytest tests/ -x -q
-```
-
-### Verify Your Setup
-
-After installation, these commands should all succeed:
-
-```bash
-# CLI works
-agentops --version # Shows version like 0.1.3.dev6
-agentops --help # Shows available commands
-
-# Tests pass
-uv run pytest tests/ -x -q # All tests should pass
-
-# Version from git
-python -m setuptools_scm # Shows version derived from git tags
-```
-
-## 4. Development Workflow
-
-### Creating a Feature
-
-```bash
-# 1. Start from the latest develop
-git checkout develop
-git pull origin develop
-
-# 2. Create your feature branch
-git checkout -b feature/my-new-feature
-
-# 3. Make changes, commit, push
-# ... edit files ...
-uv run pytest tests/ -x -q # Run tests before committing
-git add .
-git commit -m "feat: add my new feature"
-git push origin feature/my-new-feature
-
-# 4. Open a PR targeting develop
-# GitHub will run the CI pipeline automatically
-```
-
-### PR Requirements
-
-Before your PR can be merged to `develop`:
-
-1. **CI pipeline passes** - lint + tests across OS/Python matrix
-2. **Code review approved** - at least one reviewer
-3. **Architecture rules followed** - see [CONTRIBUTING.md](../CONTRIBUTING.md)
-4. **Tests included** - unit tests in `tests/unit/`, integration tests if needed
-5. **CHANGELOG updated** - add an entry under `## [Unreleased]` for user-visible changes. The `changelog` CI job enforces this; see [The CHANGELOG guard](#the-changelog-guard) below.
-
-### After Your PR is Merged
-
-```bash
-# Sync your local develop
-git checkout develop
-git pull origin develop
-
-# Delete your feature branch
-git branch -d feature/my-new-feature
-```
-
-## 5. CI Pipeline (Continuous Integration)
-
-The CI pipeline runs on **every push and PR** to `main` or `develop`.
-
-**Workflow file**: `.github/workflows/ci.yml`
-
-### Jobs
-
-| Job | What it does | Runs on |
-| --- | --- | --- |
-| **lint** | `ruff check` (linting) + `mypy` (type checking, soft-fail) | Ubuntu, Python 3.11 |
-| **changelog** | Fails a PR that changes shipped code without an `## [Unreleased]` entry | Ubuntu, PRs only |
-| **test** | `pytest tests/` with JUnit XML output | Matrix: 2 OS × 3 Python versions |
-| **coverage** | `pytest --cov` with XML coverage report | Ubuntu, Python 3.13 (after tests pass) |
-| **publish-dev** | Build package + publish to TestPyPI (develop pushes only) | Ubuntu, Python 3.12 (after lint + test pass) |
-| **verify-dev** | Install from TestPyPI + smoke test (develop pushes only) | Ubuntu, Python 3.12 (after publish-dev) |
-
-The `publish-dev` and `verify-dev` jobs only run on pushes to `develop` (not on PRs). Every merged PR automatically produces an installable dev build on TestPyPI with a version like `0.1.3.dev12`.
-
-### Test Matrix
-
-| OS | Python 3.11 | Python 3.12 | Python 3.13 |
-| ------- | ----------- | ----------- | ----------- |
-| Ubuntu | ✅ | ✅ | ✅ |
-| Windows | ✅ | ✅ | ✅ |
-
-### What CI Catches
-
-- Syntax and style issues (ruff)
-- Type errors (mypy, non-blocking)
-- Test failures across platforms
-- Import errors or missing dependencies
-- Regression in exit code behavior
-- User-visible changes shipped without a CHANGELOG entry
-
-### Viewing CI Results
-
-1. Go to the **Actions** tab → find the CI run for your PR
-2. Click into a failing job to see the error
-3. Download test result artifacts if needed
-
-### The CHANGELOG guard
-
-`cut-release.yml` does not write changelog content. It inserts a `## [X.Y.Z] - ` heading directly beneath `## [Unreleased]` and nothing more, leaving `[Unreleased]` in place and empty. If no PR wrote anything under `[Unreleased]` during the cycle, the published release section is empty and the release pipeline still goes green. Releases 0.8.4 and 0.8.5 both shipped that way and were backfilled by hand afterwards, between them hiding six bug fixes and six dependency bumps.
-
-Two jobs now close that gap, both driven by `scripts/check_changelog.py`:
-
-- The **`changelog`** job in `ci.yml` runs on every PR to `develop`.
-- A **`check-unreleased`** step in `cut-release.yml` aborts the release before the branch is created if `[Unreleased]` is empty. `scripts/cut-release.sh` and `scripts/cut-release.ps1` run the same check at the same point, so the local path cannot skip it.
-
-#### When the PR check requires an entry
-
-The PR must add a bullet under `## [Unreleased]` when **both** hold:
-
-1. The diff touches a file that ships. Changes confined to `docs/`, `tests/`, `.github/workflows/`, `.github/ISSUE_TEMPLATE/`, `.vscode/`, `media/`, `tombstones/`, or the top-level markdown files never require an entry, whatever the PR is titled.
-2. The PR title carries a user-visible conventional-commit type (`feat`, `fix`, `perf`, `revert`), is marked breaking (`feat!:` or a `BREAKING CHANGE` footer), or has no recognisable type at all. A typed `docs:`, `test:`, `ci:`, `build:`, `style:`, `refactor:`, or `chore:` PR is not asked for an entry.
-
-An untyped title is treated as needing an entry on purpose. A PR that edits shipped code and says nothing about its intent is exactly the case worth a second look.
-
-#### Where the entry has to go
-
-The check parses the CHANGELOG diff and resolves each added line to the section it lands in. A bullet added under an already-released heading fails the same as no bullet at all, because `cut-release.yml` only ever promotes `[Unreleased]`. A bare `### Fixed` subheading with no bullet under it does not count either.
-
-#### Bypassing the check
-
-Apply the **`no-changelog`** label to the PR. The job then reports why it skipped and passes. Use it for changes that genuinely cannot matter to a user of the published package, and say so in the PR description so the reviewer can disagree.
-
-#### Dependabot
-
-Dependabot PRs are exempt. The bot cannot act on a failing check, so requiring an entry would leave every dependency PR red until a human labelled it, which trains everyone to reach for `no-changelog` reflexively. That is not the same as saying dependency bumps do not belong in the changelog: the `cryptography` 48 to 50 and `mcp` 1.27.1 to 1.28.1 bumps in 0.8.5 mattered to readers. Cover them when you cut the release, where one person writes one summary line instead of twelve bots writing twelve.
-
-Nothing enforces that today. `check-unreleased` only asserts that `[Unreleased]` is non-empty, and a single bullet from any PR satisfies it, so a cycle can still reach a tag with its dependency bumps undocumented. Closing that gap properly means reading the merged Dependabot PRs for the cycle, which is a separate change.
-
-#### Running it locally
-
-```bash
-# Is the Unreleased section empty?
-python scripts/check_changelog.py check-unreleased
-
-# Would my branch pass the PR check?
-PR_TITLE="fix: something" PR_AUTHOR="$USER" PR_LABELS='[]' \
- python scripts/check_changelog.py check-pr --base origin/develop
-```
-
-## 6. Versioning with setuptools-scm
-
-AgentOps uses [setuptools-scm](https://github.com/pypa/setuptools-scm) for **fully automatic versioning**. There is **no `version` field in `pyproject.toml`** - the version is derived from git tags at build time.
-
-### How It Works
-
-setuptools-scm reads your git history and computes the version:
-
-| Git state | Example version | Explanation |
-| --------------------------------------------- | --------------- | ----------------------------- |
-| Exactly on tag `v0.2.0` | `0.2.0` | Clean release version |
-| 3 commits after `v0.2.0` | `0.2.1.dev3` | Dev version, 3 commits ahead |
-| 10 commits after `v0.1.2` on `release/v0.2.0` | `0.1.3.dev10` | Dev version on release branch |
-
-### Configuration
-
-In `pyproject.toml`:
-
-```toml
-[build-system]
-requires = ["setuptools>=68", "wheel", "setuptools-scm>=8"]
-
-[project]
-dynamic = ["version"] # Version comes from setuptools-scm, not a static field
-
-[tool.setuptools_scm]
-local_scheme = "no-local-version" # Strips +hash suffix (PyPI rejects local versions)
-```
-
-### Checking the Version
-
-```bash
-# From the installed CLI
-agentops --version
-
-# From setuptools-scm directly
-python -m setuptools_scm
-
-# From Python code
-python -c "from agentops import __version__; print(__version__)"
-```
-
-### Rules
-
-- **Never add `version = "..."` to `pyproject.toml`** - this will conflict with setuptools-scm.
-- **Tags must follow PEP 440** - use `v0.2.0`, not `release-0.2.0` or `0.2.0`.
-- **`fetch-depth: 0`** is required in CI checkout steps - setuptools-scm needs the full git history.
-- **`pip install -e .` requires `.git`** - editable installs need the git directory present (standard for development).
-
-## 7. Staging Pipeline (TestPyPI)
-
-The staging pipeline validates a release candidate by publishing to TestPyPI and verifying the installed package works.
-
-**Workflow file**: `.github/workflows/staging.yml`
-
-**Trigger**: Push to any `release/*` branch
-
-### Pipeline Flow
-
-```mermaid
-flowchart TD
- push(["push to release/v0.2.0"])
- build["_build tests + package Version: 0.2.1.dev3 (setuptools-scm)"]
- publish["publish-testpypi Upload to TestPyPI (staging environment) Trusted Publishing (OIDC, no token)"]
- verify["verify-testpypi Install from TestPyPI in fresh environment agentops --version / --help / init"]
-
- push --> build --> publish --> verify
-```
-
-### What Gets Validated
-
-1. **Tests pass** - the full test suite runs before building
-2. **Package builds** - setuptools-scm generates the correct version, wheel and sdist are created
-3. **Package uploads** - the built artifacts successfully upload to TestPyPI
-4. **Package installs** - `pip install` from TestPyPI resolves all dependencies
-5. **CLI works** - `agentops --version` and `--help` run without errors
-6. **Init works** - `agentops init` creates the expected workspace files
-
-### Iterating on a Release Branch
-
-If staging fails, fix the issue and push again:
-
-```bash
-# On your release/v0.2.0 branch
-# ... fix the issue ...
-git add .
-git commit -m "fix: correct packaging issue"
-git push origin release/v0.2.0
-# Staging pipeline re-runs automatically
-```
-
-Each push generates a new dev version (e.g. `0.2.1.dev4`, `0.2.1.dev5`), so there are no version conflicts on TestPyPI. The `skip-existing: true` flag also prevents failures if the same version is re-uploaded.
-
-### Manual Verification (Optional)
-
-After the staging pipeline passes, you can manually test the package:
-
-```bash
-# Install the specific dev version from TestPyPI
-pip install "agentops-accelerator==0.2.1.dev3" \
- --index-url https://test.pypi.org/simple/ \
- --extra-index-url https://pypi.org/simple/
-
-agentops --version
-agentops --help
-
-# Test init in a temp directory
-cd $(mktemp -d)
-agentops init
-ls .agentops/
-```
-
-> **Note**: `--extra-index-url https://pypi.org/simple/` is required so that dependencies (typer, pydantic, ruamel.yaml) resolve from the real PyPI.
-
-## 8. End-to-End Pipeline Testing
-
-Before cutting a real release, you can validate the entire pipeline end-to-end using a disposable test branch and tag. This is especially useful when:
-
-- You've modified any workflow file (`_build.yml`, `staging.yml`, `release.yml`)
-- You've changed `pyproject.toml` build configuration
-- You've updated setuptools-scm settings
-- A new engineer wants to understand the release process hands-on
-
-### 8.1 Test the Staging Pipeline
-
-#### Step 1: Create a Test Release Branch
-
-From the branch that contains your workflow changes (or from `develop`):
-
-```bash
-git checkout develop # or your feature branch with workflow changes
-git pull origin develop
-git checkout -b release/v0.0.0-test
-git push origin release/v0.0.0-test
-```
-
-This triggers the `staging.yml` workflow automatically.
-
-#### Step 2: Monitor the Pipeline
-
-1. Go to **Actions** tab → find the **Staging** workflow run for `release/v0.0.0-test`
-2. Watch all 3 jobs:
-
-```
-Job 1: build / build → Should tests pass? Package build?
-Job 2: publish-testpypi → Does TestPyPI upload succeed?
-Job 3: verify-testpypi → Can the package install and run?
-```
-
-3. Click into each job to inspect step-level output
-4. If a job fails, read the logs, fix the issue, push again:
-
-```bash
-# Fix and re-push
-git add .
-git commit -m "fix: correct workflow issue"
-git push origin release/v0.0.0-test
-# Pipeline re-runs automatically
-```
-
-#### Step 3: Verify on TestPyPI (Optional)
-
-Confirm the test package appeared on TestPyPI:
-
-```bash
-# Check the version that was published
-python -m setuptools_scm
-
-# Install and test manually
-pip install "agentops-accelerator==$(python -m setuptools_scm)" \
- --index-url https://test.pypi.org/simple/ \
- --extra-index-url https://pypi.org/simple/
-
-agentops --version
-agentops --help
-
-# Test init
-cd $(mktemp -d)
-agentops init
-ls .agentops/
-```
-
-#### Step 4: Clean Up the Test Branch
-
-```bash
-# Delete remote branch
-git push origin --delete release/v0.0.0-test
-
-# Switch back and delete local branch
-git checkout develop
-git branch -d release/v0.0.0-test
-```
-
-### 8.2 Test the Full Release Pipeline
-
-> **There is no safe dry run.** The `publish-pypi` job does not pause, so pushing
-> any `v*` tag publishes that version to real PyPI. There is no reject button to
-> catch it. PyPI versions cannot be deleted, only yanked, so a throwaway
-> `v0.0.0-test.1` tag leaves a permanent artifact on the project page.
-
-Test everything except the final publish by pushing a `release/v*` branch, which
-exercises build → TestPyPI → verify (see [8.1](#81-test-the-staging-pipeline)).
-That covers every job the release pipeline runs before `publish-pypi`, using the
-same build and the same `pypa/gh-action-pypi-publish` action.
-
-If you genuinely need to validate `publish-pypi` end to end, add required
-reviewers to the `release` environment first (see
-[Enabling a real approval gate](#enabling-a-real-approval-gate)). With reviewers
-attached, the job pauses and you can reject it.
-
-#### Verifying the publish path without publishing
-
-```bash
-# Confirm the release environment's protection rules (empty = no gate).
-gh api repos/Azure/agentops/environments/release --jq '.protection_rules'
-
-# Confirm the workflow requests an OIDC token instead of using an API key.
-grep -n "id-token\|gh-action-pypi-publish" .github/workflows/release.yml
-```
-
-Trusted Publishing must also be configured on the PyPI side under
-**Manage project → Publishing**, matching the repository, workflow filename, and
-environment name. A mismatch there surfaces as a `403` at upload time, after the
-tag has already been pushed.
-
-### 8.3 Quick E2E Test Summary
-
-| What to test | Command | What to watch |
-| ---------------------- | -------------------------------------------------------------------- | ------------------------------------ |
-| Staging only | `git push origin release/v0.0.0-test` | 3 jobs: build → TestPyPI → verify |
-| Full release | `git push origin v0.0.0-test.1` | Publishes to PyPI. No undo. Avoid. |
-| Cleanup (branch) | `git push origin --delete release/v0.0.0-test` | Branch removed |
-| Cleanup (tag) | `git push origin --delete v0.0.0-test.1 && git tag -d v0.0.0-test.1` | Tag removed, PyPI version remains |
-
-### 8.4 Testing Workflow Changes on a Feature Branch
-
-If you're modifying the workflow files on a feature branch (not yet merged to `develop`), you can still test them:
-
-```bash
-# Your workflow changes are on feature/my-ci-changes
-git checkout feature/my-ci-changes
-
-# Create a test release branch directly from your feature branch
-git checkout -b release/v0.0.0-test
-git push origin release/v0.0.0-test
-
-# GitHub Actions uses the workflow files from the pushed branch,
-# so your modifications are what actually runs
-```
-
-This is useful because GitHub Actions reads workflow files from the branch being pushed, not from `main` or `develop`. Your modified workflows execute immediately without needing to merge first.
-
-After testing:
-
-```bash
-# Clean up
-git push origin --delete release/v0.0.0-test
-git checkout feature/my-ci-changes
-git branch -d release/v0.0.0-test
-```
-
-## 9. Production Release Pipeline (PyPI)
-
-The production pipeline publishes a final release to PyPI and creates a GitHub Release.
-
-**Workflow file**: `.github/workflows/release.yml`
-
-**Trigger**: Push a `v*` tag (e.g. `v0.2.0`)
-
-### Pipeline Flow
-
-```mermaid
-flowchart TD
- tag(["push tag v0.2.0"])
- build["_build tests + package Version: 0.2.0 (clean, from tag)"]
- publishTest["publish-testpypi Final TestPyPI upload (clean version)"]
- verifyTest["verify-testpypi Smoke test from TestPyPI"]
- publishPypi["publish-pypi Publishes to PyPI immediately Trusted Publishing (OIDC, no token) environment: release (no protection rules)"]
- ghRelease["github-release Creates GitHub Release with artifacts Auto-generated release notes"]
-
- tag --> build --> publishTest --> verifyTest --> publishPypi --> ghRelease
-
- classDef gate fill:#fff3cd,stroke:#856404,color:#000;
- class tag gate;
-```
-
-> **Pushing the tag is the point of no return.** The `publish-pypi` job declares
-> `environment: release`, but that environment currently has **no protection
-> rules**, so nothing pauses for review. Verify for yourself:
->
-> ```bash
-> gh api repos/Azure/agentops/environments --jq '.environments[] | {name, protection_rules}'
-> ```
->
-> PyPI does not allow re-uploading a version, so a bad release can only be
-> yanked, never replaced. Do all your verification on TestPyPI (staging) before
-> you tag. See [Enabling a real approval gate](#enabling-a-real-approval-gate)
-> if you want the pipeline to stop for a human.
-
-### Step-by-Step: Cutting a Release
-
-#### Step 1: Cut the Release (One-Click)
-
-1. Go to the **Actions** tab → select **Cut Release** workflow
-2. Click **Run workflow**
-3. Enter the version (e.g. `0.2.0`) - no `v` prefix
-4. Click **Run workflow**
-
-The workflow automatically:
-- Creates `release/v0.2.0` from `develop`
-- Updates `CHANGELOG.md` (adds versioned section `[0.2.0] - YYYY-MM-DD`)
-- Pushes the branch (triggers [staging pipeline](#7-staging-pipeline-testpypi))
-- Opens a PR: `release/v0.2.0` → `main`
-
-> **Alternative (manual)**: If you prefer to create the release branch locally:
-> ```bash
-> git checkout develop && git pull origin develop
-> git checkout -b release/v0.2.0
-> # Edit CHANGELOG.md manually
-> git commit -m "chore: prepare release 0.2.0"
-> git push origin release/v0.2.0
-> ```
-
-#### Step 2: Wait for Staging
-
-The branch push triggers the staging pipeline automatically. Wait for it to pass.
-
-#### Step 3: Monitor Staging
-
-1. Go to **Actions** tab → find the **Staging** workflow run
-2. Verify all 3 jobs pass:
- - ✅ `build / build` - tests pass, package builds
- - ✅ `publish-testpypi` - uploaded to TestPyPI
- - ✅ `verify-testpypi` - installed and smoke-tested
-
-If any job fails, fix the issue on the release branch and push. The pipeline re-runs automatically.
-
-#### Step 4: Merge to Main
-
-Create a PR from `release/v0.2.0` → `main` (or use the one already opened by Cut Release):
-
-1. Go to GitHub → **Pull Requests** → **New Pull Request**
-2. Base: `main` ← Compare: `release/v0.2.0`
-3. Title: `Release v0.2.0`
-4. Get the required reviews and merge
-
-#### Step 5: Tag the release **and** sync `develop`
-
-These are one step, not two. Tagging publishes to PyPI; syncing `develop` keeps
-the next release's CHANGELOG correct. Run all of it in one sitting.
-
-```bash
-# 1. Tag main. This publishes to PyPI with no approval prompt.
-git checkout main
-git pull origin main
-git tag v0.2.0
-git push origin v0.2.0
-
-# 2. Immediately sync main back into develop.
-git checkout develop
-git pull origin develop
-git merge main
-git push origin develop
-
-# 3. Verify the sync. This MUST print nothing.
-git fetch origin
-git log --oneline origin/develop..origin/main
-```
-
-If step 3 prints any commits, `develop` is behind `main` and the next release
-will be built from a stale CHANGELOG. Fix it before you walk away.
-
-**Why skipping the sync corrupts the next release.** `cut-release.yml` branches
-from `develop` and rewrites the changelog by replacing the `## [Unreleased]`
-marker exactly once, so everything under `Unreleased` becomes the new version's
-content. When `develop` is behind `main`:
-
-- `develop` still carries entries that already shipped, so they get republished
- under the new version.
-- `develop` has no `## [0.2.0]` heading at all, so merging the next release PR
- into `main` **deletes the `[0.2.0]` section** from the published changelog.
-
-**If you already skipped it**, do not trust a plain `git merge main`. Git places
-the incoming `## [0.2.0] - ` heading above the unreleased entries that
-`develop` accumulated in the same spot, which nests new unreleased work inside an
-already-published version. The result is valid Markdown and easy to miss in
-review. Open `CHANGELOG.md` after the merge and confirm that everything under
-`## [Unreleased]` is genuinely unreleased before pushing.
-
-#### Step 6: Watch the release pipeline
-
-1. Go to **Actions** tab → find the **Release** workflow run for `v0.2.0`
-2. The pipeline runs build → TestPyPI → verify → **publish-pypi** → github-release
-3. `publish-pypi` does not pause. It publishes to PyPI via
- [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) using the
- workflow's OIDC identity, so there is no API token to rotate
-4. `github-release` then creates a GitHub Release with the built artifacts and
- auto-generated release notes
-
-If the run fails after `publish-pypi` succeeded, the package is already on PyPI.
-Fix forward with a new patch version rather than retrying the tag.
-
-##### Enabling a real approval gate
-
-The `release` environment exists and is referenced by the workflow, but it has no
-reviewers attached, so it is a label rather than a gate. To make the pause real,
-a repo admin adds required reviewers:
-
-**Settings → Environments → `release` → Required reviewers**, then confirm:
-
-```bash
-gh api repos/Azure/agentops/environments/release --jq '.protection_rules'
-```
-
-Once reviewers exist, `publish-pypi` stops on **"Waiting for review"** and a
-reviewer approves via **Review deployments → release → Approve and deploy**. No
-workflow change is needed; `environment: release` is already declared.
-
-#### Step 7: Delete the release branch
-
-```bash
-git push origin --delete release/v0.2.0
-git branch -d release/v0.2.0
-```
-
-#### Step 8: Verify the Published Package
-
-```bash
-# Install from PyPI
-pip install agentops-accelerator==0.2.0
-
-# Verify
-agentops --version # Should show 0.2.0
-agentops --help
-```
-
-Check the published package:
-- PyPI: https://pypi.org/project/agentops-accelerator/0.2.0/
-- GitHub Release: https://github.com/Azure/agentops/releases/tag/v0.2.0
-
-## 10. Infrastructure Setup
-
-This section covers one-time setup required before the pipelines can run.
-
-### 10.1 GitHub Environments
-
-Create two environments in **Settings → Environments → New environment**:
-
-#### `staging` Environment
-
-- **Purpose**: Controls access to TestPyPI publishing
-- **Protection rules**: None
-- **Secrets**: None. `staging.yml` requests `id-token: write` and uploads via Trusted Publishing.
-
-#### `release` Environment
-
-- **Purpose**: Scopes the PyPI publish to a named environment for Trusted Publishing
-- **Protection rules**: **None today.** The environment is declared by `release.yml`
- but has no reviewers, so `publish-pypi` runs without pausing. To turn it into a
- real gate, add required reviewers (see
- [Enabling a real approval gate](#enabling-a-real-approval-gate)).
-- **Deployment branches**: Optionally restrict to `main` branch and `v*` tags
-- **Secrets**: None. `VSCE_PAT` is a **repository** secret, not an environment secret,
- so it resolves in both `staging.yml` and `release.yml` without being attached here.
-
-#### Repository secrets
-
-| Secret | Value | How to get it |
-| ------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------- |
-| `VSCE_PAT` | VS Code Marketplace PAT with **Marketplace: Manage** | [dev.azure.com](https://dev.azure.com) → User settings → Personal access tokens |
-| `RELEASE_PAT`| PAT used by `cut-release.yml` to open the release PR | GitHub → Settings → Developer settings → Personal access tokens |
-
-No PyPI API token is stored. Check the current rules and secret locations at any time:
-
-```bash
-gh api repos/Azure/agentops/environments/release --jq '.protection_rules'
-gh api repos/Azure/agentops/environments/release/secrets --jq '.secrets[].name'
-gh api repos/Azure/agentops/actions/secrets --jq '.secrets[].name'
-```
-
-### 10.2 PyPI and TestPyPI Trusted Publishing
-
-Both `staging.yml` and `release.yml` use
-[PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/), so uploads
-are authenticated with a short-lived OIDC token minted by GitHub Actions. There
-are no API tokens to create, store, or rotate.
-
-Configure it once per index, on the index side:
-
-#### TestPyPI (Staging)
-
-1. Log in at [test.pypi.org](https://test.pypi.org/) (a separate account from PyPI)
-2. Go to the project → **Manage → Publishing → Add a new publisher → GitHub**
-3. Owner `Azure`, repository `agentops`, workflow `staging.yml`, environment `staging`
-
-#### PyPI (Production)
-
-1. Log in at [pypi.org](https://pypi.org/)
-2. Go to the project → **Manage → Publishing → Add a new publisher → GitHub**
-3. Owner `Azure`, repository `agentops`, workflow `release.yml`, environment `release`
-
-The workflow filename and environment name must match exactly. A mismatch fails
-at upload time with `403 Invalid or non-existent authentication information`,
-which on the release pipeline happens *after* the tag is already pushed.
-
-> **Note**: TestPyPI and PyPI are completely separate systems with separate accounts and namespaces. A publisher configured on one does not apply to the other.
-
-### 10.3 First-Time Package Registration
-
-Trusted Publishing cannot create a project that does not exist yet. For a brand
-new project name, either upload once manually with a temporary API token, or use
-[PyPI's pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
-flow to reserve the name for the workflow. `agentops-accelerator` is already
-registered on both indexes, so this only matters if the package is renamed.
-
-## 11. Workflow File Reference
-
-All workflow files are in `.github/workflows/`:
-
-### `ci.yml` - Continuous Integration
-
-```
-Trigger: push to develop, PR to develop
-Flow: lint → test (matrix) → coverage
- + on develop push: publish-dev → verify-dev (TestPyPI)
-Purpose: Quality gate for all code changes; auto-publish dev builds
-```
-
-Key detail: `publish-dev` and `verify-dev` only run on pushes to `develop` (not PRs). Every merge to develop produces a dev version on TestPyPI (e.g. `0.1.3.dev12`) via setuptools-scm. PRs to `main` are not covered by CI because they come from `release/*` branches which are already validated by the staging pipeline.
-
-### `_build.yml` - Reusable Build
-
-```
-Trigger: workflow_call (called by staging.yml and release.yml)
-Flow: checkout (full history) → uv sync → pytest → uv build → upload artifact
-Purpose: Single source of truth for the build process
-```
-
-Key detail: Uses `fetch-depth: 0` to ensure setuptools-scm has full git history for version derivation.
-
-### `staging.yml` - Staging Pipeline
-
-```
-Trigger: push to release/* branches, or workflow_dispatch
-Flow: _build → publish-testpypi → verify-testpypi
-Purpose: Validate release candidates before production
-```
-
-Key details:
-- `skip-existing: true` allows re-pushes without upload failures
-- Verify step uses a retry loop (5 attempts, 30s apart) for TestPyPI index propagation
-- Smoke tests cover `--version`, `--help`, and `agentops init`
-
-### `release.yml` - Production Release
-
-```
-Trigger: push v* tags, or workflow_dispatch
-Flow: _build → publish-testpypi → verify-testpypi → publish-pypi → github-release
-Purpose: Publish to PyPI and create GitHub Release
-```
-
-Key details:
-- `publish-pypi` declares `environment: release`, but that environment has no protection rules, so it publishes without pausing
-- PyPI upload uses Trusted Publishing (`id-token: write`), not an API token
-- `github-release` uses `gh release create` with `--generate-notes` for automatic release notes
-- Built artifacts (.whl, .tar.gz) are attached to the GitHub Release
-
-### `cut-release.yml` - Cut Release (Manual Dispatch)
-
-```
-Trigger: workflow_dispatch (manual button in Actions tab)
-Input: version - semver string (e.g. 0.2.0)
-Flow: validate → check [Unreleased] not empty → create release branch → update CHANGELOG → push → open PR
-Purpose: One-click release branch creation from develop
-```
-
-Key details:
-- Creates `release/v` branch from `develop`
-- Automatically updates `CHANGELOG.md` - inserts a versioned section `[] - ` at the top
-- Opens a PR from `release/v` → `main` with a checklist
-- The branch push triggers `staging.yml` automatically
-- Fails safely if the branch already exists
-- Refuses to run when `## [Unreleased]` is empty, because this workflow only inserts a versioned heading beneath that one and would otherwise publish an empty release section
-- Does NOT auto-tag or auto-publish - tagging remains a manual, intentional step
-
-## 12. Release Checklist
-
-Use this checklist when cutting a release:
-
-**Preparation**
-- [ ] All intended features/fixes are merged to `develop`
-- [ ] `CHANGELOG.md` has entries under `## [Unreleased]` for all user-visible changes, including anything Dependabot merged (Cut Release aborts if the section is empty)
-- [ ] Tests pass locally: `uv run pytest tests/ -x -q`
-- [ ] Version from setuptools-scm looks correct: `python -m setuptools_scm`
-
-**Staging**
-- [ ] Release branch created via **Cut Release** workflow (or manually)
-- [ ] CHANGELOG automatically updated with version and date
-- [ ] Staging pipeline passes: build + TestPyPI + verify (all 3 green)
-- [ ] PR opened: `release/v0.X.Y` → `main`
-
-**Production (tag + sync, do these together)**
-- [ ] PR from `release/v0.X.Y` → `main` created and approved
-- [ ] PR merged to `main`
-- [ ] Version tag created and pushed: `v0.X.Y` (this publishes to PyPI immediately)
-- [ ] Release pipeline runs: build + TestPyPI + verify + publish-pypi all green
-- [ ] **`main` merged back into `develop` and pushed**
-- [ ] **`git log --oneline origin/develop..origin/main` prints nothing**
-- [ ] `CHANGELOG.md` on `develop` shows only genuinely unreleased work under `## [Unreleased]`
-- [ ] GitHub Release created with artifacts
-- [ ] Published package verified: `pip install agentops-accelerator==0.X.Y`
-
-**Cleanup**
-- [ ] Release branch deleted (remote and local)
-
-## 13. Troubleshooting
-
-### Build Failures
-
-| Problem | Cause | Solution |
-| ---------------------------------------- | ----------------------------------- | --------------------------------------------- |
-| `setuptools_scm` can't determine version | Shallow clone (missing git history) | Ensure `fetch-depth: 0` in checkout step |
-| Version shows `0.0.0` locally | Not in a git repo or no tags exist | Run `git tag v0.0.1` to create an initial tag |
-| `ModuleNotFoundError` in tests | Dependencies not installed | Run `uv sync --group dev` |
-| Tests fail on Windows but pass on Linux | Path separator issues | Use `pathlib.Path`, not string concatenation |
-
-### TestPyPI Issues
-
-| Problem | Cause | Solution |
-| --------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
-| Upload fails with 403 | Trusted Publishing not configured for `staging.yml` / environment `staging` | Fix the publisher on test.pypi.org under **Manage → Publishing** |
-| Upload fails with "already exists" | Same version previously uploaded | Normal - `skip-existing: true` handles this. If you need a new upload, push another commit to increment the dev version |
-| Install fails with "no matching distribution" | Package not yet indexed | The verify job retries automatically (5 attempts, 30s apart). If persistent, check TestPyPI status |
-| Install fails with dependency errors | Dependency not on TestPyPI | Verify `--extra-index-url https://pypi.org/simple/` is present |
-
-### PyPI Issues
-
-| Problem | Cause | Solution |
-| ------------------------------------------ | ----------------------------------------- | -------------------------------------------------------------- |
-| Published to PyPI without being asked | Expected. `release` has no protection rules, so `publish-pypi` never pauses | Yank the release on pypi.org and ship a new patch version. See [Enabling a real approval gate](#enabling-a-real-approval-gate) |
-| Publish step stuck on "Waiting for review" | Someone added required reviewers to `release` | A listed reviewer approves via **Review deployments → release** |
-| Upload fails with 403 | Trusted Publishing not configured for `release.yml` / environment `release` | Fix the publisher on pypi.org under **Manage → Publishing**. The tag is already pushed, so bump the version and retag |
-| Version already exists on PyPI | Tag points to an already-released version | PyPI versions are immutable. You must use a new version number |
-
-### Git and Version Issues
-
-| Problem | Cause | Solution |
-| ------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------ |
-| Wrong version in built package | Tag not on the expected commit | Verify with `git log --oneline --decorate` that the tag is where you expect |
-| `pip install -e .` fails | `.git` directory missing | Editable installs need git history for setuptools-scm. Clone the repo, don't just download a zip |
-| Merge conflicts between release and develop | Normal for concurrent work | Resolve conflicts on the release branch before merging to main |
-| Next release's CHANGELOG republishes old entries, or drops the previous version's section | `develop` was left behind `main` after the last release | `git merge main` into `develop`, then hand-check `CHANGELOG.md`. See [Step 5](#step-5-tag-the-release-and-sync-develop) |
-
-### Environment and Permissions
-
-| Problem | Cause | Solution |
-| --------------------------------- | ----------------------------------- | ---------------------------------------------------------------------- |
-| "Environment not found" error | GitHub Environment not created | Create `staging` and `release` environments in Settings → Environments |
-| "Secret not found" error | Secret not added to the environment | Add secrets to the specific environment, not repository-level secrets |
-| No one was asked to approve the publish | `release` has no required reviewers | Confirm with `gh api repos/Azure/agentops/environments/release --jq '.protection_rules'` |
-| Reviewer can't approve deployment | Not listed as required reviewer | Update the environment's required reviewers list |
-
-## Architecture Diagram
-
-```mermaid
-flowchart TD
- feat["feature/*"] -->|PR| develop(["develop"])
- develop --> ci["CI (ci.yml) lint + test + coverage publish-dev → TestPyPI (dev version)"]
- develop --> cut{{"Cut Release (cut-release.yml) manual dispatch - enter version"}}
- cut --> rel(["release/v0.2.0"])
-
- rel --> stagingBuild["_build test + build"]
- stagingBuild --> stagingTest["TestPyPI publish"]
- stagingTest --> stagingVerify["Verify install"]
-
- rel -->|PR| main(["main"])
- main -->|tag| tag(["v0.2.0"])
-
- tag --> relBuild["_build"]
- relBuild --> relTest["TestPyPI"]
- relTest --> relVerify["Verify"]
- relVerify --> relPypi["PyPI (no approval gate)"]
- relPypi --> relGh["GitHub Release"]
-
- main -->|merge back, REQUIRED| develop
-
- subgraph Staging["Staging (staging.yml)"]
- stagingBuild
- stagingTest
- stagingVerify
- end
-
- subgraph Release["Release (release.yml)"]
- relBuild
- relTest
- relVerify
- relPypi
- relGh
- end
-
- classDef gate fill:#fff3cd,stroke:#856404,color:#000;
- class cut,tag gate;
-```
+# GitOps Guide: Building and Releasing AgentOps Toolkit
+
+This guide is a comprehensive instruction manual for engineers working on the **agentops-accelerator** project. It covers the full GitOps lifecycle - from setting up your development environment, through the branching model and CI pipeline, to staging and production releases.
+
+## Table of Contents
+
+- [1. GitOps Principles](#1-gitops-principles)
+- [2. Branching Model](#2-branching-model)
+- [3. Development Environment Setup](#3-development-environment-setup)
+- [4. Development Workflow](#4-development-workflow)
+- [5. CI Pipeline (Continuous Integration)](#5-ci-pipeline-continuous-integration)
+- [6. Versioning with setuptools-scm](#6-versioning-with-setuptools-scm)
+- [7. Staging Pipeline (TestPyPI)](#7-staging-pipeline-testpypi)
+- [8. End-to-End Pipeline Testing](#8-end-to-end-pipeline-testing)
+- [9. Production Release Pipeline (PyPI)](#9-production-release-pipeline-pypi)
+- [10. Infrastructure Setup](#10-infrastructure-setup)
+- [11. Workflow File Reference](#11-workflow-file-reference)
+- [12. Release Checklist](#12-release-checklist)
+- [13. Troubleshooting](#13-troubleshooting)
+
+## 1. GitOps Principles
+
+AgentOps follows GitOps practices where **git is the single source of truth** for both code and operational state:
+
+- **Declarative configuration** - All pipeline behavior is defined in YAML workflow files checked into the repository.
+- **Version-controlled releases** - Every release is traceable to a git tag. No manual version edits.
+- **Automated pipelines** - Pushing branches or tags triggers the corresponding workflow automatically.
+- **Keyless publishing** - PyPI uploads use Trusted Publishing (OIDC). There is no PyPI API token to store or rotate.
+- **Immutable artifacts** - Built packages are uploaded once and reused across pipeline stages (no rebuilds between TestPyPI and PyPI).
+
+## 2. Branching Model
+
+AgentOps uses a modified [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) strategy:
+
+```
+main ← always production-ready, receives merges from release/* branches
+ │
+develop ← integration branch, all feature PRs target here
+ │
+ ├── feature/* ← individual features branched from develop
+ │
+ └── release/* ← release preparation, branched from develop when ready to ship
+```
+
+### Branch Purposes
+
+| Branch | Purpose | Who creates | Merges into |
+| ---------------- | -------------------------------------------------------------------- | ---------------- | ----------------------------- |
+| `main` | Production-ready code. Every commit here should be a tagged release. | Maintainers only | - |
+| `develop` | Integration branch. All feature work flows through here. | - | `main` (via release branches) |
+| `feature/*` | Individual features, bug fixes, or improvements. | Any contributor | `develop` |
+| `release/v0.X.Y` | Release stabilization and staging. Triggers TestPyPI pipeline. | Maintainers | `main` |
+
+### Branch Lifecycle
+
+```
+1. feature/my-change ──PR──→ develop (contributor)
+2. develop ──branch──→ release/v0.2.0 (maintainer, when ready to release)
+3. release/v0.2.0 ──PR──→ main (maintainer, after staging validates)
+4. main ──tag──→ v0.2.0 (maintainer, publishes to PyPI immediately)
+5. main ──merge──→ develop (maintainer, REQUIRED, same sitting as step 4)
+6. release/v0.2.0 ──delete── (maintainer, cleanup)
+```
+
+Steps 4 and 5 are a single unit of work. Leaving `develop` behind `main` corrupts
+the next release's CHANGELOG. See
+[Step 5: Tag the release and sync develop](#step-5-tag-the-release-and-sync-develop).
+
+### Branch Protection Rules (Recommended)
+
+Configure these in **Settings → Branches → Branch protection rules**:
+
+| Branch | Rules |
+| ----------- | ------------------------------------------------------------------------ |
+| `main` | Require PR, require status checks (CI), require approvals, no force push |
+| `develop` | Require PR, require status checks (CI), no force push |
+| `release/*` | Require status checks (Staging pipeline), no force push |
+
+## 3. Development Environment Setup
+
+### Prerequisites
+
+- Python 3.11 or later
+- [uv](https://docs.astral.sh/uv/) (recommended) or pip
+- Git with access to the repository
+
+### First-Time Setup
+
+```bash
+# 1. Clone the repository
+git clone https://github.com/Azure/agentops.git
+cd agentops
+
+# 2. Install uv (if not already installed)
+# macOS/Linux:
+curl -LsSf https://astral.sh/uv/install.sh | sh
+# Windows:
+powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
+
+# 3. Install the project and dev dependencies
+uv sync --group dev
+
+# 4. Verify the installation
+uv run agentops --version
+uv run pytest tests/ -x -q
+```
+
+### Alternative Setup (pip)
+
+```bash
+python -m venv .venv
+# Windows:
+.venv\Scripts\Activate.ps1
+# macOS/Linux:
+source .venv/bin/activate
+
+pip install -e .
+pip install pytest
+agentops --version
+python -m pytest tests/ -x -q
+```
+
+### Verify Your Setup
+
+After installation, these commands should all succeed:
+
+```bash
+# CLI works
+agentops --version # Shows version like 0.1.3.dev6
+agentops --help # Shows available commands
+
+# Tests pass
+uv run pytest tests/ -x -q # All tests should pass
+
+# Version from git
+python -m setuptools_scm # Shows version derived from git tags
+```
+
+## 4. Development Workflow
+
+### Creating a Feature
+
+```bash
+# 1. Start from the latest develop
+git checkout develop
+git pull origin develop
+
+# 2. Create your feature branch
+git checkout -b feature/my-new-feature
+
+# 3. Make changes, commit, push
+# ... edit files ...
+uv run pytest tests/ -x -q # Run tests before committing
+git add .
+git commit -m "feat: add my new feature"
+git push origin feature/my-new-feature
+
+# 4. Open a PR targeting develop
+# GitHub will run the CI pipeline automatically
+```
+
+### PR Requirements
+
+Before your PR can be merged to `develop`:
+
+1. **CI pipeline passes** - lint + tests across OS/Python matrix
+2. **Code review approved** - at least one reviewer
+3. **Architecture rules followed** - see [CONTRIBUTING.md](https://github.com/Azure/agentops/blob/main/CONTRIBUTING.md)
+4. **Tests included** - unit tests in `tests/unit/`, integration tests if needed
+5. **CHANGELOG updated** - add an entry under `## [Unreleased]` for user-visible changes. The `changelog` CI job enforces this; see [The CHANGELOG guard](#the-changelog-guard) below.
+
+### After Your PR is Merged
+
+```bash
+# Sync your local develop
+git checkout develop
+git pull origin develop
+
+# Delete your feature branch
+git branch -d feature/my-new-feature
+```
+
+## 5. CI Pipeline (Continuous Integration)
+
+The CI pipeline runs on **every push and PR** to `main` or `develop`.
+
+**Workflow file**: `.github/workflows/ci.yml`
+
+### Jobs
+
+| Job | What it does | Runs on |
+| --- | --- | --- |
+| **lint** | `ruff check` (linting) + `mypy` (type checking, soft-fail) | Ubuntu, Python 3.11 |
+| **changelog** | Fails a PR that changes shipped code without an `## [Unreleased]` entry | Ubuntu, PRs only |
+| **test** | `pytest tests/` with JUnit XML output | Matrix: 2 OS × 3 Python versions |
+| **coverage** | `pytest --cov` with XML coverage report | Ubuntu, Python 3.13 (after tests pass) |
+| **publish-dev** | Build package + publish to TestPyPI (develop pushes only) | Ubuntu, Python 3.12 (after lint + test pass) |
+| **verify-dev** | Install from TestPyPI + smoke test (develop pushes only) | Ubuntu, Python 3.12 (after publish-dev) |
+
+The `publish-dev` and `verify-dev` jobs only run on pushes to `develop` (not on PRs). Every merged PR automatically produces an installable dev build on TestPyPI with a version like `0.1.3.dev12`.
+
+### Test Matrix
+
+| OS | Python 3.11 | Python 3.12 | Python 3.13 |
+| ------- | ----------- | ----------- | ----------- |
+| Ubuntu | ✅ | ✅ | ✅ |
+| Windows | ✅ | ✅ | ✅ |
+
+### What CI Catches
+
+- Syntax and style issues (ruff)
+- Type errors (mypy, non-blocking)
+- Test failures across platforms
+- Import errors or missing dependencies
+- Regression in exit code behavior
+- User-visible changes shipped without a CHANGELOG entry
+
+### Viewing CI Results
+
+1. Go to the **Actions** tab → find the CI run for your PR
+2. Click into a failing job to see the error
+3. Download test result artifacts if needed
+
+### The CHANGELOG guard
+
+`cut-release.yml` does not write changelog content. It inserts a `## [X.Y.Z] - ` heading directly beneath `## [Unreleased]` and nothing more, leaving `[Unreleased]` in place and empty. If no PR wrote anything under `[Unreleased]` during the cycle, the published release section is empty and the release pipeline still goes green. Releases 0.8.4 and 0.8.5 both shipped that way and were backfilled by hand afterwards, between them hiding six bug fixes and six dependency bumps.
+
+Two jobs now close that gap, both driven by `scripts/check_changelog.py`:
+
+- The **`changelog`** job in `ci.yml` runs on every PR to `develop`.
+- A **`check-unreleased`** step in `cut-release.yml` aborts the release before the branch is created if `[Unreleased]` is empty. `scripts/cut-release.sh` and `scripts/cut-release.ps1` run the same check at the same point, so the local path cannot skip it.
+
+#### When the PR check requires an entry
+
+The PR must add a bullet under `## [Unreleased]` when **both** hold:
+
+1. The diff touches a file that ships. Changes confined to `docs/`, `tests/`, `.github/workflows/`, `.github/ISSUE_TEMPLATE/`, `.vscode/`, `media/`, `tombstones/`, or the top-level markdown files never require an entry, whatever the PR is titled.
+2. The PR title carries a user-visible conventional-commit type (`feat`, `fix`, `perf`, `revert`), is marked breaking (`feat!:` or a `BREAKING CHANGE` footer), or has no recognisable type at all. A typed `docs:`, `test:`, `ci:`, `build:`, `style:`, `refactor:`, or `chore:` PR is not asked for an entry.
+
+An untyped title is treated as needing an entry on purpose. A PR that edits shipped code and says nothing about its intent is exactly the case worth a second look.
+
+#### Where the entry has to go
+
+The check parses the CHANGELOG diff and resolves each added line to the section it lands in. A bullet added under an already-released heading fails the same as no bullet at all, because `cut-release.yml` only ever promotes `[Unreleased]`. A bare `### Fixed` subheading with no bullet under it does not count either.
+
+#### Bypassing the check
+
+Apply the **`no-changelog`** label to the PR. The job then reports why it skipped and passes. Use it for changes that genuinely cannot matter to a user of the published package, and say so in the PR description so the reviewer can disagree.
+
+#### Dependabot
+
+Dependabot PRs are exempt. The bot cannot act on a failing check, so requiring an entry would leave every dependency PR red until a human labelled it, which trains everyone to reach for `no-changelog` reflexively. That is not the same as saying dependency bumps do not belong in the changelog: the `cryptography` 48 to 50 and `mcp` 1.27.1 to 1.28.1 bumps in 0.8.5 mattered to readers. Cover them when you cut the release, where one person writes one summary line instead of twelve bots writing twelve.
+
+Nothing enforces that today. `check-unreleased` only asserts that `[Unreleased]` is non-empty, and a single bullet from any PR satisfies it, so a cycle can still reach a tag with its dependency bumps undocumented. Closing that gap properly means reading the merged Dependabot PRs for the cycle, which is a separate change.
+
+#### Running it locally
+
+```bash
+# Is the Unreleased section empty?
+python scripts/check_changelog.py check-unreleased
+
+# Would my branch pass the PR check?
+PR_TITLE="fix: something" PR_AUTHOR="$USER" PR_LABELS='[]' \
+ python scripts/check_changelog.py check-pr --base origin/develop
+```
+
+## 6. Versioning with setuptools-scm
+
+AgentOps uses [setuptools-scm](https://github.com/pypa/setuptools-scm) for **fully automatic versioning**. There is **no `version` field in `pyproject.toml`** - the version is derived from git tags at build time.
+
+### How It Works
+
+setuptools-scm reads your git history and computes the version:
+
+| Git state | Example version | Explanation |
+| --------------------------------------------- | --------------- | ----------------------------- |
+| Exactly on tag `v0.2.0` | `0.2.0` | Clean release version |
+| 3 commits after `v0.2.0` | `0.2.1.dev3` | Dev version, 3 commits ahead |
+| 10 commits after `v0.1.2` on `release/v0.2.0` | `0.1.3.dev10` | Dev version on release branch |
+
+### Configuration
+
+In `pyproject.toml`:
+
+```toml
+[build-system]
+requires = ["setuptools>=68", "wheel", "setuptools-scm>=8"]
+
+[project]
+dynamic = ["version"] # Version comes from setuptools-scm, not a static field
+
+[tool.setuptools_scm]
+local_scheme = "no-local-version" # Strips +hash suffix (PyPI rejects local versions)
+```
+
+### Checking the Version
+
+```bash
+# From the installed CLI
+agentops --version
+
+# From setuptools-scm directly
+python -m setuptools_scm
+
+# From Python code
+python -c "from agentops import __version__; print(__version__)"
+```
+
+### Rules
+
+- **Never add `version = "..."` to `pyproject.toml`** - this will conflict with setuptools-scm.
+- **Tags must follow PEP 440** - use `v0.2.0`, not `release-0.2.0` or `0.2.0`.
+- **`fetch-depth: 0`** is required in CI checkout steps - setuptools-scm needs the full git history.
+- **`pip install -e .` requires `.git`** - editable installs need the git directory present (standard for development).
+
+## 7. Staging Pipeline (TestPyPI)
+
+The staging pipeline validates a release candidate by publishing to TestPyPI and verifying the installed package works.
+
+**Workflow file**: `.github/workflows/staging.yml`
+
+**Trigger**: Push to any `release/*` branch
+
+### Pipeline Flow
+
+```mermaid
+flowchart TD
+ push(["push to release/v0.2.0"])
+ build["_build tests + package Version: 0.2.1.dev3 (setuptools-scm)"]
+ publish["publish-testpypi Upload to TestPyPI (staging environment) Trusted Publishing (OIDC, no token)"]
+ verify["verify-testpypi Install from TestPyPI in fresh environment agentops --version / --help / init"]
+
+ push --> build --> publish --> verify
+```
+
+### What Gets Validated
+
+1. **Tests pass** - the full test suite runs before building
+2. **Package builds** - setuptools-scm generates the correct version, wheel and sdist are created
+3. **Package uploads** - the built artifacts successfully upload to TestPyPI
+4. **Package installs** - `pip install` from TestPyPI resolves all dependencies
+5. **CLI works** - `agentops --version` and `--help` run without errors
+6. **Init works** - `agentops init` creates the expected workspace files
+
+### Iterating on a Release Branch
+
+If staging fails, fix the issue and push again:
+
+```bash
+# On your release/v0.2.0 branch
+# ... fix the issue ...
+git add .
+git commit -m "fix: correct packaging issue"
+git push origin release/v0.2.0
+# Staging pipeline re-runs automatically
+```
+
+Each push generates a new dev version (e.g. `0.2.1.dev4`, `0.2.1.dev5`), so there are no version conflicts on TestPyPI. The `skip-existing: true` flag also prevents failures if the same version is re-uploaded.
+
+### Manual Verification (Optional)
+
+After the staging pipeline passes, you can manually test the package:
+
+```bash
+# Install the specific dev version from TestPyPI
+pip install "agentops-accelerator==0.2.1.dev3" \
+ --index-url https://test.pypi.org/simple/ \
+ --extra-index-url https://pypi.org/simple/
+
+agentops --version
+agentops --help
+
+# Test init in a temp directory
+cd $(mktemp -d)
+agentops init
+ls .agentops/
+```
+
+> **Note**: `--extra-index-url https://pypi.org/simple/` is required so that dependencies (typer, pydantic, ruamel.yaml) resolve from the real PyPI.
+
+## 8. End-to-End Pipeline Testing
+
+Before cutting a real release, you can validate the entire pipeline end-to-end using a disposable test branch and tag. This is especially useful when:
+
+- You've modified any workflow file (`_build.yml`, `staging.yml`, `release.yml`)
+- You've changed `pyproject.toml` build configuration
+- You've updated setuptools-scm settings
+- A new engineer wants to understand the release process hands-on
+
+### 8.1 Test the Staging Pipeline
+
+#### Step 1: Create a Test Release Branch
+
+From the branch that contains your workflow changes (or from `develop`):
+
+```bash
+git checkout develop # or your feature branch with workflow changes
+git pull origin develop
+git checkout -b release/v0.0.0-test
+git push origin release/v0.0.0-test
+```
+
+This triggers the `staging.yml` workflow automatically.
+
+#### Step 2: Monitor the Pipeline
+
+1. Go to **Actions** tab → find the **Staging** workflow run for `release/v0.0.0-test`
+2. Watch all 3 jobs:
+
+```
+Job 1: build / build → Should tests pass? Package build?
+Job 2: publish-testpypi → Does TestPyPI upload succeed?
+Job 3: verify-testpypi → Can the package install and run?
+```
+
+3. Click into each job to inspect step-level output
+4. If a job fails, read the logs, fix the issue, push again:
+
+```bash
+# Fix and re-push
+git add .
+git commit -m "fix: correct workflow issue"
+git push origin release/v0.0.0-test
+# Pipeline re-runs automatically
+```
+
+#### Step 3: Verify on TestPyPI (Optional)
+
+Confirm the test package appeared on TestPyPI:
+
+```bash
+# Check the version that was published
+python -m setuptools_scm
+
+# Install and test manually
+pip install "agentops-accelerator==$(python -m setuptools_scm)" \
+ --index-url https://test.pypi.org/simple/ \
+ --extra-index-url https://pypi.org/simple/
+
+agentops --version
+agentops --help
+
+# Test init
+cd $(mktemp -d)
+agentops init
+ls .agentops/
+```
+
+#### Step 4: Clean Up the Test Branch
+
+```bash
+# Delete remote branch
+git push origin --delete release/v0.0.0-test
+
+# Switch back and delete local branch
+git checkout develop
+git branch -d release/v0.0.0-test
+```
+
+### 8.2 Test the Full Release Pipeline
+
+> **There is no safe dry run.** The `publish-pypi` job does not pause, so pushing
+> any `v*` tag publishes that version to real PyPI. There is no reject button to
+> catch it. PyPI versions cannot be deleted, only yanked, so a throwaway
+> `v0.0.0-test.1` tag leaves a permanent artifact on the project page.
+
+Test everything except the final publish by pushing a `release/v*` branch, which
+exercises build → TestPyPI → verify (see [8.1](#81-test-the-staging-pipeline)).
+That covers every job the release pipeline runs before `publish-pypi`, using the
+same build and the same `pypa/gh-action-pypi-publish` action.
+
+If you genuinely need to validate `publish-pypi` end to end, add required
+reviewers to the `release` environment first (see
+[Enabling a real approval gate](#enabling-a-real-approval-gate)). With reviewers
+attached, the job pauses and you can reject it.
+
+#### Verifying the publish path without publishing
+
+```bash
+# Confirm the release environment's protection rules (empty = no gate).
+gh api repos/Azure/agentops/environments/release --jq '.protection_rules'
+
+# Confirm the workflow requests an OIDC token instead of using an API key.
+grep -n "id-token\|gh-action-pypi-publish" .github/workflows/release.yml
+```
+
+Trusted Publishing must also be configured on the PyPI side under
+**Manage project → Publishing**, matching the repository, workflow filename, and
+environment name. A mismatch there surfaces as a `403` at upload time, after the
+tag has already been pushed.
+
+### 8.3 Quick E2E Test Summary
+
+| What to test | Command | What to watch |
+| ---------------------- | -------------------------------------------------------------------- | ------------------------------------ |
+| Staging only | `git push origin release/v0.0.0-test` | 3 jobs: build → TestPyPI → verify |
+| Full release | `git push origin v0.0.0-test.1` | Publishes to PyPI. No undo. Avoid. |
+| Cleanup (branch) | `git push origin --delete release/v0.0.0-test` | Branch removed |
+| Cleanup (tag) | `git push origin --delete v0.0.0-test.1 && git tag -d v0.0.0-test.1` | Tag removed, PyPI version remains |
+
+### 8.4 Testing Workflow Changes on a Feature Branch
+
+If you're modifying the workflow files on a feature branch (not yet merged to `develop`), you can still test them:
+
+```bash
+# Your workflow changes are on feature/my-ci-changes
+git checkout feature/my-ci-changes
+
+# Create a test release branch directly from your feature branch
+git checkout -b release/v0.0.0-test
+git push origin release/v0.0.0-test
+
+# GitHub Actions uses the workflow files from the pushed branch,
+# so your modifications are what actually runs
+```
+
+This is useful because GitHub Actions reads workflow files from the branch being pushed, not from `main` or `develop`. Your modified workflows execute immediately without needing to merge first.
+
+After testing:
+
+```bash
+# Clean up
+git push origin --delete release/v0.0.0-test
+git checkout feature/my-ci-changes
+git branch -d release/v0.0.0-test
+```
+
+## 9. Production Release Pipeline (PyPI)
+
+The production pipeline publishes a final release to PyPI and creates a GitHub Release.
+
+**Workflow file**: `.github/workflows/release.yml`
+
+**Trigger**: Push a `v*` tag (e.g. `v0.2.0`)
+
+### Pipeline Flow
+
+```mermaid
+flowchart TD
+ tag(["push tag v0.2.0"])
+ build["_build tests + package Version: 0.2.0 (clean, from tag)"]
+ publishTest["publish-testpypi Final TestPyPI upload (clean version)"]
+ verifyTest["verify-testpypi Smoke test from TestPyPI"]
+ publishPypi["publish-pypi Publishes to PyPI immediately Trusted Publishing (OIDC, no token) environment: release (no protection rules)"]
+ ghRelease["github-release Creates GitHub Release with artifacts Auto-generated release notes"]
+
+ tag --> build --> publishTest --> verifyTest --> publishPypi --> ghRelease
+
+ classDef gate fill:#fff3cd,stroke:#856404,color:#000;
+ class tag gate;
+```
+
+> **Pushing the tag is the point of no return.** The `publish-pypi` job declares
+> `environment: release`, but that environment currently has **no protection
+> rules**, so nothing pauses for review. Verify for yourself:
+>
+> ```bash
+> gh api repos/Azure/agentops/environments --jq '.environments[] | {name, protection_rules}'
+> ```
+>
+> PyPI does not allow re-uploading a version, so a bad release can only be
+> yanked, never replaced. Do all your verification on TestPyPI (staging) before
+> you tag. See [Enabling a real approval gate](#enabling-a-real-approval-gate)
+> if you want the pipeline to stop for a human.
+
+### Step-by-Step: Cutting a Release
+
+#### Step 1: Cut the Release (One-Click)
+
+1. Go to the **Actions** tab → select **Cut Release** workflow
+2. Click **Run workflow**
+3. Enter the version (e.g. `0.2.0`) - no `v` prefix
+4. Click **Run workflow**
+
+The workflow automatically:
+- Creates `release/v0.2.0` from `develop`
+- Updates `CHANGELOG.md` (adds versioned section `[0.2.0] - YYYY-MM-DD`)
+- Pushes the branch (triggers [staging pipeline](#7-staging-pipeline-testpypi))
+- Opens a PR: `release/v0.2.0` → `main`
+
+> **Alternative (manual)**: If you prefer to create the release branch locally:
+> ```bash
+> git checkout develop && git pull origin develop
+> git checkout -b release/v0.2.0
+> # Edit CHANGELOG.md manually
+> git commit -m "chore: prepare release 0.2.0"
+> git push origin release/v0.2.0
+> ```
+
+#### Step 2: Wait for Staging
+
+The branch push triggers the staging pipeline automatically. Wait for it to pass.
+
+#### Step 3: Monitor Staging
+
+1. Go to **Actions** tab → find the **Staging** workflow run
+2. Verify all 3 jobs pass:
+ - ✅ `build / build` - tests pass, package builds
+ - ✅ `publish-testpypi` - uploaded to TestPyPI
+ - ✅ `verify-testpypi` - installed and smoke-tested
+
+If any job fails, fix the issue on the release branch and push. The pipeline re-runs automatically.
+
+#### Step 4: Merge to Main
+
+Create a PR from `release/v0.2.0` → `main` (or use the one already opened by Cut Release):
+
+1. Go to GitHub → **Pull Requests** → **New Pull Request**
+2. Base: `main` ← Compare: `release/v0.2.0`
+3. Title: `Release v0.2.0`
+4. Get the required reviews and merge
+
+#### Step 5: Tag the release **and** sync `develop`
+
+These are one step, not two. Tagging publishes to PyPI; syncing `develop` keeps
+the next release's CHANGELOG correct. Run all of it in one sitting.
+
+```bash
+# 1. Tag main. This publishes to PyPI with no approval prompt.
+git checkout main
+git pull origin main
+git tag v0.2.0
+git push origin v0.2.0
+
+# 2. Immediately sync main back into develop.
+git checkout develop
+git pull origin develop
+git merge main
+git push origin develop
+
+# 3. Verify the sync. This MUST print nothing.
+git fetch origin
+git log --oneline origin/develop..origin/main
+```
+
+If step 3 prints any commits, `develop` is behind `main` and the next release
+will be built from a stale CHANGELOG. Fix it before you walk away.
+
+**Why skipping the sync corrupts the next release.** `cut-release.yml` branches
+from `develop` and rewrites the changelog by replacing the `## [Unreleased]`
+marker exactly once, so everything under `Unreleased` becomes the new version's
+content. When `develop` is behind `main`:
+
+- `develop` still carries entries that already shipped, so they get republished
+ under the new version.
+- `develop` has no `## [0.2.0]` heading at all, so merging the next release PR
+ into `main` **deletes the `[0.2.0]` section** from the published changelog.
+
+**If you already skipped it**, do not trust a plain `git merge main`. Git places
+the incoming `## [0.2.0] - ` heading above the unreleased entries that
+`develop` accumulated in the same spot, which nests new unreleased work inside an
+already-published version. The result is valid Markdown and easy to miss in
+review. Open `CHANGELOG.md` after the merge and confirm that everything under
+`## [Unreleased]` is genuinely unreleased before pushing.
+
+#### Step 6: Watch the release pipeline
+
+1. Go to **Actions** tab → find the **Release** workflow run for `v0.2.0`
+2. The pipeline runs build → TestPyPI → verify → **publish-pypi** → github-release
+3. `publish-pypi` does not pause. It publishes to PyPI via
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) using the
+ workflow's OIDC identity, so there is no API token to rotate
+4. `github-release` then creates a GitHub Release with the built artifacts and
+ auto-generated release notes
+
+If the run fails after `publish-pypi` succeeded, the package is already on PyPI.
+Fix forward with a new patch version rather than retrying the tag.
+
+##### Enabling a real approval gate
+
+The `release` environment exists and is referenced by the workflow, but it has no
+reviewers attached, so it is a label rather than a gate. To make the pause real,
+a repo admin adds required reviewers:
+
+**Settings → Environments → `release` → Required reviewers**, then confirm:
+
+```bash
+gh api repos/Azure/agentops/environments/release --jq '.protection_rules'
+```
+
+Once reviewers exist, `publish-pypi` stops on **"Waiting for review"** and a
+reviewer approves via **Review deployments → release → Approve and deploy**. No
+workflow change is needed; `environment: release` is already declared.
+
+#### Step 7: Delete the release branch
+
+```bash
+git push origin --delete release/v0.2.0
+git branch -d release/v0.2.0
+```
+
+#### Step 8: Verify the Published Package
+
+```bash
+# Install from PyPI
+pip install agentops-accelerator==0.2.0
+
+# Verify
+agentops --version # Should show 0.2.0
+agentops --help
+```
+
+Check the published package:
+- PyPI: https://pypi.org/project/agentops-accelerator/0.2.0/
+- GitHub Release: https://github.com/Azure/agentops/releases/tag/v0.2.0
+
+## 10. Infrastructure Setup
+
+This section covers one-time setup required before the pipelines can run.
+
+### 10.1 GitHub Environments
+
+Create two environments in **Settings → Environments → New environment**:
+
+#### `staging` Environment
+
+- **Purpose**: Controls access to TestPyPI publishing
+- **Protection rules**: None
+- **Secrets**: None. `staging.yml` requests `id-token: write` and uploads via Trusted Publishing.
+
+#### `release` Environment
+
+- **Purpose**: Scopes the PyPI publish to a named environment for Trusted Publishing
+- **Protection rules**: **None today.** The environment is declared by `release.yml`
+ but has no reviewers, so `publish-pypi` runs without pausing. To turn it into a
+ real gate, add required reviewers (see
+ [Enabling a real approval gate](#enabling-a-real-approval-gate)).
+- **Deployment branches**: Optionally restrict to `main` branch and `v*` tags
+- **Secrets**: None. `VSCE_PAT` is a **repository** secret, not an environment secret,
+ so it resolves in both `staging.yml` and `release.yml` without being attached here.
+
+#### Repository secrets
+
+| Secret | Value | How to get it |
+| ------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------- |
+| `VSCE_PAT` | VS Code Marketplace PAT with **Marketplace: Manage** | [dev.azure.com](https://dev.azure.com) → User settings → Personal access tokens |
+| `RELEASE_PAT`| PAT used by `cut-release.yml` to open the release PR | GitHub → Settings → Developer settings → Personal access tokens |
+
+No PyPI API token is stored. Check the current rules and secret locations at any time:
+
+```bash
+gh api repos/Azure/agentops/environments/release --jq '.protection_rules'
+gh api repos/Azure/agentops/environments/release/secrets --jq '.secrets[].name'
+gh api repos/Azure/agentops/actions/secrets --jq '.secrets[].name'
+```
+
+### 10.2 PyPI and TestPyPI Trusted Publishing
+
+Both `staging.yml` and `release.yml` use
+[PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/), so uploads
+are authenticated with a short-lived OIDC token minted by GitHub Actions. There
+are no API tokens to create, store, or rotate.
+
+Configure it once per index, on the index side:
+
+#### TestPyPI (Staging)
+
+1. Log in at [test.pypi.org](https://test.pypi.org/) (a separate account from PyPI)
+2. Go to the project → **Manage → Publishing → Add a new publisher → GitHub**
+3. Owner `Azure`, repository `agentops`, workflow `staging.yml`, environment `staging`
+
+#### PyPI (Production)
+
+1. Log in at [pypi.org](https://pypi.org/)
+2. Go to the project → **Manage → Publishing → Add a new publisher → GitHub**
+3. Owner `Azure`, repository `agentops`, workflow `release.yml`, environment `release`
+
+The workflow filename and environment name must match exactly. A mismatch fails
+at upload time with `403 Invalid or non-existent authentication information`,
+which on the release pipeline happens *after* the tag is already pushed.
+
+> **Note**: TestPyPI and PyPI are completely separate systems with separate accounts and namespaces. A publisher configured on one does not apply to the other.
+
+### 10.3 First-Time Package Registration
+
+Trusted Publishing cannot create a project that does not exist yet. For a brand
+new project name, either upload once manually with a temporary API token, or use
+[PyPI's pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
+flow to reserve the name for the workflow. `agentops-accelerator` is already
+registered on both indexes, so this only matters if the package is renamed.
+
+## 11. Workflow File Reference
+
+All workflow files are in `.github/workflows/`:
+
+### `ci.yml` - Continuous Integration
+
+```
+Trigger: push to develop, PR to develop
+Flow: lint → test (matrix) → coverage
+ + on develop push: publish-dev → verify-dev (TestPyPI)
+Purpose: Quality gate for all code changes; auto-publish dev builds
+```
+
+Key detail: `publish-dev` and `verify-dev` only run on pushes to `develop` (not PRs). Every merge to develop produces a dev version on TestPyPI (e.g. `0.1.3.dev12`) via setuptools-scm. PRs to `main` are not covered by CI because they come from `release/*` branches which are already validated by the staging pipeline.
+
+### `_build.yml` - Reusable Build
+
+```
+Trigger: workflow_call (called by staging.yml and release.yml)
+Flow: checkout (full history) → uv sync → pytest → uv build → upload artifact
+Purpose: Single source of truth for the build process
+```
+
+Key detail: Uses `fetch-depth: 0` to ensure setuptools-scm has full git history for version derivation.
+
+### `staging.yml` - Staging Pipeline
+
+```
+Trigger: push to release/* branches, or workflow_dispatch
+Flow: _build → publish-testpypi → verify-testpypi
+Purpose: Validate release candidates before production
+```
+
+Key details:
+- `skip-existing: true` allows re-pushes without upload failures
+- Verify step uses a retry loop (5 attempts, 30s apart) for TestPyPI index propagation
+- Smoke tests cover `--version`, `--help`, and `agentops init`
+
+### `release.yml` - Production Release
+
+```
+Trigger: push v* tags, or workflow_dispatch
+Flow: _build → publish-testpypi → verify-testpypi → publish-pypi → github-release
+Purpose: Publish to PyPI and create GitHub Release
+```
+
+Key details:
+- `publish-pypi` declares `environment: release`, but that environment has no protection rules, so it publishes without pausing
+- PyPI upload uses Trusted Publishing (`id-token: write`), not an API token
+- `github-release` uses `gh release create` with `--generate-notes` for automatic release notes
+- Built artifacts (.whl, .tar.gz) are attached to the GitHub Release
+
+### `cut-release.yml` - Cut Release (Manual Dispatch)
+
+```
+Trigger: workflow_dispatch (manual button in Actions tab)
+Input: version - semver string (e.g. 0.2.0)
+Flow: validate → check [Unreleased] not empty → create release branch → update CHANGELOG → push → open PR
+Purpose: One-click release branch creation from develop
+```
+
+Key details:
+- Creates `release/v` branch from `develop`
+- Automatically updates `CHANGELOG.md` - inserts a versioned section `[] - ` at the top
+- Opens a PR from `release/v` → `main` with a checklist
+- The branch push triggers `staging.yml` automatically
+- Fails safely if the branch already exists
+- Refuses to run when `## [Unreleased]` is empty, because this workflow only inserts a versioned heading beneath that one and would otherwise publish an empty release section
+- Does NOT auto-tag or auto-publish - tagging remains a manual, intentional step
+
+## 12. Release Checklist
+
+Use this checklist when cutting a release:
+
+**Preparation**
+- [ ] All intended features/fixes are merged to `develop`
+- [ ] `CHANGELOG.md` has entries under `## [Unreleased]` for all user-visible changes, including anything Dependabot merged (Cut Release aborts if the section is empty)
+- [ ] Tests pass locally: `uv run pytest tests/ -x -q`
+- [ ] Version from setuptools-scm looks correct: `python -m setuptools_scm`
+
+**Staging**
+- [ ] Release branch created via **Cut Release** workflow (or manually)
+- [ ] CHANGELOG automatically updated with version and date
+- [ ] Staging pipeline passes: build + TestPyPI + verify (all 3 green)
+- [ ] PR opened: `release/v0.X.Y` → `main`
+
+**Production (tag + sync, do these together)**
+- [ ] PR from `release/v0.X.Y` → `main` created and approved
+- [ ] PR merged to `main`
+- [ ] Version tag created and pushed: `v0.X.Y` (this publishes to PyPI immediately)
+- [ ] Release pipeline runs: build + TestPyPI + verify + publish-pypi all green
+- [ ] **`main` merged back into `develop` and pushed**
+- [ ] **`git log --oneline origin/develop..origin/main` prints nothing**
+- [ ] `CHANGELOG.md` on `develop` shows only genuinely unreleased work under `## [Unreleased]`
+- [ ] GitHub Release created with artifacts
+- [ ] Published package verified: `pip install agentops-accelerator==0.X.Y`
+
+**Cleanup**
+- [ ] Release branch deleted (remote and local)
+
+## 13. Troubleshooting
+
+### Build Failures
+
+| Problem | Cause | Solution |
+| ---------------------------------------- | ----------------------------------- | --------------------------------------------- |
+| `setuptools_scm` can't determine version | Shallow clone (missing git history) | Ensure `fetch-depth: 0` in checkout step |
+| Version shows `0.0.0` locally | Not in a git repo or no tags exist | Run `git tag v0.0.1` to create an initial tag |
+| `ModuleNotFoundError` in tests | Dependencies not installed | Run `uv sync --group dev` |
+| Tests fail on Windows but pass on Linux | Path separator issues | Use `pathlib.Path`, not string concatenation |
+
+### TestPyPI Issues
+
+| Problem | Cause | Solution |
+| --------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
+| Upload fails with 403 | Trusted Publishing not configured for `staging.yml` / environment `staging` | Fix the publisher on test.pypi.org under **Manage → Publishing** |
+| Upload fails with "already exists" | Same version previously uploaded | Normal - `skip-existing: true` handles this. If you need a new upload, push another commit to increment the dev version |
+| Install fails with "no matching distribution" | Package not yet indexed | The verify job retries automatically (5 attempts, 30s apart). If persistent, check TestPyPI status |
+| Install fails with dependency errors | Dependency not on TestPyPI | Verify `--extra-index-url https://pypi.org/simple/` is present |
+
+### PyPI Issues
+
+| Problem | Cause | Solution |
+| ------------------------------------------ | ----------------------------------------- | -------------------------------------------------------------- |
+| Published to PyPI without being asked | Expected. `release` has no protection rules, so `publish-pypi` never pauses | Yank the release on pypi.org and ship a new patch version. See [Enabling a real approval gate](#enabling-a-real-approval-gate) |
+| Publish step stuck on "Waiting for review" | Someone added required reviewers to `release` | A listed reviewer approves via **Review deployments → release** |
+| Upload fails with 403 | Trusted Publishing not configured for `release.yml` / environment `release` | Fix the publisher on pypi.org under **Manage → Publishing**. The tag is already pushed, so bump the version and retag |
+| Version already exists on PyPI | Tag points to an already-released version | PyPI versions are immutable. You must use a new version number |
+
+### Git and Version Issues
+
+| Problem | Cause | Solution |
+| ------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------ |
+| Wrong version in built package | Tag not on the expected commit | Verify with `git log --oneline --decorate` that the tag is where you expect |
+| `pip install -e .` fails | `.git` directory missing | Editable installs need git history for setuptools-scm. Clone the repo, don't just download a zip |
+| Merge conflicts between release and develop | Normal for concurrent work | Resolve conflicts on the release branch before merging to main |
+| Next release's CHANGELOG republishes old entries, or drops the previous version's section | `develop` was left behind `main` after the last release | `git merge main` into `develop`, then hand-check `CHANGELOG.md`. See [Step 5](#step-5-tag-the-release-and-sync-develop) |
+
+### Environment and Permissions
+
+| Problem | Cause | Solution |
+| --------------------------------- | ----------------------------------- | ---------------------------------------------------------------------- |
+| "Environment not found" error | GitHub Environment not created | Create `staging` and `release` environments in Settings → Environments |
+| "Secret not found" error | Secret not added to the environment | Add secrets to the specific environment, not repository-level secrets |
+| No one was asked to approve the publish | `release` has no required reviewers | Confirm with `gh api repos/Azure/agentops/environments/release --jq '.protection_rules'` |
+| Reviewer can't approve deployment | Not listed as required reviewer | Update the environment's required reviewers list |
+
+## Architecture Diagram
+
+```mermaid
+flowchart TD
+ feat["feature/*"] -->|PR| develop(["develop"])
+ develop --> ci["CI (ci.yml) lint + test + coverage publish-dev → TestPyPI (dev version)"]
+ develop --> cut{{"Cut Release (cut-release.yml) manual dispatch - enter version"}}
+ cut --> rel(["release/v0.2.0"])
+
+ rel --> stagingBuild["_build test + build"]
+ stagingBuild --> stagingTest["TestPyPI publish"]
+ stagingTest --> stagingVerify["Verify install"]
+
+ rel -->|PR| main(["main"])
+ main -->|tag| tag(["v0.2.0"])
+
+ tag --> relBuild["_build"]
+ relBuild --> relTest["TestPyPI"]
+ relTest --> relVerify["Verify"]
+ relVerify --> relPypi["PyPI (no approval gate)"]
+ relPypi --> relGh["GitHub Release"]
+
+ main -->|merge back, REQUIRED| develop
+
+ subgraph Staging["Staging (staging.yml)"]
+ stagingBuild
+ stagingTest
+ stagingVerify
+ end
+
+ subgraph Release["Release (release.yml)"]
+ relBuild
+ relTest
+ relVerify
+ relPypi
+ relGh
+ end
+
+ classDef gate fill:#fff3cd,stroke:#856404,color:#000;
+ class cut,tag gate;
+```
diff --git a/docs/retrieval-optimization.md b/docs/retrieval-optimization.md
new file mode 100644
index 00000000..847a245a
--- /dev/null
+++ b/docs/retrieval-optimization.md
@@ -0,0 +1,201 @@
+# Retrieval optimization
+
+This is an Operate-phase activity. Once your agent is shipped and the gate is green,
+operating means making it better over time, not just keeping it from regressing.
+When the [operating loop](operate.md#the-operating-loop) shows weak grounding or
+off-topic answers, the root cause is often retrieval: the agent answered from the
+wrong chunks. This page shows how to measure and tune retrieval quality directly,
+using the Foundry [Document Retrieval evaluator](https://learn.microsoft.com/azure/foundry/concepts/evaluation-evaluators/rag-evaluators#document-retrieval).
+
+## Where this fits
+
+The smoke gate in the [HTTP agent tutorial](tutorial-http-agent.md) already scores
+two RAG signals on every PR:
+
+- **Groundedness**: is the answer supported by what was retrieved.
+- **Retrieval**: are the retrieved chunks relevant to the question.
+
+Both are LLM-judge metrics on a 1 to 5 scale and need no ground truth, so they
+guard the gate cheaply. They tell you *that* retrieval is weak. They do not tell
+you *how to fix it*, and they cannot compare two search configurations
+objectively.
+
+Document Retrieval is the tuning tool. It scores the *ranking* of your retrieved
+documents against relevance labels you author by hand (qrels) and returns
+position-aware metrics like NDCG. You run it offline, on a small labeled query
+set, whenever you change a search parameter and want to know if retrieval
+actually improved.
+
+!!! note "Why this is not a PR gate"
+ Document Retrieval needs hand-labeled ground truth, returns composite metrics
+ instead of a single pass/fail score, and its labels reference specific chunk
+ ids. Rebuilding the index can change those ids and invalidate the labels. That
+ is fine for a deliberate tuning study, but too brittle to run green on every
+ commit. Keep Groundedness and Retrieval on the gate, and use Document
+ Retrieval here, on demand.
+
+## Prerequisites
+
+You need grey-box mode enabled so the agent returns the documents it retrieved.
+This is the same `X-Eval-Context` contract set up in
+[step 11 of the HTTP agent tutorial](tutorial-http-agent.md#11-score-live-retrieval).
+With it on, a request returns the ranked retrieval alongside the answer:
+
+```json
+{
+ "answer": "...",
+ "context": "...",
+ "retrieved_documents": [
+ { "id": "documents-vw-fuel-system-pdf-c00002", "score": 0.71, "title": "...", "content": "..." },
+ { "id": "documents-vw-fuel-system-pdf-c00001", "score": 0.55, "title": "...", "content": "..." }
+ ]
+}
+```
+
+You also need the evaluator SDK:
+
+```powershell
+pip install azure-ai-evaluation
+```
+
+## The two shapes you map
+
+The orchestrator and the evaluator describe documents differently, so you map one
+to the other.
+
+| Concept | Orchestrator returns | Document Retrieval expects |
+|---|---|---|
+| A retrieved chunk | `{ "id": ..., "score": ... }` | `{ "document_id": ..., "relevance_score": ... }` |
+| A relevance label (qrels) | you author it | `{ "document_id": ..., "query_relevance_label": 0..4 }` |
+
+`relevance_score` is the retriever's own confidence (used to rank). The
+`query_relevance_label` is *your* judgment of how relevant the chunk truly is,
+from `0` (irrelevant) to `4` (perfect). Document Retrieval compares the ranking
+the retriever produced against the ranking your labels imply.
+
+## Step 1: capture the live retrieval
+
+Call the agent once per query you want to study and keep the `retrieved_documents`
+list. Map it to the evaluator shape.
+
+```python
+import os, requests
+
+ENDPOINT = os.environ["ORCHESTRATOR_URL"] # .../orchestrator
+QUERY = "What is the fuel pump rating?"
+
+resp = requests.post(
+ ENDPOINT,
+ headers={"Content-Type": "application/json", "X-Eval-Context": "true"},
+ json={"ask": QUERY},
+ timeout=120,
+).json()
+
+retrieved_documents = [
+ {"document_id": d["id"], "relevance_score": d["score"]}
+ for d in resp["retrieved_documents"]
+]
+```
+
+## Step 2: author the qrels
+
+For each query, label the chunks you care about. You do not have to label every
+chunk in the index, only the ones that matter for this question. Look at the
+`content` of each retrieved chunk and decide how relevant it is.
+
+```python
+retrieval_ground_truth = [
+ {"document_id": "documents-vw-fuel-system-pdf-c00002", "query_relevance_label": 4},
+ {"document_id": "documents-vw-fuel-system-pdf-c00001", "query_relevance_label": 2},
+ {"document_id": "documents-vw-brakes-pdf-c00007", "query_relevance_label": 0},
+]
+```
+
+!!! tip "Keep qrels small and stable"
+ A handful of well-labeled queries beats a large noisy set. Store the labels
+ next to your dataset and treat them as ground truth you maintain. Because the
+ ids are tied to your index, re-check the labels after any reindex or chunking
+ change.
+
+## Step 3: run the evaluator
+
+```python
+from azure.ai.evaluation import DocumentRetrievalEvaluator
+
+evaluator = DocumentRetrievalEvaluator(
+ ground_truth_label_min=0,
+ ground_truth_label_max=4,
+)
+
+result = evaluator(
+ retrieval_ground_truth=retrieval_ground_truth,
+ retrieved_documents=retrieved_documents,
+)
+print(result)
+```
+
+## Step 4: read the metrics
+
+Document Retrieval returns a set of ranking metrics, not a single score. These are
+the ones you will use most.
+
+| Metric | What it tells you | Direction |
+|---|---|---|
+| `ndcg@3` | ranking quality in the top 3, rewarding relevant chunks near the top | higher is better |
+| `xdcg@3` | like NDCG but weights the very top results more heavily | higher is better |
+| `fidelity` | how much of the truly relevant set the retriever managed to surface | higher is better |
+| `top1_relevance` | the label of the single best-ranked chunk | higher is better |
+| `top3_max_relevance` | the best label found anywhere in the top 3 | higher is better |
+| `holes` | retrieved chunks you never labeled, a sign your qrels are incomplete | lower is better |
+| `holes_ratio` | holes as a fraction of retrieved chunks | lower is better |
+
+Pick one headline metric to optimize, usually `ndcg@3`, and watch `holes_ratio`
+to make sure your labels still cover what the retriever returns. A rising
+`holes_ratio` usually means the index changed and your qrels need a refresh.
+
+## Step 5: the optimization loop
+
+Now use it to tune search. Change one thing, re-run the same labeled queries, and
+compare the headline metric.
+
+```mermaid
+flowchart LR
+ B["Baseline ndcg@3"] --> C["Change one search setting"]
+ C --> R["Re-run labeled queries"]
+ R --> M["Compare ndcg@3"]
+ M --> K{"Better?"}
+ K -->|yes| A["Keep it"]
+ K -->|no| X["Revert"]
+ A --> C
+ X --> C
+```
+
+Things worth sweeping one at a time:
+
+- **top-k**: how many chunks the retriever returns.
+- **search mode**: keyword, vector, or hybrid, and whether semantic reranking is on.
+- **chunk size and overlap**: how the documents were split at ingestion.
+- **reranker or filters**: any post-retrieval scoring you apply.
+
+Keep the change that raises `ndcg@3` without inflating `holes_ratio`. When you are
+done, the improvement should show up on the gate too: better ranking feeds better
+context, so the LLM-judge **Groundedness** and **Retrieval** scores in your PR
+smoke run should rise as well. That is the loop closing, retrieval tuning here,
+confirmed by the gate there.
+
+## Caveats
+
+- **Labels are index-specific.** The `document_id` values are chunk ids from your
+ current index. Re-author or re-verify qrels after reindexing or changing chunking.
+- **Keep the labeled set focused.** This is a diagnostic and tuning surface, not a
+ regression suite. A small, trusted set of queries is easier to maintain and
+ reason about.
+- **It complements, it does not replace, the gate.** Groundedness and Retrieval
+ stay on every PR. Document Retrieval is the deeper look you reach for when those
+ scores say retrieval is the problem.
+
+## Related
+
+- [Operate](operate.md): the operating loop this activity belongs to.
+- [HTTP agent tutorial, step 11](tutorial-http-agent.md#11-score-live-retrieval): how grey-box capture is wired.
+- [Foundry RAG evaluators](https://learn.microsoft.com/azure/foundry/concepts/evaluation-evaluators/rag-evaluators): the full evaluator reference, including every Document Retrieval score key.
diff --git a/docs/ship.md b/docs/ship.md
new file mode 100644
index 00000000..adfe26b8
--- /dev/null
+++ b/docs/ship.md
@@ -0,0 +1,209 @@
+# Ship
+
+This page explains how AgentOps gates a release in CI/CD. AgentOps owns the
+repo-side quality gate; your platform owns infrastructure and deployment. The
+goal is simple: the agent version that gets deployed is the exact version that
+was evaluated.
+
+For the full GitHub Actions reference, including every workflow file, the YAML,
+and the OIDC and RBAC setup steps, see
+[AgentOps on GitHub Actions](ci-github-actions.md). This page is the overview
+that explains why the pieces fit together.
+
+## Branches and environments
+
+AgentOps assumes a GitFlow-style branch model. Feature PRs run an eval against
+the candidate agent before merge. The PR from `release/**` to `main` is a manual
+approval gate for the already-tested release branch; it does not call agents.
+Deploy workflows run after merge and promote the reviewed branch to its
+environment:
+
+```mermaid
+flowchart LR
+ feature["feature/*"] --> prDev["PR eval candidate"]
+ prDev --> sandbox["sandbox"]
+ prDev --> develop["develop"]
+ develop --> devDeploy["Eval + deploy agentops-deploy-dev"]
+ devDeploy --> devEnv["dev"]
+
+ develop --> release["release/*"]
+ release --> qaDeploy["Eval + deploy agentops-deploy-qa"]
+ qaDeploy --> qaEnv["qa"]
+
+ release --> prProd["PR: release to main manual approval"]
+ prProd --> main["main"]
+ main --> prodDeploy["Prod release process deploy + smoke test agentops-deploy-prod"]
+ prodDeploy --> prodEnv["production"]
+
+ classDef branch fill:#e7f0fd,stroke:#1f4e79,color:#000;
+ classDef pipeline fill:#ede7f6,stroke:#4527a0,color:#000;
+ classDef env fill:#d1ecf1,stroke:#0c5460,color:#000;
+ class feature,develop,release,main branch;
+ class prDev,devDeploy,qaDeploy,prProd,prodDeploy pipeline;
+ class sandbox,devEnv,qaEnv,prodEnv env;
+```
+
+Legend:
+ Git branch
+ PR or workflow gate
+ deployed environment
+
+The PR gate and the deploy workflows each have one job, so it helps to read the
+flow as two tables: what runs on a pull request, and what runs after a merge.
+
+| Pull request | What runs | Notes |
+|---|---|---|
+| Feature PR into `develop` or `release/**` | `agentops-pr.yml` evaluates the PR candidate. | Protects the change before it enters the branch. It does not validate the already-deployed dev app. For HTTP agents it evaluates the sandbox endpoint; for prompt agents it stages and evaluates the candidate prompt in sandbox. |
+| PR from `release/**` into `main` | Manual approval gate with static checks only. | No agents are called; it approves the already-tested release branch. |
+
+| Merge into | Deploy workflow | Target environment |
+|---|---|---|
+| `develop` | per-environment deploy | dev |
+| `release/**` | per-environment deploy | QA |
+| `main` | `agentops-deploy-prod` production release process (deploy and smoke test) | production |
+
+!!! note "Where the full setup lives"
+ The two workflows you start with are covered next. The full set, including
+ the workflow YAML and the GitHub Environment and OIDC setup, is in
+ [AgentOps on GitHub Actions](ci-github-actions.md).
+
+## The two core workflows
+
+A generated AgentOps scaffold ships a PR gate and per-environment deploy
+workflows. The two that matter most early are the PR gate and the dev deploy.
+
+| Workflow | Trigger | What it does |
+|---|---|---|
+| `agentops-pr.yml` | PRs to `develop`, `release/**` | Evaluates the PR candidate, runs the Doctor gate, and comments on the PR. |
+| `agentops-deploy-dev.yml` | push to `develop` | Evaluates, then builds and deploys to the dev environment. |
+
+You do not write these by hand. `agentops workflow analyze` reads the repo and
+recommends the deploy wiring and eval runner, and `agentops workflow generate`
+writes the workflow files from that recommendation. Because both use the same
+analysis, the plan and the generated files do not drift.
+
+!!! note "Generate the PR gate first"
+ Start with `agentops workflow generate --kinds pr`. Add the dev, qa, and
+ prod deploys only after GitHub Environments and Azure OIDC are ready. This
+ keeps your first green run small and avoids wiring deploy steps before the
+ gate works.
+
+## Candidate versioning
+
+For Foundry prompt agents, each deploy stages a **candidate version** from your
+source-controlled `prompt_file` before evaluating it. PR-run candidates are
+tagged in Foundry with `agentops:candidate=true` plus the PR number and a
+timestamp, so portal viewers can tell abandoned PR candidates apart from
+deployed versions of record.
+
+The deployed-of-record version is tracked in `foundry-agent.json`, written per
+environment as a workflow artifact after the gate passes. That file, not the
+Foundry version number, is the supported way to know what each environment runs.
+
+!!! info "Prompt SHA and git SHA are the durable identity"
+ Foundry version numbers are local to each project, so sandbox
+ `travel-agent:2` may not match the dev or prod number. AgentOps instead
+ records `agentops.prompt_sha256` (the prompt text) and `agentops.git_sha`
+ (the commit) on every version and in `foundry-agent.json`. To check whether
+ two environments run the same prompt, compare those SHAs, not the version
+ numbers.
+
+This is the invariant the whole flow protects: the evaluated agent version is
+the deployed agent version. Foundry manages the candidate versions; AgentOps
+supplies the gate, the deployment record, and Cockpit visibility.
+
+## Why use a candidate
+
+The PR gate validates the proposed agent before it enters `develop` or a
+`release/**` branch. For HTTP agents, that usually means the sandbox endpoint
+you configured in the workflow. For Foundry prompt agents, the workflow stages a
+throwaway prompt version in sandbox and evaluates that candidate. The dev
+project is updated only by the deploy workflow after the PR merges.
+
+A passing PR gate is evidence that the proposed change is safe to merge. The dev
+deploy then promotes the reviewed branch and records the deployed prompt SHA.
+
+## The Doctor gate in CI
+
+Every PR run also runs `agentops doctor --evidence-pack` after the eval step.
+The eval step is the hard merge gate; the Doctor gate adds readiness checks like
+regression detection against the rolling baseline.
+
+| `--doctor-gate` value | PR behavior |
+|---|---|
+| `critical` (default) | Blocks the PR on critical findings, including a regression that drops a metric well below baseline even when eval thresholds still pass. |
+| `warning` | Also blocks on smaller regression drops. |
+| `none` | Doctor still writes and uploads evidence, but does not block; the eval step stays the only hard gate. |
+
+Production deploy templates always run Doctor with a critical finding gate. To
+understand the findings and severities behind these gates, see
+[Operate](operate.md) and the [Doctor checks reference](doctor-checks.md).
+
+## Identity and access
+
+CI authenticates to Azure with GitHub OIDC and federated credentials, so no
+long-lived secrets live in the repo. The same principal needs the right Azure
+RBAC roles before the first run, or the eval step fails with null metrics.
+
+!!! warning "Two roles are required for prompt-agent gates"
+ The OIDC principal needs **Foundry User** on the Foundry project and
+ **Cognitive Services OpenAI User** on the AI Services account that hosts the
+ judge model. If only one is in place, every metric returns `null` and the
+ gate fails without an obvious cause. The full setup, including the role ids
+ and the federation steps, is in [AgentOps on GitHub Actions](ci-github-actions.md).
+
+For the underlying procedures, follow Microsoft's own docs rather than copying
+long steps here:
+
+- [GitHub OIDC with Azure (workload identity federation)](https://learn.microsoft.com/azure/active-directory/workload-identities/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp)
+- [Assign Azure roles (RBAC)](https://learn.microsoft.com/azure/role-based-access-control/role-assignments-portal)
+
+## Try it
+
+Generate the CI/CD workflows from the same analysis AgentOps uses, smallest gate
+first.
+
+1. Read the repo and recommend the deploy wiring and eval runner.
+
+ ```bash
+ agentops workflow analyze
+ ```
+
+2. Generate just the PR gate first so your first green run stays small.
+
+ ```bash
+ agentops workflow generate --kinds pr
+ ```
+
+3. Add the per-environment deploy workflows once GitHub Environments and Azure OIDC are ready.
+
+ ```bash
+ agentops workflow generate --kinds pr,dev,qa,prod
+ ```
+
+4. Target Azure DevOps Pipelines instead of GitHub Actions when that is your platform.
+
+ ```bash
+ agentops workflow generate --kinds pr --platform azure-devops
+ ```
+
+## Run from your coding agent
+
+Install the AgentOps skills so your coding agent can wire and explain the
+pipeline for you.
+
+```bash
+agentops skills install --platform copilot
+```
+
+The skill that maps to shipping is:
+
+| Skill | What it helps with |
+|---|---|
+| `agentops-workflow` | Set up, generate, and explain CI/CD workflows. |
+
+## Next
+
+Read the full [GitHub Actions](ci-github-actions.md) reference, score readiness
+after each run on the [Operate](operate.md) page, or see where production signal
+comes from on the [Observe](observe.md) page.
diff --git a/docs/styles.css b/docs/styles.css
new file mode 100644
index 00000000..38c4d53e
--- /dev/null
+++ b/docs/styles.css
@@ -0,0 +1,386 @@
+/* AgentOps brand primary (Azure / Foundry blue) */
+[data-md-color-primary=custom] {
+ --md-primary-fg-color: #0078D4;
+ --md-primary-fg-color--light: #2B88D8;
+ --md-primary-fg-color--dark: #005A9E;
+ --md-primary-bg-color: #ffffff;
+ --md-primary-bg-color--light: #ffffffb3;
+}
+
+[data-md-color-accent=blue] {
+ --md-accent-fg-color: #0078D4;
+}
+
+/* Keep the docs chrome quiet. The product mark belongs in the landing hero,
+ not as a tiny icon in the navigation drawer. */
+.md-header {
+ background: rgba(255, 255, 255, 0.94);
+ color: #17233f;
+ border-bottom: 1px solid rgba(15, 23, 42, 0.10);
+ box-shadow: none;
+ backdrop-filter: blur(10px);
+}
+
+.md-header__button.md-logo {
+ display: none;
+}
+
+.md-header__title {
+ margin-left: 0;
+ font-weight: 650;
+}
+
+.md-header__topic[data-md-component="header-topic"] {
+ display: none;
+}
+
+.md-tabs {
+ background: rgba(255, 255, 255, 0.94);
+ color: #24364f;
+ border-bottom: 1px solid rgba(15, 23, 42, 0.08);
+}
+
+.md-tabs__link {
+ opacity: 0.72;
+}
+
+.md-tabs__link--active,
+.md-tabs__link:is(:focus, :hover) {
+ color: #005A9E;
+ opacity: 1;
+}
+
+.md-nav--primary .md-nav__title {
+ background: var(--md-default-bg-color);
+ color: var(--md-default-fg-color);
+ border-bottom: 1px solid var(--md-default-fg-color--lightest);
+ box-shadow: none;
+ font-weight: 650;
+}
+
+.md-nav--primary .md-nav__title .md-logo {
+ display: none;
+}
+
+[data-md-color-scheme="slate"] .md-header,
+[data-md-color-scheme="slate"] .md-tabs {
+ background: rgba(24, 29, 38, 0.94);
+ color: #f5f9ff;
+ border-bottom-color: rgba(255, 255, 255, 0.10);
+}
+
+[data-md-color-scheme="slate"] .md-tabs__link--active,
+[data-md-color-scheme="slate"] .md-tabs__link:is(:focus, :hover) {
+ color: #8cc8ff;
+}
+
+.mermaid {
+ font-size: 18px;
+}
+
+.mermaid svg {
+ max-width: 100%;
+ height: auto;
+}
+
+/* Landing banner (hero) */
+.agentops-banner {
+ position: relative;
+ margin: -0.25rem 0 1.4rem 0;
+ padding: 1.35rem 1.6rem 1.25rem 1.6rem;
+ border: 1px solid rgba(0, 120, 212, 0.16);
+ border-radius: 14px;
+ overflow: hidden;
+ color: var(--md-default-fg-color);
+ background:
+ radial-gradient(circle at 100% 0%, rgba(126, 63, 242, 0.13), transparent 34%),
+ linear-gradient(135deg, rgba(0, 120, 212, 0.12), rgba(255, 255, 255, 0.96) 46%, rgba(255, 255, 255, 1));
+ box-shadow: 0 8px 24px rgba(7, 35, 90, 0.08);
+}
+
+.agentops-banner::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background:
+ linear-gradient(90deg, rgba(0, 120, 212, 0.72), rgba(126, 63, 242, 0.52));
+ height: 3px;
+ pointer-events: none;
+}
+
+.agentops-banner-inner {
+ position: relative;
+ z-index: 1;
+}
+
+.agentops-banner-head {
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
+ margin: 0 0 0.35rem 0;
+}
+
+.agentops-banner-logo {
+ width: 40px;
+ height: 40px;
+ border-radius: 11px;
+ box-shadow: 0 6px 18px rgba(0, 120, 212, 0.16);
+ margin: 0;
+ flex: none;
+ display: block;
+}
+
+.agentops-banner h1 {
+ color: #16325c;
+ font-weight: 760;
+ font-size: 1.85rem;
+ line-height: 1.1;
+ letter-spacing: -0.02em;
+ margin: 0;
+}
+
+.agentops-banner h1 .headerlink {
+ display: none;
+}
+
+.agentops-banner-tagline {
+ color: var(--md-default-fg-color--light);
+ font-size: 0.86rem;
+ line-height: 1.35;
+ max-width: 760px;
+ margin: 0 0 0.35rem 0;
+}
+
+.agentops-banner-question {
+ color: #005A9E;
+ font-weight: 700;
+ font-size: 0.95rem;
+ margin: 0.05rem 0 0.75rem 0;
+}
+
+.agentops-banner-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.45rem;
+}
+
+.agentops-banner-actions .md-button--pill {
+ background: rgba(0, 120, 212, 0.08);
+ color: #005A9E;
+ border-color: rgba(0, 120, 212, 0.24);
+}
+
+.agentops-banner-actions .md-button--pill:hover {
+ background: rgba(0, 120, 212, 0.14);
+ border-color: rgba(0, 120, 212, 0.38);
+}
+
+[data-md-color-scheme="slate"] .agentops-banner {
+ border-color: rgba(77, 166, 255, 0.20);
+ color: var(--md-default-fg-color);
+ background:
+ radial-gradient(circle at 100% 0%, rgba(126, 63, 242, 0.16), transparent 34%),
+ linear-gradient(135deg, rgba(0, 120, 212, 0.14), rgba(24, 29, 38, 0.98) 46%, rgba(24, 29, 38, 1));
+ box-shadow: 0 10px 32px rgba(0, 0, 0, 0.30);
+}
+
+[data-md-color-scheme="slate"] .agentops-banner h1 {
+ color: #f5f9ff;
+}
+
+[data-md-color-scheme="slate"] .agentops-banner-question,
+[data-md-color-scheme="slate"] .agentops-banner-actions .md-button--pill {
+ color: #8cc8ff;
+}
+
+@media screen and (max-width: 600px) {
+ .agentops-banner {
+ padding: 1.25rem 1rem 1.15rem 1rem;
+ border-radius: 14px;
+ }
+ .agentops-banner h1 {
+ font-size: 1.65rem;
+ }
+ .agentops-banner-tagline {
+ font-size: 0.86rem;
+ }
+}
+
+.agentops-tagline {
+ font-size: 1.05rem;
+ color: var(--md-default-fg-color--light);
+ margin: 0.2rem 0 0.6rem 0;
+}
+
+.agentops-video-embed {
+ position: relative;
+ width: 100%;
+ max-width: 900px;
+ aspect-ratio: 16 / 9;
+ border-radius: 14px;
+ box-shadow: 0 10px 34px rgba(0, 0, 0, 0.20);
+ margin: 1.6rem auto 0.75rem auto;
+ overflow: hidden;
+ background: #000;
+}
+
+.agentops-video-embed iframe {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ border: 0;
+}
+
+[data-md-color-scheme="slate"] .agentops-video-embed {
+ box-shadow: 0 10px 34px rgba(0, 0, 0, 0.55);
+}
+
+.agentops-reference-architecture {
+ display: block;
+ width: 100%;
+ max-width: 100%;
+ margin: 1.25rem auto 1.5rem;
+ border: 1px solid var(--md-default-fg-color--lightest);
+ border-radius: 0.35rem;
+ background: var(--md-default-bg-color);
+}
+
+
+/* Pill buttons for release / install links */
+.md-button--pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3em;
+
+ padding: 0.3em 0.85em;
+ font-size: 0.72rem;
+ font-weight: 500;
+
+ background-color: #f6f8fa;
+ color: #0969da;
+ border: 1px solid #d0d7de;
+ border-radius: 999px;
+
+ text-decoration: none;
+ line-height: 1.2;
+}
+
+.md-button--pill:hover {
+ background-color: #eef1f4;
+ border-color: #c5ccd3;
+}
+
+[data-md-color-scheme="slate"] .md-button--pill {
+ background-color: #1c2128;
+ color: #4493f8;
+ border-color: #30363d;
+}
+
+.md-button--pill--rc {
+ background-color: var(--md-accent-fg-color--transparent);
+ color: var(--md-accent-fg-color);
+ border-color: var(--md-accent-fg-color);
+}
+
+.md-button--pill--rc:hover {
+ background-color: var(--md-accent-fg-color);
+ border-color: var(--md-accent-fg-color);
+ color: var(--md-accent-bg-color);
+}
+
+/* Avoid single-word "widows" wrapping alone on the last line of headings */
+.md-typeset h1,
+.md-typeset h2,
+.md-typeset h3,
+.md-typeset h4,
+.agentops-banner h1,
+.agentops-card h3 {
+ text-wrap: balance;
+}
+
+/* Feature cards on the landing page */
+.agentops-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 1rem;
+ margin: 1.2rem 0;
+}
+
+.agentops-card {
+ border: 1px solid var(--md-default-fg-color--lightest);
+ border-radius: 12px;
+ padding: 1rem 1.1rem;
+ background: var(--md-default-bg-color);
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+}
+
+.agentops-card:hover {
+ border-color: var(--md-accent-fg-color);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
+}
+
+.agentops-card h3 {
+ margin: 0 0 0.3rem 0;
+ font-size: 0.95rem;
+}
+
+.agentops-card p {
+ margin: 0;
+ font-size: 0.85rem;
+ color: var(--md-default-fg-color--light);
+}
+
+/* Knowledge nugget: a teaching aside distinct from info/tip admonitions.
+ Use as: !!! concept "Title" (or ??? concept for collapsible). */
+:root {
+ --md-admonition-icon--concept: url('data:image/svg+xml;charset=utf-8,');
+}
+
+.md-typeset .admonition.concept,
+.md-typeset details.concept {
+ border-color: #7E3FF2;
+}
+
+.md-typeset .concept > .admonition-title,
+.md-typeset .concept > summary {
+ background-color: rgba(126, 63, 242, 0.10);
+}
+
+.md-typeset .concept > .admonition-title::before,
+.md-typeset .concept > summary::before {
+ background-color: #7E3FF2;
+ -webkit-mask-image: var(--md-admonition-icon--concept);
+ mask-image: var(--md-admonition-icon--concept);
+}
+
+[data-md-color-scheme="slate"] .md-typeset .concept > .admonition-title,
+[data-md-color-scheme="slate"] .md-typeset .concept > summary {
+ background-color: rgba(140, 90, 255, 0.16);
+}
+
+/* Get-started call to action above the cards */
+.agentops-cta {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 0.75rem;
+ max-width: none;
+ margin: 1.4rem 0 0.4rem 0;
+ padding: 1.1rem 1.2rem;
+ border: 1px solid var(--md-default-fg-color--lightest);
+ border-radius: 12px;
+ background: var(--md-default-bg-color);
+}
+
+.agentops-cta p {
+ margin: 0;
+}
+
+.agentops-cta p:last-child {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 0.6rem;
+}
diff --git a/docs/tutorial-end-to-end.md b/docs/tutorial-end-to-end.md
index a399ca3b..914df295 100644
--- a/docs/tutorial-end-to-end.md
+++ b/docs/tutorial-end-to-end.md
@@ -506,7 +506,7 @@ agentops workflow generate `
> not yet contain the agent, it reads that block plus `prompt_file` and
> creates the first version automatically. No per-environment manual
> seeding. See the
-> [prompt-agent tutorial](tutorial-prompt-agent-quickstart.md) for the
+> [prompt-agent tutorial](tutorial-prompt-agent.md) for the
> full multi-environment journey.
Before running that workflow, make the PR gate runnable in GitHub. Install the
@@ -1028,10 +1028,12 @@ You are ready for a release review when:
- **Detailed prompt-agent walkthrough** (sandbox + dev journey, regression
PR, Doctor-blocking gate, fix + redeploy):
- [tutorial-prompt-agent-quickstart.md](tutorial-prompt-agent-quickstart.md).
+ [tutorial-prompt-agent.md](tutorial-prompt-agent.md).
- **Detailed hosted-agent walkthrough** (same sandbox + dev story but
for endpoints, with the git SHA / image tag identity story):
- [tutorial-hosted-agent-quickstart.md](tutorial-hosted-agent-quickstart.md).
+ [tutorial-hosted-agent.md](tutorial-hosted-agent.md) for Foundry hosted
+ runtimes, or [tutorial-http-agent.md](tutorial-http-agent.md) for an
+ agent you operate behind your own URL.
- **CI/CD reference** ([docs/ci-github-actions.md](ci-github-actions.md))
for full `agentops workflow generate` flag reference including the
`--doctor-gate` semantics.
diff --git a/docs/tutorial-hosted-agent-quickstart.md b/docs/tutorial-hosted-agent-quickstart.md
deleted file mode 100644
index d2f0f78f..00000000
--- a/docs/tutorial-hosted-agent-quickstart.md
+++ /dev/null
@@ -1,924 +0,0 @@
-# Tutorial: Foundry Hosted Agent or HTTP Agent (sandbox → dev with PR gate)
-
-Use this tutorial when the agent is reachable as an endpoint URL. The
-example creates a small **Travel Agent** HTTP endpoint locally (your
-**sandbox**), then shows how to swap in a deployed Foundry Hosted Agent
-or cloud-hosted URL (your **dev** environment) for CI.
-
-This path validates the AgentOps local route in a two-environment
-arrangement:
-
-- Foundry or your app platform manages hosting and runtime operations in
- each environment.
-- AgentOps invokes the endpoint from CI, applies repo thresholds, writes
- normalized `results.json`, runs Doctor with `--severity-fail critical`
- so regressions block the PR, and produces release evidence.
-
-The toolkit benefit is the same as the prompt-agent tutorial, adapted
-for endpoint-based agents: you author and iterate against a local
-sandbox, then let CI verify the deployed dev environment is still
-healthy on every PR. Production-readiness gates (eval thresholds plus
-Doctor critical findings) sit between you and a merge.
-
-## Repository set used in this tutorial
-
-This tutorial intentionally connects the hosted-agent path to the
-Microsoft projects that make the Operate story complete. The official
-Foundry extension, Azure services, and AgentOps workflow remain the
-actual runtime path.
-
-| Repository / skill | Role in the journey |
-|---|---|
-| `Azure/agentops` | Provides endpoint evaluation, thresholds, `results.json`, Doctor, Cockpit, and evidence. |
-| `microsoft-foundry` skill (Copilot Chat) | External, not bundled with AgentOps. Demonstrates how a skill outside the AgentOps toolkit can guide Foundry hosted agent creation and Operate wiring. The tutorial gives a portal-first fallback because the skill is optional. |
-| `microsoft/ai-agent-evals` | Reference for Foundry prompt-agent eval behavior; hosted endpoints use AgentOps local eval because CI must invoke your endpoint directly. |
-| `microsoft/foundry-toolkit` | Frames the Hosted Agent create/debug/deploy flow and the Operate handoff in VS Code. |
-| `microsoft/azure-skills` | Shows where the Microsoft Foundry skill can guide hosted-agent CI/CD, observe, and trace-regression follow-through. |
-| `Azure-Samples/microsoft-foundry-e2e-agent-observability-workshop` | Reference for the Foundry Observe/Optimize/Protect loop: OpenTelemetry traces, App Insights, Operate Ask AI, evaluations, and red-team follow-through. |
-
-## Before you run the tutorial
-
-Do this once before a live walkthrough or guided session. The goal is to keep the
-demo focused on the hosted-agent, observability, and AgentOps flow, not on
-unexpected permission prompts.
-
-| Check | Why it matters |
-|---|---|
-| Azure CLI is installed and `az login` succeeds with the tenant that owns the Foundry project. | AgentOps discovery, Doctor, Cockpit, and telemetry setup all use that Azure context. |
-| You can create or use a Foundry project and a chat-capable Azure OpenAI deployment. | Local endpoint evals still need a judge model for quality scoring. |
-| You can create or attach Application Insights, or you already have an App Insights connection string. | The local FastAPI sample emits OpenTelemetry spans only after telemetry is configured. |
-| You can deploy or expose the hosted endpoint that CI will call. | `localhost` is fine for local eval, but GitHub Actions or Azure Pipelines need a reachable HTTPS URL. |
-| You can push to the tutorial GitHub repository and run GitHub Actions or Azure Pipelines. | The PR gate only runs after the repo is published. |
-| GitHub CLI is authenticated with `gh auth login` if you use GitHub PR commands while testing CI. | The workflow handoff is smoother when repo, PR, and Actions access are already confirmed. |
-| You can create a GitHub environment named `dev` and add Actions variables/secrets. | The generated workflow uses that environment for Azure auth, endpoint settings, and evaluator settings. |
-| You can create an Entra app registration with federated credentials, or an admin is ready to provide the client ID, tenant ID, and subscription ID. | The workflow skill can wire OIDC cleanly; without this, CI cannot authenticate to Azure. |
-| Copilot or your coding-agent CLI is signed in before you ask it to run AgentOps skills. | The skill handoff assumes an authenticated coding-agent session that can read the repo and propose GitHub/Azure setup steps. |
-
-Unlike the Prompt Agent tutorial, this endpoint tutorial does not point the
-generated PR workflow at `ai-agent-evals`. Hosted and HTTP agents are evaluated
-through the AgentOps local runner because CI must invoke your endpoint, extract
-the response, apply repo thresholds, and write the normalized `results.json`.
-
-## Mental model: sandbox vs dev for hosted endpoints
-
-Even though hosted/HTTP agents don't have Foundry-managed prompt versions
-the way prompt agents do, the same **sandbox → dev → qa → prod** separation
-applies. For this tutorial you will work with two of them:
-
-| Environment | What it is in this tutorial | Purpose |
-|---|---|---|
-| **sandbox** | The local FastAPI endpoint on your machine (`http://127.0.0.1:8000`). For a more realistic setup, this can also be a Foundry Hosted Agent or ACA revision shared by the team (or per-stream/per-developer if your team prefers that). | Author-side experimentation. Iterate, regress, fix, and validate with `agentops eval run` locally. No shared-with-CI blast radius. |
-| **dev** | A deployed Foundry Hosted Agent, Azure Container Apps revision, AKS service, or any HTTPS endpoint reachable from CI. | Team-shared environment. The PR workflow evaluates this URL to verify it is still healthy. Deploy workflows (or your existing CI) update it on merge. |
-
-Each environment maps to its own `.azure//.env` file with its own
-`TRAVEL_AGENT_ENDPOINT` (and optional Foundry project endpoint for
-observability). The sandbox is the default; dev is added once the tutorial
-moves into CI.
-
-### The promotion identity for hosted agents
-
-The prompt-agent tutorial uses **prompt SHA-256** + **git SHA** as the
-cross-environment identity. Hosted agents don't have a `prompt_file`, so
-the identity story is even simpler:
-
-```
-git commit SHA (and container image tag, if you containerize)
- │
- └─ cross-environment identity
- │
- ├── sandbox endpoint (your localhost or dev-machine deploy)
- ├── dev endpoint (https://travel-agent-dev.example.com)
- ├── qa endpoint (https://travel-agent-qa.example.com)
- └── prod endpoint (https://travel-agent.example.com)
-```
-
-> **The cross-environment identifier for hosted agents is the git commit
-> SHA, and (when you containerize) the image tag derived from it.** Each
-> environment's endpoint URL changes; what you cite when traceability
-> matters is the SHA that produced the deployed code. AgentOps records
-> the git SHA in `.agentops/results//results.json` and in
-> release evidence, so the eval result and the source code stay linked
-> across environments.
-
-## Journey you will exercise
-
-```
-sandbox (local FastAPI)
- │ iterate, regress, fix locally with `agentops eval run`
- ▼
-PR opened with code change
- │ PR workflow evaluates dev URL with --doctor-gate critical
- ▼
-PR green ── merge ── deploy workflow updates dev endpoint
- │
- ▼
-deploy workflow re-runs eval + Doctor against the freshly updated dev URL
- │
- ▼
-green dev → ready for promotion to qa / prod
-```
-
-| Stage | Main tool | What you do | AgentOps role |
-|---|---|---|---|
-| Author + iterate | Your code editor + local FastAPI | Change endpoint behavior, run `agentops eval run` against `localhost`. | Local runner; baseline comparison. |
-| Open PR | GitHub or Azure DevOps + generated PR workflow | PR workflow runs eval against the **dev URL** and Doctor with `--severity-fail critical`. | PR gate (eval thresholds + critical Doctor findings block merge). |
-| Merge + deploy to dev | Your existing deploy pipeline (Foundry Toolkit, azd, ACA, AKS) + generated dev deploy workflow | Update the dev endpoint with the new commit and re-evaluate. | Deploy-time gate with the same `--severity-fail critical` (always strict on deploy). |
-| Observe runtime | Foundry Operate, Azure Monitor, Application Insights | Confirm traces, latency, errors, and metrics exist. | Checks whether telemetry is wired. |
-| Review readiness | AgentOps Doctor and Cockpit | Check CI, eval, telemetry, evidence, and links. | Primary repo-side release proof surface. |
-
-> **Architectural note.** For hosted endpoints the natural regression
-> gate runs at **deploy time** (post-merge), not PR time. The PR
-> workflow's eval verifies the dev URL is still healthy; it cannot
-> evaluate the PR's *unmerged* code unless your CI does a per-PR
-> ephemeral deploy. If you need PR-time regression catching for hosted
-> agents, the workflow skill can guide you through adding a per-PR
-> ephemeral deploy step (out of scope for this tutorial). The
-> sandbox loop (local FastAPI + `agentops eval run`) is the
-> equivalent author-side gate.
-
-Observability needs an App Insights resource connected to the Foundry project or
-agent runtime. If you ask Foundry to create or attach that resource from the
-Traces view, your identity must have the required Azure permissions. The local
-FastAPI sample below emits custom OpenTelemetry spans only after you enable the
-observability step; a real Foundry Hosted Agent emits richer Foundry runtime
-spans.
-
-> **Name the Azure container resources up front.** If you use the
-> `microsoft-foundry` skill or Foundry Toolkit to create a hosted-agent project,
-> tell it the resource group, Foundry / AI Services resource name, region, and
-> model deployment you want, for example `rg-agentops-travel-`,
-> `foundry-agentops-travel-`, `East US 2`, and `gpt-4o-mini`.
-> Replace `` with a short unique suffix when multiple people share
-> the same subscription. Resource group names are unique within a subscription;
-> Foundry / AI Services resource names should also be unique enough to avoid
-> Azure naming conflicts. Also ask the skill/tool to grant or verify `Foundry
-> User` access for your signed-in user (some portal screens still call this
-> `Azure AI User`) and `Cognitive Services OpenAI User` data-plane access for
-> your signed-in user plus any Foundry/Azure AI managed identities that will
-> call evaluator models. For a recorded tutorial, one shared resource group is
-> easiest because RBAC and cleanup happen in one place; production teams may
-> split resource groups by environment. For a fuller Azure baseline with
-> networking, identity, security, and operations patterns, see
-> [Azure AI Landing Zone](https://aka.ms/ailz).
-
-## 1. Create a clean workspace and install dependencies
-
-```powershell
-mkdir agentops-hosted-quickstart
-cd agentops-hosted-quickstart
-python -m venv .venv
-.\.venv\Scripts\Activate.ps1
-python -m pip install -U pip
-python -m pip install "agentops-accelerator[agent]" fastapi "uvicorn[standard]"
-agentops --version
-```
-
-For normal usage, prefer the published package above. For this tutorial path,
-install the aligned reference branch so the CLI, generated workflows, and
-tutorial steps stay in sync:
-
-```powershell
-python -m pip install "agentops-accelerator[agent] @ git+https://github.com/Azure/agentops.git@develop"
-```
-
-## 2. Create the Travel Agent endpoint
-
-Create a minimal HTTP agent with the same travel behavior you would later deploy
-with Foundry Toolkit, Azure Container Apps, AKS, or another hosting path.
-
-```powershell
-@'
-import os
-
-from fastapi import FastAPI
-from pydantic import BaseModel
-
-app = FastAPI(title="Travel Agent")
-
-
-class ChatRequest(BaseModel):
- message: str
-
-
-def plan_trip(message: str) -> str:
- if os.getenv("TRAVEL_AGENT_MODE") == "regressed":
- return "Travel depends on your preference. Search online and pick what looks best."
-
- text = message.lower()
- if "lisbon" in text:
- return (
- "Summary: Lisbon is a strong 3-day food and history trip. "
- "Day 1: Baixa, Chiado, and a sunset viewpoint. "
- "Day 2: Alfama, Sao Jorge Castle, and fado. "
- "Day 3: Belem, pastries, and a riverside walk. "
- "Notes: use transit, reserve popular restaurants early, and I cannot make live bookings."
- )
- if "seattle" in text:
- return (
- "Summary: Seattle can work well for a low-budget coffee and museum weekend. "
- "Day 1: Pike Place, waterfront, and independent coffee shops. "
- "Day 2: Museum of Pop Culture or Seattle Art Museum plus Capitol Hill. "
- "Notes: use transit, plan for rain, choose free viewpoints, and I cannot make live bookings."
- )
- if "tokyo" in text:
- return (
- "Summary: Tokyo with kids works best with short travel hops and flexible pacing. "
- "Plan: mix Ueno, Asakusa, Shibuya, teamLab or a science museum, parks, and one easy day trip. "
- "Notes: use IC transit cards, avoid overpacking each day, and I cannot make live bookings."
- )
- return (
- "Summary: I can help plan a short leisure trip. "
- "Please share the destination, trip length, budget, and traveler preferences. "
- "I cannot make live bookings."
- )
-
-
-@app.post("/chat")
-def chat(request: ChatRequest) -> dict[str, str]:
- return {"text": plan_trip(request.message)}
-'@ | Set-Content -Encoding utf8 app.py
-```
-
-Start the endpoint in a second terminal:
-
-```powershell
-cd agentops-hosted-quickstart
-.\.venv\Scripts\Activate.ps1
-python -m uvicorn app:app --host 127.0.0.1 --port 8000
-```
-
-From the first terminal, test it:
-
-```powershell
-Invoke-RestMethod `
- -Method Post `
- -Uri "http://127.0.0.1:8000/chat" `
- -ContentType "application/json" `
- -Body '{"message":"Plan a 3-day first-time trip to Lisbon for a couple who likes food and history."}'
-```
-
-For local validation, use:
-
-```powershell
-$env:TRAVEL_AGENT_ENDPOINT = "http://127.0.0.1:8000/chat"
-```
-
-### Make it a real Foundry Hosted Agent
-
-For CI or a real Foundry Hosted Agent flow, deploy through the official Foundry
-Toolkit path instead of leaving the endpoint on localhost:
-
-1. Install the
- [Foundry Toolkit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=TeamsDevApp.vscode-ai-foundry).
-2. Confirm the Foundry project has a deployed model and the required Hosted
- Agent permissions for your user or project identity.
-3. In VS Code, open the command palette and run
- `Microsoft Foundry: Create a New Hosted Agent`.
-4. Choose a single-agent template, Python or C#, and the model deployment.
-5. Replace the generated agent instructions or source logic with the Travel
- Agent behavior from this tutorial.
-6. Press F5 to debug locally with Agent Inspector.
-7. Run `Microsoft Foundry: Deploy Hosted Agent` from the command palette.
-8. Copy the deployed endpoint URL from the Foundry Toolkit or Foundry portal.
-9. Set:
-
- ```powershell
- $env:TRAVEL_AGENT_ENDPOINT = "https://"
- ```
-
-The endpoint used in CI must be reachable by the CI runner. If the deployed
-Foundry Hosted Agent follows the Responses API shape, use `protocol: responses`
-later in `agentops.yaml`.
-
-For the tutorial narrative, keep
-`https://github.com/placerda/foundry-toolkit` open alongside the official
-extension. You do not install the extension from that repository reference; use
-it as the reference point for the Operate handoff after Hosted Agent deploy:
-evaluation gate, telemetry readiness, trace links, and release evidence.
-
-## 3. Create the travel eval dataset
-
-```powershell
-New-Item -ItemType Directory -Force .agentops\data | Out-Null
-@'
-{"input":"Plan a 3-day first-time trip to Lisbon for a couple who likes food and history.","expected":"A concise 3-day Lisbon itinerary with food, history, neighborhoods such as Baixa, Alfama, and Belem, practical notes, and no claim to make live bookings."}
-{"input":"Suggest a low-budget weekend in Seattle for a solo traveler who likes coffee and museums.","expected":"A practical weekend Seattle plan with low-budget choices, coffee and museum suggestions, transit or weather notes, and no claim to make live bookings."}
-{"input":"I want to visit Tokyo for 5 days with two kids. What should we do?","expected":"A family-friendly 5-day Tokyo itinerary with kid-appropriate activities, transit and pacing notes, and no claim to make live bookings."}
-'@ | Set-Content -Encoding utf8 .agentops\data\travel-smoke.jsonl
-```
-
-## 4. Capture Foundry and endpoint values
-
-You need:
-
-| Value | Example |
-|---|---|
-| Agent endpoint | `http://127.0.0.1:8000/chat` for local validation, or `https:///chat` for CI |
-| Request field | `message` |
-| Response field | `text` |
-| Bearer token env var | optional, for example `HOSTED_AGENT_TOKEN` |
-| Foundry project endpoint | optional, but recommended for links and evaluators |
-| Azure OpenAI endpoint | `https://.openai.azure.com`, used later by local AI-assisted evaluators |
-| Evaluator model deployment | `gpt-4o-mini`, used later by local AI-assisted evaluators |
-| Application Insights connection string | recommended for observability and Doctor links |
-
-If the deployed endpoint needs a bearer token:
-
-```powershell
-$env:HOSTED_AGENT_TOKEN = ""
-```
-
-### Grant agent-build and data-plane access to your identity and Foundry managed identities
-
-The local AI-assisted evaluators that AgentOps runs in step 8 call
-chat-completions on the AI Services account that backs your Foundry
-project. Creating a project through the portal only assigns you
-`Foundry User` **at the project scope**. Creating/building agents in the
-Foundry UI can also require `Foundry User` on the parent Foundry / AI Services
-resource; some portal screens still use the previous role name,
-`Azure AI User`. `Foundry User` also does not cover the OpenAI data-plane action on
-the parent account. Even subscription `Owner` is insufficient: the built-in
-`Owner` role has `actions: ["*"]` but `dataActions: []`. Skipping the OpenAI
-role causes the eval to fail with `PermissionDenied` on
-`Microsoft.CognitiveServices/accounts/OpenAI/deployments/chat/completions/action`.
-
-If your skill/tool already confirmed these role assignments, treat the commands
-below as a verification/fallback step. Otherwise, run these assignments once per
-AI Services account hosting a Foundry project you will evaluate against. Local
-AI-assisted evaluators use your identity, while Foundry-hosted/server-side eval
-paths may use Azure AI managed identities from the same resource group.
-Assigning only the user can still leave server-side graders failing with
-`AuthenticationError`. Replace `` with the resource group you
-chose above, for example `rg-agentops-travel-`, and
-`` with the parent Foundry / AI Services account name.
-
-```powershell
-$subscriptionId = az account show --query id -o tsv
-$resourceGroup = ""
-$accountName = ""
-$accountScope = az cognitiveservices account show `
- --resource-group $resourceGroup `
- --name $accountName `
- --query id -o tsv
-$userObjectId = az ad signed-in-user show --query id -o tsv
-
-az role assignment create `
- --assignee $userObjectId `
- --role "53ca6127-db72-4b80-b1b0-d745d6d5456d" `
- --scope $accountScope
-
-az role assignment create `
- --assignee $userObjectId `
- --role "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" `
- --scope $accountScope
-
-az resource list -g $resourceGroup `
- --query "[?identity.principalId!=null].identity.principalId" -o tsv |
- ForEach-Object {
- az role assignment create `
- --assignee-object-id $_ `
- --assignee-principal-type ServicePrincipal `
- --role "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" `
- --scope $accountScope
- }
-```
-
-> **Give the assignment a few minutes to propagate.** Data-plane role
-> assignments on the AI Services account do **not** take effect
-> instantly — propagation to the local/Foundry evaluator workers can
-> take several minutes (occasionally up to ~15). Evaluators authenticate
-> per call, so the **first eval right after granting the role may show
-> intermittent `AuthenticationError` on a subset of graders and report
-> `Threshold status: FAILED` even when every threshold is green**. This
-> is a grader execution failure, not a quality regression — wait a few
-> minutes and re-run the eval.
-
-## 5. Initialize AgentOps interactively
-
-```powershell
-agentops init
-```
-
-Answer the prompts as the wizard asks them:
-
-| Prompt | Answer |
-|---|---|
-| Foundry project endpoint | `https://.services.ai.azure.com/api/projects/`, or press Enter if you are only testing the local endpoint |
-| Agent | The value in `$env:TRAVEL_AGENT_ENDPOINT`, for example `http://127.0.0.1:8000/chat` |
-| Dataset path | `.agentops/data/travel-smoke.jsonl` |
-
-The wizard does not ask for App Insights. Later runtime commands try to discover
-the connected App Insights resource through the Azure AI Projects SDK. If the
-project has no resource attached, or your identity cannot read it, run
-`agentops init --appinsights-connection-string ""` or set
-`APPLICATIONINSIGHTS_CONNECTION_STRING` manually in `.agentops/.env`.
-
-If the first run shows starter defaults such as `Agent [my-agent:1]` or
-`Dataset path [.agentops/data/smoke.jsonl]`, replace them with the hosted Travel
-Agent values above. Those defaults only come from the scaffolded starter file.
-
-By default, local Azure values go to `.agentops/.env`. If this repo already uses
-`azd`, or you want AgentOps to write to an azd env, run
-`agentops init --azd-env `.
-
-Then edit `agentops.yaml` so AgentOps knows how to call the endpoint:
-
-```yaml
-version: 1
-agent: http://127.0.0.1:8000/chat
-dataset: .agentops/data/travel-smoke.jsonl
-protocol: http-json
-request_field: message
-response_field: text
-```
-
-If your endpoint returns retrieval context or retrieved documents in the same
-JSON response as the answer, capture those fields with `response_fields` instead
-of relying only on static dataset `context`. In evaluator `input_mapping`,
-`$context` still refers to the dataset row, while `$response.context` refers to
-the context captured from the live endpoint response. See
-[`how-it-works.md`](how-it-works.md) for the full grey-box retrieval example.
-
-The `.agentops/.env` file is intentional: AgentOps keeps local Azure values out
-of source control while eval, Doctor, and Cockpit commands resolve the same
-workspace environment. The Foundry project endpoint lives there instead of in
-`agentops.yaml`; if you force an App Insights connection string later, it is
-saved there too. Existing azd workspaces keep using `.azure//.env`.
-
-For a deployed endpoint protected by a bearer token, add:
-
-```yaml
-auth_header_env: HOSTED_AGENT_TOKEN
-```
-
-For a Foundry hosted endpoint that already follows the Responses API shape, use:
-
-```yaml
-protocol: responses
-```
-
-For a raw Foundry invocations endpoint, use:
-
-```yaml
-protocol: invocations
-```
-
-## 6. Observe the endpoint in App Insights
-
-The local FastAPI endpoint is useful for the AgentOps eval loop, but it is not a
-Foundry-managed runtime. To make the observability story concrete, add
-OpenTelemetry spans that flow to the same App Insights backend Foundry uses for
-trace drilldown.
-
-Install the Azure Monitor OpenTelemetry distro when you reach this step:
-
-```powershell
-python -m pip install azure-monitor-opentelemetry
-```
-
-Make sure the AgentOps local env has an App Insights connection string. If it
-is not present yet, store the value once:
-
-```powershell
-agentops init --appinsights-connection-string ""
-```
-
-Load that value into the terminal that will run `uvicorn`:
-
-```powershell
-$env:APPLICATIONINSIGHTS_CONNECTION_STRING = (
- Get-Content .agentops\.env |
- Where-Object { $_ -like "APPLICATIONINSIGHTS_CONNECTION_STRING=*" } |
- Select-Object -First 1
-) -replace "^APPLICATIONINSIGHTS_CONNECTION_STRING=", ""
-```
-
-Open `app.py` and add these imports after `import os`:
-
-```python
-from azure.monitor.opentelemetry import configure_azure_monitor
-from opentelemetry import trace
-```
-
-Add this after `app = FastAPI(title="Travel Agent")`:
-
-```python
-if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"):
- configure_azure_monitor()
-
-tracer = trace.get_tracer("agentops.travel-agent")
-```
-
-Replace the `/chat` handler with:
-
-```python
-@app.post("/chat")
-def chat(request: ChatRequest) -> dict[str, str]:
- with tracer.start_as_current_span("travel-agent.chat") as span:
- mode = os.getenv("TRAVEL_AGENT_MODE", "normal")
- span.set_attribute("travel.agent.mode", mode)
- span.set_attribute("travel.query.length", len(request.message))
- response_text = plan_trip(request.message)
- span.set_attribute("travel.response.length", len(response_text))
- return {"text": response_text}
-```
-
-Restart the server and replay the dataset prompts:
-
-```powershell
-@(
- "Plan a 3-day first-time trip to Lisbon for a couple who likes food and history.",
- "Suggest a low-budget weekend in Seattle for a solo traveler who likes coffee and museums.",
- "I want to visit Tokyo for 5 days with two kids. What should we do?"
-) | ForEach-Object {
- Invoke-RestMethod `
- -Method Post `
- -Uri $env:TRAVEL_AGENT_ENDPOINT `
- -ContentType "application/json" `
- -Body (@{ message = $_ } | ConvertTo-Json)
-}
-```
-
-Then open Application Insights **Logs** and wait 2-5 minutes if the telemetry is
-not visible immediately. For the local FastAPI sample, look for the
-`travel-agent.chat` operation and the custom attributes in `customDimensions`:
-
-```kusto
-union traces, requests, dependencies
-| where timestamp > ago(1h)
-| where operation_Name has "travel-agent" or tostring(customDimensions["travel.agent.mode"]) != ""
-| project timestamp, itemType, operation_Id, operation_Name, message, customDimensions
-| order by timestamp desc
-```
-
-If you are demonstrating a real Foundry Hosted Agent instead of the local
-FastAPI sample, spend a minute in the Foundry observability panels too:
-
-| Foundry / Azure surface | What to show | Why it matters |
-|---|---|---|
-| Hosted agent / endpoint page | The deployed endpoint or agent reference that `agentops.yaml` calls. | Connects the repo target to the runtime being observed. |
-| Agent Traces | A recent request, Trace ID, spans, input/output, metadata, latency, model call, tool calls, and conversation context when present. | Shows the richer Foundry-managed runtime trace that the local sample cannot emit. |
-| Operate overview | Aggregate latency, failures, usage, and Ask AI when available. | Shows service health beyond one request. |
-| Application Insights Logs | KQL for the same operation ID or trace ID. | Gives the raw Azure Monitor drilldown path. |
-
-The transition is the same as the prompt-agent tutorial: Foundry and Azure
-Monitor own live observability; AgentOps checks whether those signals are wired
-into eval gates, Doctor findings, Cockpit, and release evidence.
-
-Those attributes are tutorial conventions, not special Foundry fields. A
-deployed Foundry Hosted Agent uses the same App Insights backend and Foundry
-trace surface, but its runtime spans include richer agent, tool, model, and
-conversation semantics that the local FastAPI sample does not produce.
-
-## 7. Check the selected eval runner
-
-```powershell
-agentops workflow analyze --format text
-```
-
-For hosted endpoints, AgentOps should recommend:
-
-```text
-Recommendation
- deploy placeholder
- evaluate AgentOps local eval
- workflow edits needed - review project-specific build/deploy steps
- Copilot skills installed - available for workflow adaptation handoff
-```
-
-That is expected. The default for a hosted endpoint is AgentOps local eval, so
-the repo can invoke the endpoint, normalize results, apply thresholds, and keep
-a stable `results.json` contract.
-
-You can opt into server-side execution instead. Set `execution: cloud` in
-`agentops.yaml` and Foundry runs the agent and the evaluators, with the run
-appearing in the New Foundry Evaluations panel. This requires the hosted URL to
-include `/agents//versions/`, since that pair is how Foundry
-identifies the target. The trade-off is that latency becomes Foundry-side
-rather than client-measured, and custom evaluators are skipped.
-
-## 8. Run a local eval
-
-Local AI-assisted evaluators need a judge model deployment. This is separate
-from `agentops init`: initialization captures the workspace target, while this
-environment configuration tells the evaluator which model to use.
-
-```powershell
-$env:AZURE_OPENAI_ENDPOINT = "https://.openai.azure.com"
-$env:AZURE_OPENAI_DEPLOYMENT = "gpt-4o-mini"
-```
-
-```powershell
-agentops eval analyze
-agentops eval run --output .agentops\results\manual-hosted-smoke
-code .agentops\results\manual-hosted-smoke\report.md
-```
-
-The run writes:
-
-```text
-.agentops/results/manual-hosted-smoke/results.json
-.agentops/results/manual-hosted-smoke/report.md
-.agentops/results/latest/
-```
-
-## 9. Force an endpoint regression, compare, then fix it
-
-The sample endpoint includes a deliberate regression switch. Stop the server in
-the second terminal, restart it in regressed mode, and run a comparison against
-the good baseline:
-
-```powershell
-$env:TRAVEL_AGENT_MODE = "regressed"
-python -m uvicorn app:app --host 127.0.0.1 --port 8000
-```
-
-From the first terminal:
-
-```powershell
-agentops eval run `
- --baseline .agentops\results\manual-hosted-smoke `
- --output .agentops\results\regressed-hosted
-code .agentops\results\regressed-hosted\report.md
-```
-
-The report should show that the vague response lost quality against the travel
-dataset. Now stop the server, remove the regression switch, restart it, and run
-the comparison again:
-
-```powershell
-Remove-Item Env:\TRAVEL_AGENT_MODE -ErrorAction SilentlyContinue
-python -m uvicorn app:app --host 127.0.0.1 --port 8000
-```
-
-```powershell
-agentops eval run `
- --baseline .agentops\results\regressed-hosted `
- --output .agentops\results\fixed-hosted
-code .agentops\results\fixed-hosted\report.md
-```
-
-This is the core AgentOps loop for hosted endpoints: keep a stable dataset,
-compare a changed runtime against the last known result, fix the agent, and
-rerun the same gate before a PR or release.
-
-If this hosted endpoint is backed by a Foundry / azd eval recipe, you can use the
-same conversation-aware contract as the prompt-agent Travel Agent tutorial
-before you generate CI: set `execution: azd`, add `dataset_kind: multi-turn`, run
-`agentops eval init --force`, and then run `agentops eval run`. Add a rubric only
-after your Foundry project already has a real rubric evaluator and the azd run
-emits metric names you can bind to thresholds.
-
-## 10. Generate CI and Doctor evidence
-
-Generate both the PR and dev deploy workflows with `--doctor-gate critical`
-so the PR template fails when Doctor reports critical regression findings.
-For hosted agents, the auto-detection path resolves to a placeholder deploy
-workflow (or `azd` if `azure.yaml` exists); you customize it with your
-existing deploy steps later.
-
-```powershell
-agentops workflow generate `
- --kinds pr,dev `
- --doctor-gate critical `
- --force
-agentops doctor --workspace . --evidence-pack
-code .agentops\agent\report.md
-code .agentops\release\latest\evidence.md
-```
-
-The generated PR gate reuses the same `agentops.yaml` contract. If you promoted
-the hosted endpoint to an azd/Foundry eval recipe with rubrics, CI runs that
-recipe and blocks on the rubric thresholds; otherwise it runs the local hosted
-endpoint gate and normalized thresholds. In both cases Doctor and the evidence
-pack surface multi-turn coverage, trace sampling readiness, replay/evaluation
-links, and trace-to-dataset lineage when those signals exist.
-
-> **`--deploy-mode prompt-agent` does not apply to hosted endpoints.**
-> That mode is specific to Foundry prompt agents (the stage-prompt-as-
-> candidate flow). For hosted endpoints, `agentops workflow generate`
-> auto-detects `azd` or falls back to a placeholder you customize with
-> your existing deploy steps (Foundry Toolkit deploy, `azd deploy`,
-> ACA revision update, AKS rollout, etc.).
-
-> **`--doctor-gate critical` is the default and what this tutorial uses.**
-> The PR workflow runs `agentops doctor --severity-fail critical`, which
-> exits non-zero (and fails the PR check) when Doctor reports any
-> critical finding. Use `--doctor-gate warning` to also fail on warnings
-> during hardening sprints. Use `--doctor-gate none` to make Doctor
-> advisory-only (the pre-`--doctor-gate` behavior). The deploy workflow
-> already runs Doctor with `--severity-fail critical`; that part is not
-> configurable because production gates should be strict.
-
-`agentops doctor` can take a few minutes because it checks Azure auth, Foundry
-discovery, Azure Monitor/App Insights, local eval history, and repo workflow
-evidence. The terminal progress line should keep moving while those sources are
-collected.
-
-Read the output in this order: `AgentOps pre-flight` lists the local access and
-telemetry-discovery checks, `Release readiness` is the readiness verdict,
-`Findings` / `Finding summary` names the blocking or warning items, and the
-evidence paths are the files to open. Warnings are advisory unless strict
-pre-flight is enabled; `blocked` means the report has findings to review, not
-that Doctor failed. If App Insights is already connected but AgentOps cannot
-discover it, run `az login`, confirm Reader on the Foundry project resource
-group, or set `APPLICATIONINSIGHTS_CONNECTION_STRING` explicitly.
-
-Use this quick readout while presenting the terminal output:
-
-| Output | How to explain it |
-|---|---|
-| `AgentOps pre-flight 4 ok` | The workspace, Azure auth, Foundry project, and App Insights discovery checks are all usable. |
-| `Wrote` | The local Doctor diagnostic report was generated. |
-| `Release readiness: blocked` | The command succeeded, but the current evidence has findings that block release readiness. |
-| `Evidence pack` / `Evidence report` | These are the release-review artifacts to open or attach to the PR/release discussion. |
-| `Findings: ...` | This is the severity rollup; critical items are what you discuss first. |
-| `Finding summary` | This is the terminal triage list. For hosted endpoints, explain production latency/errors and eval regressions first, then treat workflow, threshold, RAI, and trace-regression warnings as hardening follow-ups. |
-
-The useful story is the insight list, not the fact that a file was written.
-For hosted endpoints, Doctor connects runtime signals and repo readiness: latency
-or error findings point to production behavior, regression findings point to eval
-quality loss, and operational findings point to the missing release machinery
-such as deploy workflows, thresholds, continuous eval, action SHA pinning, and
-trace-to-regression feedback. Use critical findings as release blockers and
-warnings as the hardening backlog.
-
-The generated PR gate runs `agentops eval run` against the dev endpoint URL.
-Before using that workflow in GitHub Actions or Azure Pipelines, replace any
-localhost agent URL with the deployed Foundry Hosted or cloud endpoint (set
-`AGENTOPS_AGENT_ENDPOINT` as an Actions variable on the `dev` GitHub environment).
-Have the Entra app-registration permission or the admin-provided OIDC values
-ready before using a workflow skill to connect the repo to Azure.
-
-With `--doctor-gate critical` set during workflow generation, the PR workflow's
-Doctor step blocks the PR on critical findings (eval thresholds are *also* a
-hard gate via the `agentops eval run` exit code). A green PR run means: the dev
-endpoint passed eval thresholds **and** Doctor found nothing critical. A
-blocked PR means one of those two gates flagged a problem; the PR comment and
-the run summary include the Doctor finding summary so the author knows exactly
-which findings to address. Use `--doctor-gate warning` if you want warnings to
-block too, or `--doctor-gate none` to revert to the pre-`--doctor-gate`
-advisory-only behavior. Production deploy workflows always run Doctor as a
-critical release gate regardless of the PR setting.
-
-### Add the dev environment to azd
-
-The seed workspace created by `agentops init` lives under
-`.azure//.env`. For the CI flow to use a separate dev project (or
-dev observability target), add a sibling env. AgentOps does this entirely on
-the filesystem; no `azd` CLI required:
-
-```powershell
-New-Item -ItemType Directory -Force .azure\dev | Out-Null
-@'
-AZURE_AI_FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
-APPLICATIONINSIGHTS_CONNECTION_STRING=
-AZURE_OPENAI_ENDPOINT=https://.openai.azure.com
-AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini
-'@ | Set-Content -Encoding utf8 .azure\dev\.env
-```
-
-Keep `.azure/config.json` pointed at the sandbox env (`defaultEnvironment`) so
-local commands default to sandbox; CI passes `--azd-env dev` (or sets the env
-explicitly) so it uses dev. The Foundry project endpoint plus the agent URL
-(`AGENTOPS_AGENT_ENDPOINT` set as an Actions variable on the `dev` GitHub
-environment) together let CI evaluate the deployed dev endpoint and land
-results in the dev observability target.
-
-Use the same workflow-skill handoff pattern as the Prompt Agent tutorial, but
-keep the scope to the hosted endpoint:
-
-```powershell
-agentops skills install --platform copilot --force
-```
-
-Then ask Copilot:
-
-```text
-Use the AgentOps workflow skill to get the generated PR gate running for this
-hosted-agent project.
-
-Create or connect the GitHub repo if needed, set AGENTOPS_AGENT_ENDPOINT in the
-`dev` environment to the deployed HTTPS endpoint, wire Azure OIDC and required
-Actions variables in the `dev` environment, verify AZURE_TENANT_ID is the tenant
-that owns the Entra app registration and its federated credential, and set any
-required endpoint token as a secret. The PR gate uses --doctor-gate critical so
-the workflow blocks on critical Doctor findings (regressions or other strict
-signals). Do not add scheduled Doctor, QA, or production workflows yet. Show me
-the plan before changing GitHub or Azure, and call out anything that needs
-owner/admin permission.
-```
-
-> **Before the first run**, check the repository's OIDC subject claim prefix
-> with `gh api repos///actions/oidc/customization/sub`. Accounts
-> with immutable IDs send `repo:@/@:...`, and
-> Entra matches the federated credential subject literally, so a credential
-> built from the plain `repo:/:...` format fails with
-> `AADSTS700213`. See
-> [`ci-github-actions.md`](ci-github-actions.md#federated-credential-subject-check-sub_claim_prefix-first).
-
-Open both Doctor outputs. The report explains the findings; the evidence pack
-summarizes what a reviewer needs to decide whether the endpoint is releasable.
-In a fresh tutorial workspace, warnings about production telemetry, CI history, or trace
-regression history are expected and useful: they show what remains before this
-local endpoint becomes an operated service.
-
-If production telemetry *does* carry enough live traffic to trip latency or
-error criticals, those are honest signals. The thresholds that decide
-critical-vs-warning live in `.agentops/agent.yaml`
-(`checks.latency.p95_threshold_seconds`, `checks.errors.rate_threshold`) and are
-separate from the `agentops.yaml` eval-gate thresholds; raise them only if you
-deliberately want to relax the production gate for a demo.
-
-If you later want a separate cadence outside PRs, generate the optional Doctor
-workflow with `agentops workflow generate --kinds doctor --force`.
-
-
-This is also where `placerda/azure-skills` fits the story. AgentOps
-generates the repo-side gate and evidence; the Microsoft Foundry skill is the
-natural guidance layer to teach Copilot/agents how to connect Foundry Toolkit,
-Azure Monitor, trace regression, and CI/CD readiness without making the tutorial
-look self-contained inside AgentOps.
-
-## 11. Open Cockpit
-
-```powershell
-agentops cockpit --workspace .
-```
-
-Cockpit starts a read-only local web server and prints
-`http://127.0.0.1:8090` (this is the Cockpit UI port, not your agent's
-`:8000`). Open that URL in your browser; press `Ctrl+C` in the terminal to
-stop it. It reflects the **active azd environment** (`sandbox`, from
-`defaultEnvironment` in `.azure/config.json`) — there is no URL switch. To
-inspect `dev`, stop Cockpit, point the active env at `dev` (set
-`defaultEnvironment: dev` in `.azure/config.json`, or export
-`AZURE_ENV_NAME=dev`), then rerun the command.
-
-Read the page top to bottom and confirm each card:
-
-| Section | What to confirm |
-|---|---|
-| **Foundry connection** | The Foundry project / tenant resolve, and the agent is your hosted endpoint URL. |
-| **Open in Foundry** | The deep-links open your project in the correct tenant. |
-| **Observability readiness** | Trace setup / sampling status from the latest Doctor analysis. |
-| **AgentOps Doctor** | The same finding rollup from the Doctor / evidence-pack step (criticals first, then warnings). |
-| **Local eval history** | Your `agentops eval run` baseline, regressed, and fixed reruns appear. |
-| **Quality metrics** | Evaluator score trends from your runs. |
-| **Production telemetry** | App Insights latency / error snapshot for the `travel-agent.chat` operation (or a "no live traffic" state in a fresh workspace). |
-| **CI/CD Pipelines** | The PR and dev deploy workflows you generated are listed. |
-| **Next actions** | The prioritized backlog Cockpit derives from the open findings. |
-
-Cockpit does not run checks or mutate anything — it renders the latest
-`results.json`, Doctor report, and evidence pack you already produced, and
-links out to Foundry / Azure Monitor for live runtime data.
-
-## Success criteria
-
-You are done when:
-
-- The Travel Agent endpoint responds to `POST /chat` in the sandbox
- (local FastAPI) and the dev environment (your deployed endpoint or a
- placeholder URL you plan to wire to a deploy workflow).
-- At least one sandbox endpoint request appears in App Insights Logs
- with the `travel-agent.chat` operation. If you deploy as a real
- Foundry Hosted Agent in the dev project, its richer runtime spans can
- also appear in Foundry Traces.
-- `agentops workflow analyze` selects `agentops-local`.
-- `agentops eval run` writes `results.json` and `report.md`, and you
- forced the endpoint into regressed mode, compared it with the
- baseline, fixed it, and reran the comparison locally — proving the
- author-side gate works before opening a PR.
-- The generated PR workflow uses `--severity-fail critical` for the
- `agentops doctor` step (set by `--doctor-gate critical` during
- `agentops workflow generate`), so a regression that lands in dev
- blocks the next PR until it is fixed.
-- `.azure/` contains both a sandbox env (default) and a dev env, each
- with its own `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` and
- `APPLICATIONINSIGHTS_CONNECTION_STRING`.
-- `agentops doctor --evidence-pack` writes
- `.agentops/release/latest/evidence.md`, and the workflow summary
- surfaces its Doctor finding summary.
-- Cockpit opens and shows the local eval history plus Doctor readiness.
-- Optional ASSERT, ACS, and red-team evidence artifacts are either absent
- (Doctor stays silent) or wired through `assert_path`, `acs_path`, and
- `redteam_path` in `agentops.yaml`. AgentOps cites their status/hash in release
- evidence; it does not execute ASSERT, apply ACS controls, or run red-team
- campaigns.
-
-## Where to go next
-
-- **Add per-PR regression catching for hosted agents.** Per-PR ephemeral
- deploys (e.g., ACA revision per PR, dedicated Foundry Hosted Agent per
- PR) are the architectural answer if you want PR-time eval to catch
- endpoint regressions before merge. The workflow skill can scaffold
- this.
-- **Promote to qa and prod.** Mirror the dev pattern: create
- `.azure/qa/.env` and `.azure/prod/.env`, set GitHub Environments with
- the right `AGENTOPS_AGENT_ENDPOINT`, and use `agentops workflow
- generate --kinds qa,prod --force`.
-- **Walk through the prompt-agent tutorial** at
- [tutorial-prompt-agent-quickstart.md](tutorial-prompt-agent-quickstart.md)
- to see the full prompt-as-code regression journey (stage-then-eval
- at PR time, no per-PR deploys required) and contrast the two
- architectures.
diff --git a/docs/tutorial-hosted-agent.md b/docs/tutorial-hosted-agent.md
new file mode 100644
index 00000000..5689fe82
--- /dev/null
+++ b/docs/tutorial-hosted-agent.md
@@ -0,0 +1,433 @@
+# Hosted agent tutorial
+
+Use this tutorial when your agent is a **Foundry Hosted Agent**. Foundry runs
+the agent for you as a managed runtime, so you deploy code and Foundry serves
+it behind a stable endpoint. The worked example is a small Travel Agent, and you
+use AgentOps to add a PR gate that catches regressions before merge, a dev
+deploy, Doctor evidence, and Cockpit.
+
+A hosted agent is not an HTTP agent. With an HTTP agent you run the web server
+yourself and wire the telemetry by hand. With a hosted agent the Foundry runtime
+serves the request and emits the trace for you, so `invoke_agent` spans show up
+in Application Insights without any `configure_azure_monitor` call in your code.
+If your agent runs as a URL service you operate yourself, use the
+[HTTP agent tutorial](tutorial-http-agent.md) instead.
+
+You will do four things:
+
+1. **Evaluate** the hosted agent while you experiment in sandbox.
+2. **Ship** the code through GitHub so the same reviewed commit deploys to dev.
+3. **Observe** the dev run with server-side traces, telemetry, and Doctor findings.
+4. **Operate** with release evidence, thresholds, and a Cockpit summary.
+
+```mermaid
+flowchart LR
+ E["Evaluate Deploy to sandbox Run evals"]
+ S["Ship Move code to git Open PR, deploy to dev"]
+ O["Observe Read server-side traces Run Doctor"]
+ W["Operate Review evidence Make the ship call"]
+ E --> S --> O --> W
+```
+
+The idea is simple: sandbox is for trying things, Git is the source of truth,
+and CI evaluates the PR candidate against the dev endpoint before anything is
+promoted. If Doctor finds a critical regression, the PR should not ship.
+
+## Before you run the tutorial
+
+Run through this once before a live walkthrough, grouped by area, so the demo
+stays on the Foundry plus AgentOps flow instead of permission prompts.
+
+**Foundry projects**
+
+- A Foundry project with a deployed model (for example `gpt-4o-mini`) and the Hosted Agent permissions your user or project identity needs to create and deploy a hosted agent.
+- Application Insights connected to the project, with Reader granted to the project's managed identity. This powers the server-side traces and telemetry that make the Observe step real.
+
+**Azure**
+
+- Azure CLI installed and `az login` working on the tenant that owns the project.
+- An Entra app registration with federated credentials, or an admin ready to provide the client, tenant, and subscription id, for the CI deploy.
+
+**GitHub**
+
+- Push access to the tutorial repo and permission to run GitHub Actions.
+- GitHub environments named `sandbox` and `dev` for Azure auth and Foundry endpoints.
+- `gh auth login` authenticated for the PR commands.
+
+**Tooling**
+
+- The [Foundry Toolkit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=TeamsDevApp.vscode-ai-foundry) installed, so you can create and deploy the hosted agent from the command palette.
+- Your coding-agent CLI (Copilot or similar) signed in before you run AgentOps skills, so it can read the repo and propose the GitHub and Azure setup.
+
+## What happens in this tutorial
+
+One commit moves through four stages. Use this as a checklist:
+
+| Stage | What it means |
+|---|---|
+| **Deploy to sandbox** | Create the hosted agent, deploy it to a sandbox endpoint, and try it. |
+| **Move code** | Keep the agent source in Git, which becomes the source of truth. |
+| **Create dev environment** | Leave dev empty. CI reads the AgentOps config and deploys the dev hosted agent from the merged commit. |
+| **Block regressions** | CI evaluates the PR candidate against the dev endpoint, applies thresholds, and runs Doctor. Serious regressions stop the PR. |
+
+### Why the git SHA matters
+
+Foundry gives each deployed hosted agent a version number that is local to its
+project, so sandbox `travel-agent:2` may not match the number in dev, qa, or
+prod. The stable identity across environments is the **git commit SHA** that
+produced the deployed code, plus the container image tag when you containerize.
+
+```
+git commit SHA (and container image tag, if you containerize)
+ │
+ └─ cross-environment identity
+ │
+ ├── sandbox endpoint (your team sandbox deploy)
+ ├── dev endpoint (https://travel-agent-dev.example.com)
+ ├── qa endpoint (https://travel-agent-qa.example.com)
+ └── prod endpoint (https://travel-agent.example.com)
+```
+
+Each environment's endpoint URL changes, but the SHA that produced the running
+code stays the same. AgentOps records the git SHA in
+`.agentops/results//results.json` and in release evidence, so the
+eval result and the source code stay linked across environments. To check
+whether dev and prod run the same code, compare git SHAs, not the Foundry
+version numbers.
+
+## 1. Create the workspace
+
+First, create and activate a workspace folder with its own virtual environment:
+
+```powershell
+mkdir agentops-hosted-quickstart
+cd agentops-hosted-quickstart
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+```
+
+Then install AgentOps and confirm the CLI:
+
+```powershell
+python -m pip install "agentops-accelerator[agent]"
+agentops --help
+```
+
+The `[agent]` extra is what makes `agentops cockpit` work in step 13. Without
+it, the CLI installs fine and the eval commands run, but Cockpit raises an
+`ImportError`.
+
+## 2. Install the skills
+
+Install the AgentOps Copilot skills so your coding agent can read the repo and
+propose the GitHub and Azure wiring for you:
+
+```powershell
+agentops skills install
+```
+
+The skills are optional for the core loop, but they make the CI and OIDC steps
+much faster because the agent adapts the generated workflows to your project.
+
+## 3. Create the hosted agent
+
+Deploy the Travel Agent through the official Foundry Toolkit path so it runs as
+a real Foundry Hosted Agent, not a service on your laptop:
+
+1. Confirm the Foundry project has a deployed model and the required Hosted Agent permissions for your user or project identity.
+2. In VS Code, open the command palette and run `Microsoft Foundry: Create a New Hosted Agent`.
+3. Choose a single-agent template, Python or C#, and the model deployment.
+4. Give the agent Travel Agent behavior: plan short trips from a free-text request, with a day-by-day itinerary and a few concrete suggestions.
+5. Press F5 to debug locally with Agent Inspector and confirm it answers.
+6. Run `Microsoft Foundry: Deploy Hosted Agent` from the command palette.
+7. Copy the deployed endpoint URL from the Foundry Toolkit or the Foundry portal.
+
+Store the sandbox endpoint so the AgentOps commands can reach it:
+
+```powershell
+$env:TRAVEL_AGENT_ENDPOINT = "https://.services.ai.azure.com/api/projects//agents//versions/"
+```
+
+This deployed endpoint is your **sandbox** for the tutorial. Later you add a
+separate **dev** endpoint that CI evaluates on every PR.
+
+## 4. Try the agent
+
+Send a request to the deployed endpoint to confirm it responds before you wire
+evaluation around it. Use the shape your hosted agent expects; a Responses API
+hosted agent takes an input string and returns an output message.
+
+Once you get a sensible itinerary back, you are ready to evaluate it.
+
+## 5. Create the dataset
+
+Create a small smoke dataset that AgentOps replays against the endpoint:
+
+```powershell
+mkdir .agentops\data
+```
+
+Create `.agentops\data\travel-smoke.jsonl` with three cases, one JSON object per
+line:
+
+```json
+{"message": "Plan a 3-day first-time trip to Lisbon for a couple who likes food and history."}
+{"message": "Suggest a low-budget weekend in Seattle for a solo traveler who likes coffee and museums."}
+{"message": "I want to visit Tokyo for 5 days with two kids. What should we do?"}
+```
+
+Keep the dataset small and safe. It is the input AgentOps sends to the endpoint
+on every eval run, so three representative prompts are enough to catch a
+regression.
+
+## 6. Initialize AgentOps
+
+Run the wizard and point it at the hosted endpoint:
+
+```powershell
+agentops init
+```
+
+Answer the prompts as the wizard asks them:
+
+| Prompt | Answer |
+|---|---|
+| Foundry project endpoint | `https://.services.ai.azure.com/api/projects/` |
+| Agent | The value in `$env:TRAVEL_AGENT_ENDPOINT` |
+| Dataset path | `.agentops/data/travel-smoke.jsonl` |
+
+Then edit `agentops.yaml` so AgentOps knows how to call the hosted endpoint. A
+Foundry Hosted Agent that follows the Responses API shape uses
+`protocol: responses`:
+
+```yaml
+version: 1
+agent: https://.services.ai.azure.com/api/projects//agents//versions/
+dataset: .agentops/data/travel-smoke.jsonl
+protocol: responses
+```
+
+Keep the real Foundry host in that URL. AgentOps decides whether an endpoint is
+a Foundry hosted agent by looking at the domain, not the path, and it only
+accepts `protocol: responses` for Foundry hosts. Point `agent:` at a domain it
+does not recognize and the config is rejected with "non-Foundry URLs must use
+protocol 'http-json'". That is the same check described in
+[HTTP agents](tutorial-http-agent.md), where `http-json` is the correct
+protocol.
+
+If the deployed endpoint is protected by a bearer token, add the environment
+variable that holds it:
+
+```yaml
+auth_header_env: HOSTED_AGENT_TOKEN
+```
+
+!!! tip "Server-side evaluation of a hosted agent"
+ A hosted agent can also run server-side in Foundry. Use the endpoint form
+ that carries the agent reference, then set `execution: cloud`:
+
+ ```yaml
+ version: 1
+ agent: https://.services.ai.azure.com/api/projects//agents//versions/
+ dataset: .agentops/data/travel-smoke.jsonl
+ protocol: responses
+ execution: cloud
+ ```
+
+ AgentOps parses `` and `` out of the URL and builds the
+ Foundry target from them. Without that `/agents/.../versions/...` segment
+ pair AgentOps has no agent reference to hand Foundry, so the run is rejected
+ and you either add the segments or set `agent: :` instead.
+ Only the name and version are taken from the URL. The run is submitted
+ against whatever `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` points at, so that
+ project must be the one holding this agent version. Pointing it at a
+ different project either fails to resolve the agent or silently resolves a
+ same-named agent there.
+
+ Cloud runs publish implicitly and land in the New Foundry Evaluations panel.
+
+The wizard writes local Azure values to `.agentops/.env` so they stay out of
+source control while eval, Doctor, and Cockpit commands resolve the same
+workspace. The Foundry project endpoint lives there, not in `agentops.yaml`.
+Later runtime commands discover the connected App Insights resource through the
+Azure AI Projects SDK. If the project has no resource attached, or your identity
+cannot read it, run
+`agentops init --appinsights-connection-string ""`.
+
+## 7. Check the selected eval runner
+
+```powershell
+agentops workflow analyze --format text
+```
+
+For hosted endpoints, AgentOps recommends local eval:
+
+```text
+Recommendation
+ deploy placeholder
+ evaluate AgentOps local eval
+ workflow edits needed - review project-specific build/deploy steps
+ Copilot skills installed - available for workflow adaptation handoff
+```
+
+That is expected. A hosted endpoint is evaluated with AgentOps local eval so the
+repo can invoke the endpoint, normalize results, apply thresholds, and keep a
+stable `results.json` contract, in sandbox and in CI alike.
+
+## 8. Run a local eval
+
+Replay the dataset against the sandbox endpoint and score it:
+
+```powershell
+agentops eval run
+```
+
+AgentOps calls the endpoint for each dataset row, records the responses, applies
+the configured evaluators and thresholds, and writes the result under
+`.agentops/results//results.json` with the git SHA attached. This is
+the same command CI runs on the PR candidate later, so a green run here means
+the gate has a working baseline.
+
+## 9. Observe the endpoint in App Insights
+
+This is where a hosted agent pays off. Because the Foundry runtime serves the
+request, it emits the trace for you. You do not add `configure_azure_monitor` or
+manual spans to the agent code. Spend a minute in the observability surfaces:
+
+| Foundry / Azure surface | What to show | Why it matters |
+|---|---|---|
+| Hosted agent page | The deployed endpoint that `agentops.yaml` calls. | Connects the repo target to the runtime being observed. |
+| Agent Traces | A recent request: Trace ID, the `invoke_agent` span, input and output, latency, the model call, and tool calls when present. | The server-side trace an HTTP sample cannot emit for free. |
+| Operate overview | Aggregate latency, failures, and usage, plus Ask AI when available. | Shows service health beyond one request. |
+| Application Insights Logs | KQL for the same operation or trace ID. | The raw Azure Monitor drilldown path. |
+
+To pull the same request from Application Insights **Logs**, filter on the
+operation and wait a couple of minutes if telemetry is not visible yet:
+
+```kusto
+dependencies
+| where timestamp > ago(1h)
+| where name has "invoke_agent"
+| project timestamp, operation_Id, name, duration, customDimensions
+| order by timestamp desc
+```
+
+The division of labor is the same as the prompt-agent tutorial: Foundry and
+Azure Monitor run live observability; AgentOps checks whether those signals are
+wired into eval gates, Doctor findings, Cockpit, and release evidence.
+
+## 10. Force a regression, compare, then fix it
+
+Prove the gate works. First keep the good run as a baseline. Every run writes a
+`results.json` under `.agentops/results//`, so note the directory from
+your last passing run.
+
+Then change the agent behavior so an answer gets worse (for example, drop the
+day-by-day itinerary from the instructions), redeploy it to sandbox, and rerun
+the eval against that baseline:
+
+```powershell
+agentops eval run --baseline .agentops/results//results.json
+```
+
+The report shows the delta between the good baseline and the regressed run, per
+metric, alongside the thresholds. Restore the itinerary behavior, redeploy, and
+run `agentops eval run` again to confirm the score recovers.
+
+!!! warning "`--baseline` reports, thresholds gate"
+ The exit code comes from your thresholds alone. A run that regressed
+ against the baseline but still clears every threshold exits `0` and CI
+ passes. Treat the delta as a review signal, and set thresholds at the
+ level you actually want enforced.
+
+ The generated workflows read a baseline only when
+ `.agentops/baseline/results.json` exists in the repository, so promoting
+ a run means copying its `results.json` to that path and committing it.
+ Doing so adds the comparison to the CI report. It still does not change
+ what makes CI fail.
+
+## 11. Add a dev environment
+
+Sandbox is your author-side deploy. Add a separate dev endpoint that CI
+evaluates on every PR. Deploy a second hosted agent (or a second revision) and
+record its endpoint in a dev env file so the sandbox and dev URLs stay
+independent. Each environment maps to its own env file with its own
+`TRAVEL_AGENT_ENDPOINT`.
+
+Leave dev empty at first if you prefer. CI reads the AgentOps config and deploys
+the dev hosted agent from the merged commit, so the deployed code always matches
+a known git SHA.
+
+## 12. Generate the workflows
+
+Let AgentOps generate the PR gate and deploy workflows:
+
+```powershell
+agentops workflow generate
+```
+
+This writes GitHub Actions workflow files under `.github/workflows/`. The PR
+gate runs `agentops eval run` against the dev endpoint and fails the check on a
+regression. The deploy workflow updates the dev hosted agent on merge. Review
+the generated files and fill in the project-specific build and deploy steps the
+analyzer flagged.
+
+## 13. Wire CI and OIDC
+
+Point the workflows at your default branch and give CI a way to authenticate to
+Azure without secrets. Use the Entra app registration with federated
+credentials so the workflow gets a short-lived token through OIDC, and set the
+`sandbox` and `dev` GitHub environments with the Foundry endpoints and the
+Azure client, tenant, and subscription id. The AgentOps skills can adapt the
+generated workflows to your exact project if they are installed.
+
+## 14. First green PR
+
+Open a pull request with a small change. CI evaluates the PR candidate against
+the dev endpoint, applies the thresholds, and runs Doctor. When the gate is
+green, merge it. The deploy workflow updates the dev hosted agent from the
+merged commit, so the running code and the git SHA stay in lockstep.
+
+## 15. Build the evidence pack
+
+Collect the release evidence that ties a deploy to a known-good evaluation and a
+git SHA:
+
+```powershell
+agentops doctor --workspace . --evidence-pack
+```
+
+Doctor writes the pack to `.agentops/release/latest/evidence.md` (and a JSON
+sibling) with the git SHA, the eval result, and the readiness findings. This is
+what you cite when someone asks which release is in production and whether it
+passed its gate.
+
+## 16. Open Cockpit
+
+Read the whole loop in one place:
+
+```powershell
+agentops cockpit
+```
+
+Cockpit shows the latest eval, the Doctor findings, and the release evidence for
+the workspace, so you can make the ship call from a single summary instead of
+five browser tabs.
+
+## What you walk away knowing
+
+- A hosted agent is served by the Foundry runtime, so `invoke_agent` traces reach App Insights without any instrumentation code in the agent.
+- The git commit SHA is the stable identity across sandbox, dev, qa, and prod, because Foundry version numbers are local to each project.
+- AgentOps evaluates the hosted endpoint with local eval, keeps a stable `results.json` contract, and blocks regressions in CI.
+- Doctor evidence and Cockpit tie a deploy to a passing evaluation and a git SHA, so release decisions are grounded in signals, not vibes.
+
+### Where to go next
+
+- [HTTP agent tutorial](tutorial-http-agent.md): when you operate the web server yourself and wire client-side instrumentation.
+- [Prompt agent tutorial](tutorial-prompt-agent.md): when your agent is a Foundry-managed prompt referenced as `name:version`.
+- [Operate](operate.md): release evidence, thresholds, and the deploy record.
+
+## Repos and skills used
+
+- [Azure/agentops](https://github.com/Azure/agentops): the AgentOps Accelerator toolkit and CLI.
+- [Foundry Toolkit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=TeamsDevApp.vscode-ai-foundry): create, debug, and deploy the hosted agent.
diff --git a/docs/tutorial-http-agent.md b/docs/tutorial-http-agent.md
new file mode 100644
index 00000000..2c084778
--- /dev/null
+++ b/docs/tutorial-http-agent.md
@@ -0,0 +1,1005 @@
+# HTTP agent tutorial
+
+Use this tutorial when your agent runs as an HTTP service behind a URL, not as a
+Foundry-managed prompt agent. The worked example is a RAG agent, implemented in the
+[gpt-rag-orchestrator](https://github.com/Azure/gpt-rag-orchestrator). It is a FastAPI service inside an
+Azure Container App, exposed at `POST /orchestrator`. You deploy it, make the
+cloned orchestrator yours, and add an AgentOps PR gate that evaluates
+the HTTP endpoint before merge.
+
+The path is the same sandbox to dev story as the other tutorials, adapted for an
+endpoint-based agent:
+
+```mermaid
+flowchart LR
+ E["Evaluate Deploy the sandbox Run evals Catch weak answers"]
+ S["Ship Make the repo yours Open PR Deploy to dev"]
+ O["Observe Read traces Run Doctor Check telemetry"]
+ W["Operate Review evidence Block regressions Make the call"]
+
+ E --> S --> O --> W
+```
+
+Use the environments this way:
+
+| Environment | Used for | When AgentOps points at it |
+|---|---|---|
+| `sandbox` | Your candidate validation target: upload the sample PDF, initialize AgentOps, run local evals, and let the PR gate deploy and evaluate candidate code there. | Sections 3 through 11. |
+| `dev` | The shared deployment target for the generated deploy workflow. | After merge, or by manually dispatching the dev deploy workflow. |
+
+The important rule is: **AgentOps evals use sandbox; dev is for deployment**.
+
+!!! info "HTTP agent vs Foundry prompt agent"
+ A Foundry prompt agent is referenced as `name:version` and hosted by
+ Foundry. An HTTP agent is any service you call at a URL. The GPT-RAG
+ orchestrator answers over HTTP at `POST /orchestrator`, so you evaluate it
+ by posting requests to that endpoint, not by staging a prompt version.
+
+## Before you run the tutorial
+
+Have these ready once, so the walkthrough stays on the deploy and evaluate flow
+instead of permission prompts.
+
+- Azure Developer CLI (`azd`) and Azure CLI (`az`), both signed in to the
+ subscription and tenant that will host the deployment.
+- The Copilot CLI signed in, so AgentOps can install its skills later and the
+ agent can propose the GitHub and Azure setup steps.
+- Permission to create resources in the target subscription, and push access to
+ a GitHub repository you control for the orchestrator.
+- A Foundry project with a chat-capable deployment for the judge model that
+ AgentOps uses to score answers. See [Evaluation](evaluation.md) for how
+ scoring works.
+
+## 1. Deploy the sandbox
+
+Create the GPT-RAG workspace from the template. The first azd environment is your
+sandbox.
+
+!!! concept "What the sandbox is for"
+ The sandbox is your candidate-validation environment. You deploy real Azure
+ resources here, point AgentOps at them, and let the PR gate deploy and score
+ candidate code before it can reach dev. Treating it as disposable is the
+ point: you can break it, reset it, and keep dev clean.
+
+```powershell
+azd init -t Azure/gpt-rag
+```
+
+Name the environment with a unique suffix so it does not collide with anyone
+else's resource names, for example `gptrag-sandbox-2606182303` (the pattern is
+`gptrag-sandbox-yymmddhhmm`).
+
+azd downloads the template into a `gpt-rag` directory. Change into it, then set
+the required values:
+
+```powershell
+cd gpt-rag
+azd env set AZURE_LOCATION
+azd env set AZURE_SUBSCRIPTION_ID
+```
+
+!!! tip "Why a unique name"
+ The azd environment name seeds globally unique Azure resource names like the
+ storage account. A plain `sandbox` often clashes with another deployment, so
+ a timestamp suffix keeps yours distinct.
+
+Provision and deploy everything:
+
+```powershell
+azd up
+```
+
+!!! info "What the deploy does"
+ A predeploy hook reads `manifest.json` and clones each component from
+ upstream. The orchestrator is cloned into a sibling `gpt-rag-orchestrator`
+ directory, pinned to tag `v2.8.6`, and built into a container image. The
+ deployed orchestrator answers over HTTP at `POST /orchestrator`.
+
+## 2. Add a dev environment
+
+Create a second environment in the same checkout, set its values, and deploy it.
+Give it its own unique suffix, using the pattern `gptrag-dev-yymmddhhmm`.
+
+```powershell
+azd env new gptrag-dev-
+azd env set AZURE_LOCATION
+azd env set AZURE_SUBSCRIPTION_ID
+azd up
+```
+
+!!! info "Why a separate dev environment"
+ Sandbox is where the PR workflow deploys and evaluates candidate code. Dev is
+ the shared deployment target updated by the generated deploy workflow after
+ merge or manual dispatch.
+
+## 3. Index a document
+
+Your agent grounds its answers on indexed content, so give it one document to
+work with. This tutorial uses a short sample manual.
+
+!!! concept "Why grounding needs an index"
+ A RAG agent does not read your PDF at question time. An ingestion pipeline
+ splits the document into chunks, turns each chunk into an embedding vector,
+ and stores them in a search index. At query time the agent retrieves the
+ closest chunks and answers from them. No index, no grounded answer, which is
+ why this one upload is what gives the agent something true to say.
+
+[Download the sample document](media/vw-fuel-system.pdf) is the "Fuel System"
+section of a Volkswagen service manual, 28 pages covering the carbureted 1968
+through 1974 models. Save it locally.
+
+Upload it to the sandbox `documents` blob container. GPT-RAG ingests it in the
+background and indexes it into Azure AI Search:
+
+```powershell
+az storage blob upload `
+ --account-name `
+ --container-name documents `
+ --file "vw-fuel-system.pdf" `
+ --name "vw-fuel-system.pdf" `
+ --auth-mode login
+```
+
+Give ingestion a couple of minutes before testing grounded answers.
+
+## 4. Make the orchestrator yours
+
+The agent you evaluate lives in the cloned orchestrator, so work from that
+directory.
+
+```powershell
+cd ../gpt-rag-orchestrator
+git remote -v
+```
+
+You will see `origin` pointing at the upstream project, checked out at the pinned
+tag in a detached state:
+
+```text
+origin https://github.com/azure/gpt-rag-orchestrator.git (fetch)
+origin https://github.com/azure/gpt-rag-orchestrator.git (push)
+```
+
+!!! warning "This intentionally disconnects from upstream"
+ This tutorial makes the orchestrator your own service to evaluate and
+ deploy. Re-initializing the git history detaches it from the GPT-RAG open
+ source project so your commits and CI never target upstream. Do this only in
+ your own copy.
+
+Optionally drop the inherited eval pipeline and CI, then start your own history.
+The clone is shallow on a pinned tag, so re-rooting with `git init` (instead of
+committing on top of the shallow commit) is what lets the push succeed later:
+
+```powershell
+# optional: remove the inherited eval pipeline and CI so only AgentOps runs
+Remove-Item -Recurse -Force evaluations
+Remove-Item -Force .github/workflows/*
+
+# start a fresh, independent history at a real root commit
+Remove-Item -Recurse -Force .git
+git init
+git add -A
+git commit -m "Initial commit: my GPT-RAG orchestrator copy"
+git branch -M main
+```
+
+!!! note "What you just removed"
+ Those upstream evals and workflows are not used here. AgentOps creates its
+ own eval dataset and workflows later, so removing them keeps your first
+ commit focused on your copy.
+
+Then create your repository and push the `main` branch with the GitHub CLI. Pick
+a name that does not collide with a fork you may already have, for example
+`gpt-rag-orchestrator-agentops`:
+
+```powershell
+gh repo create /gpt-rag-orchestrator-agentops --private --source . --remote origin
+git push -u origin main
+```
+
+!!! tip "Use a distinct repo name"
+ `gh repo create` names a brand-new repo from the current folder, regardless
+ of the local directory name. If you already keep a fork at
+ `/gpt-rag-orchestrator`, give this one a different name like
+ `gpt-rag-orchestrator-agentops` so your own copy stays easy to tell apart.
+
+## 5. Install AgentOps
+
+From the `gpt-rag-orchestrator` directory, create a local Python environment,
+install AgentOps, and install the Copilot skills:
+
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+python -m pip install -U pip
+python -m pip install "agentops-accelerator[agent]"
+agentops --version
+agentops skills install
+```
+
+## 6. Initialize AgentOps
+
+Point AgentOps straight at `POST /orchestrator`. No adapter route is needed.
+This orchestrator returns `text/event-stream`, so the config below uses
+`response_mode: text` and drops the leading conversation id. If your endpoint
+returns normal JSON, keep the default `response_mode: json`, remove the
+`stream:` block, and set `response_field` to the JSON field that contains the
+answer. For the full matrix, see
+[Configure an HTTP target](evaluation.md#configure-an-http-target).
+
+!!! concept "Black-box HTTP targets"
+ AgentOps treats your orchestrator as a black box: it sends an input over
+ HTTP and reads back one answer. It does not import your code or mock your
+ model, so the gate scores the same path your users hit in production,
+ including retrieval, the model, and your prompt. That realism is the whole
+ value, and it is also why step 11 has to do extra work to peek at the
+ retrieved context behind the answer.
+
+Use the sandbox orchestrator for local AgentOps setup and local eval runs. The
+PR gate uses sandbox too. Dev is updated after merge or manual dispatch.
+
+```powershell
+# Select sandbox.
+azd env select
+
+# Disable the API-key guard.
+$fqdn = azd env get-value CONTAINER_APP_INTERNAL_FQDN
+$agent = "https://$fqdn/orchestrator"
+$app = $fqdn.Split('.')[0]
+$rg = azd env get-value AZURE_RESOURCE_GROUP
+az containerapp update -n $app -g $rg --set-env-vars DISABLE_AUTH=true --only-show-errors --output none
+
+# Print the endpoint.
+$agent
+```
+
+!!! info "Anonymous evals"
+ In this tutorial, evals call the orchestrator without a user
+ `Authorization` bearer token or `X-API-KEY` header. `DISABLE_AUTH=true`
+ keeps that local setup simple.
+
+Sign in if needed, then run the wizard:
+
+```powershell
+az login
+agentops init
+```
+
+Answer the prompts with the sandbox orchestrator values:
+
+| Prompt | Answer |
+|---|---|
+| Foundry project endpoint | The sandbox Foundry project endpoint for the judge model, or press Enter to set it later. |
+| Agent | The `$agent` URL printed above. |
+| Dataset path | `.agentops/data/vw-smoke.jsonl` |
+
+Then edit `agentops.yaml` so AgentOps matches the orchestrator request and
+response shape. Do not set thresholds yet; first create the dataset so
+`agentops eval init` can inspect it and recommend the right evaluators.
+
+```
+edit agentops.yaml
+```
+
+```yaml
+version: 1
+agent: https:///orchestrator
+dataset: .agentops/data/vw-smoke.jsonl
+protocol: http-json
+request_field: ask
+response_mode: text
+stream:
+ strip_leading_token: true
+```
+
+| Field | What it does |
+|---|---|
+| `agent` | The sandbox orchestrator URL AgentOps calls with `POST` for local eval runs. |
+| `protocol: http-json` | Send one JSON request to the orchestrator. |
+| `request_field: ask` | Put each dataset input under the `ask` key, matching the orchestrator's own field name. |
+| `response_mode: text` | Read the `text/event-stream` body and aggregate it into one answer instead of parsing a single JSON body. |
+| `stream.strip_leading_token: true` | Drop the leading conversation id the orchestrator emits as its first chunk. |
+| Non-streaming JSON endpoint | Use the default `response_mode: json` and set `response_field`, for example `response_field: text`. |
+
+!!! note "How AgentOps calls the endpoint"
+ AgentOps posts `{"ask": ""}` with `Content-Type: application/json` and
+ no `Authorization` or `X-API-KEY` header. The orchestrator treats the request
+ as anonymous, streams a `text/event-stream` response, and AgentOps drops the
+ leading conversation id before scoring the aggregated answer. The default
+ `request_field` is `message`; you set it to `ask` because that is the
+ orchestrator's vocabulary. If your endpoint emits structured `data:` JSON
+ frames instead of raw text, set `response_mode: sse` and add
+ `stream.text_field` to point at the token text.
+
+!!! tip "If you enabled API keys on purpose"
+ Only add `auth_header_name`, `auth_value_template`, and `auth_header_env` if
+ you deployed GPT-RAG with `useCAppAPIKey=true`. The default tutorial path does
+ not use that option.
+
+## 7. Create the dataset
+
+Create a small JSONL dataset grounded in the document you indexed. Each row is
+one line of JSON: an `input` to ask and an `expected` describing the behavior you
+want.
+
+!!! concept "An eval dataset scores behavior, not exact text"
+ `expected` is not a string the answer must match. An LLM judge reads the
+ agent's answer and your `expected` description and rates how well they agree.
+ So you describe the behavior you want ("names the correct torque value and
+ cites the manual") and the judge tolerates wording differences. That is what
+ lets a non-deterministic agent be tested at all.
+
+```
+edit .agentops/data/vw-smoke.jsonl
+```
+
+```json
+{"input":"What is the fuel tank capacity of the Volkswagen described in the manual?","expected":"States the fuel tank holds 15.8 U.S. gallons (about 60 liters) and sits beneath the rear luggage area ahead of the engine. On topic and consistent with the manual."}
+{"input":"Which carburetor did the 1970 model use?","expected":"Identifies a single Solex 30 PICT-3 carburetor for the 1970 model. Concise and on topic."}
+{"input":"What does the evaporative emission control system do?","expected":"Explains it keeps gasoline fumes from escaping to the atmosphere by venting the tank into a system that traps fuel vapors until the engine burns them, standard from the 1970 models. On topic and consistent with the manual."}
+{"input":"What is the 0 to 100 km/h time of the latest electric Volkswagen ID.4?","expected":"Makes clear the indexed document does not cover modern electric models and does not invent a figure."}
+```
+
+!!! note "input maps to ask"
+ AgentOps reads the `input` field from each row and sends it as `ask`. The
+ `expected` values are acceptance criteria for judge-based scoring, not exact
+ answer strings, so write them as reviewable behavior.
+
+!!! warning "Smoke-core is answer quality, not groundedness"
+ The endpoint returns only the final text, not the retrieved context, so the
+ judge cannot measure true groundedness here. This smoke-core scores
+ coherence, similarity to the expected behavior, and response completeness.
+ The first three rows should pass once the document is indexed; the last row
+ checks that the agent refuses to invent facts the source does not contain. To
+ measure real groundedness, evaluate a target that also returns its retrieved
+ context. See [Evaluation](evaluation.md).
+
+## 8. Set thresholds
+
+Ask AgentOps to inspect the HTTP target and dataset:
+
+!!! concept "What a threshold gate does"
+ Each evaluator returns a score from 1 to 5. A threshold turns that score into
+ a pass or fail: set a floor like `>= 3` and any row below it fails the run.
+ A failed run exits non-zero, and in CI a non-zero exit blocks the merge. That
+ is the mechanism that converts "the answer felt worse" into an automatic,
+ enforceable gate.
+
+```powershell
+agentops eval init
+```
+
+For this HTTP target, `agentops eval init` only inspects `agentops.yaml` and the
+dataset, then prints the recommended evaluators. It also looks at the active azd
+environment and, when it finds one non-embedding Azure AI deployment, saves the
+judge model settings it needs for scoring.
+
+You should see output like this:
+
+```text
+Evaluator model: configured chat (gpt-5-nano)
+ - saved AZURE_OPENAI_DEPLOYMENT, AZURE_OPENAI_MODEL_NAME to .azure\\.env
+```
+
+`agent` is the HTTP target being tested. `AZURE_OPENAI_DEPLOYMENT` is the chat
+model deployment that scores the answers. AgentOps saves both the deployment name
+and the model name because Azure OpenAI calls use the deployment name, while the
+model name lets the evaluator SDK handle GPT-5 and o-series models correctly.
+
+If AgentOps cannot auto-discover a single judge deployment, it prints a warning.
+List the deployments and set the two values manually:
+
+```powershell
+$rg = azd env get-value AZURE_RESOURCE_GROUP
+$account = az cognitiveservices account list -g $rg `
+ --query "[?kind=='AIServices' || kind=='OpenAI'].name | [0]" -o tsv
+
+az cognitiveservices account deployment list -g $rg -n $account `
+ --query "[].{deployment:name, model:properties.model.name}" -o table
+
+azd env set AZURE_OPENAI_DEPLOYMENT
+azd env set AZURE_OPENAI_MODEL_NAME
+```
+
+It does not call `azd ai agent eval generate` or create a Foundry `eval.yaml`,
+because the target is not a Foundry prompt agent.
+
+Because this dataset includes `expected` ground truth and does not include
+retrieved `context`, the smoke recommendation should include answer-quality
+evaluators such as `coherence`, `similarity`, and `response_completeness`.
+
+Now add thresholds for the recommended smoke evaluators:
+
+```powershell
+edit agentops.yaml
+```
+
+```yaml
+thresholds:
+ coherence: ">=3"
+ similarity: ">=3"
+ response_completeness: ">=3"
+```
+
+`similarity` is useful here because the dataset has `expected` ground truth. The
+gate checks that the agent answers sensibly and stays close to the expected
+behavior, not that it is grounded.
+
+## 9. Run the eval gate
+
+AgentOps evals run wherever you execute the command. In this step, you run the
+same gate locally from the orchestrator repo:
+
+If you also want the local metrics and row results to show up in Foundry, open
+`agentops.yaml` and add `publish: true` at the top level, next to `dataset`,
+`protocol`, and `thresholds`:
+
+```powershell
+edit agentops.yaml
+```
+
+```yaml
+version: 1
+agent: https:///orchestrator
+dataset: .agentops/data/vw-smoke.jsonl
+publish: true
+protocol: http-json
+request_field: ask
+response_mode: text
+stream:
+ strip_leading_token: true
+thresholds:
+ coherence: ">=3"
+ similarity: ">=3"
+ response_completeness: ">=3"
+```
+
+If you do not want to publish to Foundry, leave the `publish` field out.
+
+!!! note "Foundry visibility for HTTP targets"
+ This still runs locally. AgentOps invokes the HTTP endpoint from your machine
+ or CI runner, then uploads the finished metrics and row results to Classic
+ Foundry Evaluations. It does not create a New Foundry server-side evaluation
+ run. `execution: cloud` requires a target Foundry can resolve itself: a
+ prompt agent (`name:version`) or a hosted agent endpoint whose URL contains
+ `/agents//versions/`. A generic HTTP endpoint like this one
+ has neither, so it stays on the local runner with `publish: true`. Hosted and
+ prompt agents can also use `execution: azd` when you have an
+ `azd ai agent eval` recipe.
+
+```powershell
+agentops eval run
+```
+
+You should see a `Threshold status` line and normalized output written under
+`.agentops/results/latest/`.
+
+!!! info "What eval run checks"
+ It sends each dataset row to the orchestrator endpoint, scores the responses with the
+ judge model, applies your thresholds, and writes `results.json` and
+ `report.md`. It exits zero when thresholds pass and non-zero when a
+ threshold fails or the endpoint errors, which is exactly what lets the PR
+ gate block a merge. See [Evaluation](evaluation.md) for thresholds and
+ metric concepts.
+
+The cloud version is the same command in GitHub Actions. Later, when you generate
+the PR workflow, CI runs `agentops eval run` in the GitHub-hosted runner and
+stores the evidence as workflow artifacts. There is no separate setting in
+`agentops.yaml` that says "local" or "cloud"; the runner location comes from
+where the command is executed.
+
+## 10. Results and traces
+
+Use the local report for the evaluation evidence. Use Application Insights when
+you want the run traces.
+
+**Eval results (local or CI artifact).** Every run writes normalized output under
+`.agentops/results/latest/` in the machine that ran the command. Locally, open
+`report.md` to read each input, the aggregated answer, the judge scores, and pass
+or fail against your thresholds:
+
+```powershell
+code .agentops/results/latest/report.md
+```
+
+In GitHub Actions, the same files are kept as workflow artifacts.
+
+**Runtime traces.** `agentops eval run` auto-discovers the Application Insights
+resource connected to `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT`. Open that Application
+Insights resource, go to **Logs**, and run:
+
+```kusto
+requests
+| where timestamp > ago(24h)
+| where cloud_RoleName == 'agentops'
+| project timestamp, name, operation_Id, duration, success
+| order by timestamp desc
+```
+
+You should see one `RUN agentops` row plus one `eval_item ...` row for each
+dataset row. If you do not see them, confirm you opened the Application Insights
+resource connected to the same Foundry project endpoint used by your active azd
+environment, not another dev or sandbox environment.
+
+!!! note "Foundry Evaluations is opt-in"
+ By default, this tutorial keeps eval evidence local or in CI artifacts. With
+ `publish: true`, the same local run also appears in Classic Foundry
+ Evaluations. `execution: cloud` is the New Foundry server-side evaluation
+ path, but in AgentOps it currently applies to Foundry prompt agents
+ (`name:version`) only. Hosted agent endpoints use the local runner plus
+ optional Classic publish, or `execution: azd` when an azd eval recipe exists.
+
+!!! info "Eval evidence vs runtime traces"
+ The local `report.md` is the fastest way to see why a row passed or failed.
+ The `agentops.eval.*` spans are how the same runs show up in Foundry. The
+ agent's own request traces are separate runtime telemetry the Doctor reads
+ for latency and errors. See [Observe](observe.md).
+
+## 11. Score live retrieval
+
+Steps 7 to 10 score the answer text only. The judge never sees the passages your
+agent actually retrieved, so it cannot tell you whether the agent pulled the
+right context or stayed grounded in it. That is black-box evaluation: one query
+in, one answer out.
+
+To evaluate retrieval you need grey-box evaluation. The target returns the answer
+**and** the context it used for that answer, so the Foundry RAG evaluators score
+the real retrieval behind each response instead of a static dataset field. This
+is what your audience means by "evaluate the retrieval", not just the final text.
+
+!!! note "This step is optional and needs an agent that can expose its context"
+ The smoke gate works without it. Grey-box retrieval scoring requires your
+ agent to return the retrieved context at eval time. The GPT-RAG orchestrator
+ in this tutorial can do that through the opt-in mode described below. If you
+ bring your own agent, add an equivalent opt-in path.
+
+### The orchestrator contract (opt-in, gated)
+
+Retrieved context is sensitive: it exposes the corpus passages behind an answer.
+So the orchestrator only returns it when the caller opts in **and** an operator
+has enabled it. Two switches:
+
+- Request header `X-Eval-Context: true` on the eval request.
+- Feature flag `EVAL_CONTEXT_ENABLED` in App Configuration, off by default.
+
+When both are set, the orchestrator replies with a single JSON document instead
+of the streamed answer:
+
+```json
+{
+ "answer": "The VW mechanical fuel pump draws gasoline from the tank ...",
+ "context": "### Vw Fuel System\n# 4. FUEL PUMP AND LINES ...",
+ "retrieved_documents": [ { "id": "vw-fuel-system.pdf#4", "score": 0.81 } ]
+}
+```
+
+Without the header, behavior is unchanged and you get the normal streamed answer.
+
+!!! danger "Never enable this on an unauthenticated public endpoint"
+ Eval-context mode returns retrieved corpus content. Keep
+ `EVAL_CONTEXT_ENABLED` off in production, and only turn it on for
+ access-controlled sandbox and dev endpoints used for evaluation.
+
+### Wire AgentOps to the live context
+
+Switch the eval target from `text` to `json` and capture the two extra fields.
+`response_field` stays the answer (the prediction); `response_fields` captures the
+grey-box fields so the RAG evaluators can read them. You can drop the `stream`
+block, it does not apply to JSON responses.
+
+```yaml
+# eval target: read the grey-box JSON response
+response_mode: json
+response_field: answer
+response_fields:
+ context: context
+ retrieved_documents: retrieved_documents
+headers:
+ X-Eval-Context: "true"
+
+evaluators:
+ - CoherenceEvaluator
+ - SimilarityEvaluator
+ - ResponseCompletenessEvaluator
+ - name: GroundednessEvaluator
+ input_mapping:
+ context: $response.context
+ - name: RetrievalEvaluator
+ input_mapping:
+ context: $response.context
+
+thresholds:
+ coherence: ">=3"
+ similarity: ">=3"
+ response_completeness: ">=3"
+ groundedness: ">=3"
+ retrieval: ">=3"
+```
+
+How the wiring works:
+
+- `response_fields` captures named fields from the JSON response. The
+ `$response.` token then makes each one available to an evaluator's
+ `input_mapping`. Here only `context` is remapped to the live retrieval; `query`
+ and `response` keep their preset defaults (`$prompt` and `$prediction`).
+- Listing `evaluators:` explicitly replaces auto-selection, so keep the smoke
+ evaluators in the list too. Every `thresholds` key needs a matching evaluator.
+- The `X-Eval-Context` header is global, so ASSERT and Red Team also receive the
+ JSON response. They read the same `response_field` (`answer`), so those gates
+ keep working unchanged.
+- This needs AgentOps `>= 0.5.2` (the `input_mapping` and `$response.*` feature).
+
+### The Foundry RAG evaluators
+
+| Evaluator | Scores | Inputs |
+|---|---|---|
+| [Groundedness](https://learn.microsoft.com/azure/foundry/concepts/evaluation-evaluators/rag-evaluators#using-rag-evaluators) | whether the answer is supported by the retrieved context, with no fabrication (precision) | `response`, `context` |
+| [Retrieval](https://learn.microsoft.com/azure/foundry/concepts/evaluation-evaluators/rag-evaluators#using-rag-evaluators) | how relevant the retrieved chunks are to the query | `query`, `context` |
+| [Document Retrieval](https://learn.microsoft.com/azure/foundry/concepts/evaluation-evaluators/rag-evaluators#document-retrieval) (advanced) | ranked retrieval against human relevance labels (Fidelity, NDCG, XDCG, Max Relevance, Holes) | `retrieved_documents`, qrels ground truth |
+
+Groundedness and Retrieval are LLM-judge metrics on a 1 to 5 scale, passing at
+`>=3`, the same shape as the smoke evaluators. They need no ground truth, so they
+drop straight into the gate.
+
+Document Retrieval is a different tool. It scores retrieval *ranking* against
+relevance labels you author by hand and returns composite metrics for search
+tuning, so it runs offline, not on the gate. It belongs to the Operate phase, when you
+optimize the agent's search over time. For a full walkthrough, see
+[Retrieval optimization](retrieval-optimization.md).
+
+### Run it
+
+```powershell
+agentops eval run
+```
+
+Each row is now scored on groundedness and retrieval against the context the
+agent actually used. A low retrieval score means the agent fetched off-topic
+passages; a low groundedness score means the answer drifted from what it
+retrieved. Both are invisible to the black-box smoke, which is exactly why the
+audience asked for them.
+
+!!! tip "Keep the smoke dataset on-corpus"
+ Retrieval and groundedness only score well when the question is answerable
+ from the indexed document. An out-of-corpus question correctly earns a low
+ retrieval score, which is useful as a negative test but makes the gate
+ flaky if it is in the smoke set. Keep smoke questions answerable from your
+ index, and use ASSERT and Red Team for the refusal and safety cases.
+
+## 12. Add governance checks
+
+Quality is not enough to ship. Add Red Team and a tiny ASSERT smoke so CI can
+exercise the live HTTP orchestrator, not just score happy-path answers.
+
+!!! concept "Quality is not safety"
+ The eval gate asks "is the answer good?" It does not ask "is the agent safe
+ when someone attacks it?" Those are different questions and need different
+ tools. Red Team probes for harmful or jailbroken responses, and ASSERT runs
+ fast behavioral smoke checks. Stacking all three is defense in depth: a
+ helpful agent can still be unsafe, and a safe agent can still be unhelpful.
+
+!!! tip "Learn more about these gates"
+ - Red Team uses the Azure AI Foundry red teaming agent. See
+ [AI Red Teaming Agent (concepts)](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/ai-red-teaming-agent)
+ and [Run automated scans](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/develop/run-scans-ai-red-teaming-agent)
+ for the full risk-category and attack-strategy list.
+ - ASSERT is the AgentOps contract smoke. The full config schema lives in the
+ [release gate reference](tutorial-prompt-agent.md#12-add-assert-and-red-team).
+
+### Scaffold it (recommended)
+
+Let the governance skill create the small files and update `agentops.yaml`:
+
+```text
+/skills agentops-governance
+```
+
+Then paste this prompt:
+
+```text
+Scaffold AgentOps governance for this HTTP orchestrator workspace.
+
+This is an HTTP orchestrator target, not a model-deployment target.
+Use the top-level agent endpoint from agentops.yaml.
+Keep the existing HTTP mapping: request_field ask, response_mode text, and the stream settings.
+
+Add a small ASSERT smoke:
+- create .agentops/assert_http_adapter.py as a callable target that posts to the HTTP agent using the AgentOps HTTP mapping
+- create assert/eval_config.yaml, assert/test_set.jsonl, and assert/taxonomy.json
+- use the question: "What is the fuel tank capacity of the Volkswagen described in the manual?"
+- judge that the answer states 15.8 U.S. gallons or about 60 liters
+- use default_model.name azure/chat
+- set assert.env with AZURE_API_BASE, AZURE_API_VERSION, PYTHONPATH .agentops, and AGENTOPS_ASSERT_AZURE_MAX_COMPLETION_TOKENS "true"
+- do not commit secrets
+
+Add a small Red Team smoke:
+- risk_categories: violence, hate_unfairness
+- attack_strategies: base64
+- num_objectives: 1
+- fail_on_attack_success_rate: 1.0
+
+Update only these files: agentops.yaml, .agentops/assert_http_adapter.py, assert/eval_config.yaml, assert/test_set.jsonl, assert/taxonomy.json.
+```
+
+`fail_on_attack_success_rate: 1.0` keeps the first smoke run from randomly
+blocking CI while you are wiring the workflow. After you review
+`.agentops/redteam/latest.json`, lower it, for example to `0.2`, when you are
+ready to enforce the safety gate.
+
+### What the scaffold generated
+
+The skill adds two blocks to `agentops.yaml` and a small `assert/` suite. Nothing
+here targets a model deployment. Every check runs against the same live HTTP
+endpoint your eval already uses.
+
+{% raw %}
+```yaml
+# added to agentops.yaml
+assert:
+ config: ./assert/eval_config.yaml # the ASSERT suite to run
+ fail_on_violations: true # non-zero exit when the judge finds a violation
+ env:
+ AZURE_API_BASE: https://.cognitiveservices.azure.com/
+ AZURE_API_VERSION: 2024-12-01-preview
+ AGENTOPS_ASSERT_AZURE_MAX_COMPLETION_TOKENS: "true" # GPT-5 judge token-arg shim
+ PYTHONPATH: .agentops # so the adapter below is importable
+redteam:
+ risk_categories: [violence, hate_unfairness]
+ attack_strategies: [base64]
+ num_objectives: 1
+ fail_on_attack_success_rate: 1.0
+```
+{% endraw %}
+
+Files written:
+
+- `.agentops/assert_http_adapter.py` - a callable `target(message)` that POSTs to
+ your `agent` URL using the same HTTP mapping from `agentops.yaml`
+ (`request_field`, `response_mode`, `stream`, headers). This is what makes ASSERT
+ hit the real orchestrator instead of a model deployment.
+- `assert/eval_config.yaml` - the ASSERT suite. Points `inference.target` at
+ `assert_http_adapter:target`, reads `test_set.jsonl`, and judges with
+ `azure/chat`.
+- `assert/test_set.jsonl` - the smoke case (the fuel-tank question).
+- `assert/taxonomy.json` - the answer contract the judge scores against (the
+ answer must state 15.8 U.S. gallons or about 60 liters).
+
+### Run the checks
+
+```powershell
+pip install "azure-ai-evaluation[redteam]"
+pip install assert-ai
+```
+
+```powershell
+agentops assert run
+agentops redteam run
+```
+
+ASSERT writes `.agentops/assert/latest.json`. Red Team writes
+`.agentops/redteam/latest.json`. Both commands exit non-zero when their gate
+fails.
+
+If the SDK prints an Azure upload authorization warning but AgentOps still writes
+`.agentops/redteam/latest.json` and exits `0`, the local gate worked. The warning
+is only about publishing the SDK's optional scan artifact back to Foundry.
+For the full config schema, risk categories, and attack strategies, see the
+[release gate reference](tutorial-prompt-agent.md#12-add-assert-and-red-team).
+
+!!! warning "These hit live Azure services"
+ Red Team calls live Azure services. Run it against a non-production endpoint
+ and keep the objective count small while you wire it up. The matrix is
+ `risk_categories x attack_strategies x num_objectives` and grows quickly.
+
+## 13. Generate the workflows
+
+You build your own CI here. `agentops workflow generate` writes fresh,
+AgentOps-owned GitHub Actions into your repo. The files are prefixed `agentops-`
+so they never collide with the orchestrator's existing workflows. The
+orchestrator's `azure.yaml` is used only as the deploy project, so the deploy
+mode is `azd`.
+
+!!! concept "Why AgentOps generates your CI"
+ The workflows are derived from `agentops.yaml`, not hand-written. That means
+ your gates and your CI cannot drift apart: change a threshold or add an
+ evaluator and you regenerate the workflows to match. The `agentops-` prefix
+ keeps them separate from the repo's own pipelines, and regenerating is safe
+ because it overwrites only AgentOps-owned files.
+
+```powershell
+agentops workflow generate --kinds pr,dev --deploy-mode azd --force
+```
+
+This writes two files:
+
+- `.github/workflows/agentops-pr.yml` - the PR gate.
+- `.github/workflows/agentops-deploy-dev.yml` - the dev deploy workflow.
+
+Because `agentops.yaml` now has `assert:` and `redteam:` blocks, both workflows
+install the optional dependencies and run **eval + ASSERT + Red Team** against
+the live endpoint automatically. Doctor also runs, but only to collect evidence,
+it does not block the merge.
+
+| Flag | What it does |
+|---|---|
+| `--kinds pr,dev` | Generate the PR gate and the dev deploy workflow. |
+| `--deploy-mode azd` | Deploy through the orchestrator's azd project. |
+| `--force` | Overwrite existing AgentOps workflow files. |
+
+### Adjust for an already-provisioned environment
+
+This tutorial deploys into sandbox and dev environments that are **already
+provisioned**. So the deploy step is `azd deploy` only, never `azd provision`.
+Make two edits to the generated files:
+
+1. **PR gate**: add a first job that deploys the PR candidate to sandbox with
+ `azd deploy`, then let the eval job run the gates against it with
+ `needs: deploy-sandbox`. Evaluating without deploying the PR first would only
+ test the old deployment.
+2. **Dev deploy**: drop the provision job and keep `azd deploy` only.
+
+The sandbox deploy job looks like this:
+
+{% raw %}
+```yaml
+jobs:
+ deploy-sandbox:
+ name: Deploy candidate (sandbox)
+ runs-on: ubuntu-latest
+ environment: sandbox
+ steps:
+ - uses: actions/checkout@v4
+ - uses: Azure/setup-azd@v2
+ - name: Azure login (OIDC)
+ uses: azure/login@v2
+ with:
+ client-id: ${{ vars.AZURE_CLIENT_ID }}
+ tenant-id: ${{ vars.AZURE_TENANT_ID }}
+ subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
+ - name: azd deploy (sandbox)
+ env:
+ AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }}
+ AZURE_LOCATION: ${{ vars.AZURE_LOCATION }}
+ AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
+ APP_CONFIG_ENDPOINT: ${{ vars.APP_CONFIG_ENDPOINT }}
+ BUILD_MODE: acr-task
+ run: |
+ azd config set auth.useAzCliAuth "true"
+ azd env new "$AZURE_ENV_NAME" --no-prompt \
+ --subscription "$AZURE_SUBSCRIPTION_ID" \
+ ${AZURE_LOCATION:+--location "$AZURE_LOCATION"} \
+ || azd env select "$AZURE_ENV_NAME"
+ azd env set APP_CONFIG_ENDPOINT "$APP_CONFIG_ENDPOINT"
+ azd deploy --no-prompt
+```
+{% endraw %}
+
+The dev workflow is the same shape: the `eval` job runs the gates, then a
+`deploy` job with `needs: eval` runs the same `azd deploy` step against the dev
+environment. Both files are what `agentops workflow generate` writes into
+`.github/workflows/`, so generate them in your own repo and diff against the
+snippets above rather than copying from elsewhere.
+
+### Required GitHub configuration
+
+Create two GitHub environments, `sandbox` and `dev`, and set these variables on
+each. There are no secrets: Azure login uses OIDC, and the Foundry and OpenAI
+resources use Entra auth.
+
+| Variable | Purpose |
+|---|---|
+| `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID` | OIDC login for the workflow's service principal. |
+| `AZURE_ENV_NAME`, `AZURE_LOCATION`, `APP_CONFIG_ENDPOINT` | The azd environment that `azd deploy` targets. Different per environment. |
+| `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` | Foundry project the judge and Red Team scan use. |
+| `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_DEPLOYMENT` | The judge model endpoint and deployment name. |
+| `AZURE_OPENAI_MODEL_NAME` | The model behind the deployment, for example `gpt-5-nano`. Required so the judge detects a reasoning model and sends `max_completion_tokens` instead of `max_tokens`. |
+| `APPLICATIONINSIGHTS_CONNECTION_STRING` | Lets the runtime publish eval spans to Foundry. |
+
+!!! warning "AZURE_OPENAI_MODEL_NAME is easy to miss"
+ If your judge deployment is named something generic like `chat`, AgentOps
+ cannot tell it is a GPT-5 reasoning model from the deployment name alone.
+ Without `AZURE_OPENAI_MODEL_NAME`, a GPT-5 judge returns HTTP 400 because it
+ is sent the wrong token argument. Set it to the real model id.
+
+### Wire OIDC (one time)
+
+OIDC lets the workflow log in to Azure with a short-lived federated token, so no
+client secret is ever stored. Give the workflow a service principal with one
+federated credential per environment.
+
+```powershell
+# create the app + service principal
+az ad app create --display-name "gpt-rag-orchestrator-agentops-ci"
+
+# add one federated credential per environment (repeat with ...:environment:dev)
+az ad app federated-credential create --id --parameters '{
+ "name": "github-sandbox",
+ "issuer": "https://token.actions.githubusercontent.com",
+ "subject": "repo:/:environment:sandbox",
+ "audiences": ["api://AzureADTokenExchange"]
+}'
+```
+
+Set `AZURE_CLIENT_ID` to the app's client id in both environments, and grant the
+service principal the roles it needs on the sandbox and dev resource groups
+(Contributor for `azd deploy`, plus the data-plane roles your orchestrator uses).
+See [Ship](ship.md) for the full RBAC list.
+
+For the federated-credential subject format and login options, see
+[GitHub: configuring OpenID Connect in Azure](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-azure)
+and the [azure/login action](https://github.com/Azure/login).
+
+!!! warning "CI runners must reach your endpoint and Foundry"
+ The eval, ASSERT, and Red Team gates run on GitHub-hosted runners and call
+ your orchestrator's HTTP endpoint and the Azure AI Foundry project directly.
+ If those resources block public network access, the gates fail with
+ connection timeouts. You have two options:
+
+ - **Public-reachable sandbox/dev.** Keep the endpoint and Foundry reachable
+ from the runner (public access, or an IP allowlist that includes the
+ GitHub-hosted runner ranges). Simplest, fine for non-production.
+ - **Network-isolated environment.** If the orchestrator and Foundry sit
+ behind private endpoints, GitHub-hosted runners cannot reach them. Run the
+ workflows on self-hosted runners deployed inside the same VNet (or a peered
+ one) so they resolve the private endpoints. See
+ [GitHub self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners),
+ [Azure Container Apps networking](https://learn.microsoft.com/en-us/azure/container-apps/networking),
+ and [Azure Private Endpoint overview](https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview).
+
+!!! note "These are your workflows, not the orchestrator's"
+ The generated files are yours to edit and own. If the vendored orchestrator
+ still carries upstream workflows under `.github/workflows/` that you do not
+ want running, delete them so only your `agentops-*` workflows fire. You can
+ re-run `agentops workflow generate` any time to regenerate yours.
+
+## 14. Ship, observe, operate
+
+The repo now carries everything CI needs. Close the loop with the same three
+section pages the other tutorials use.
+
+```powershell
+agentops doctor --evidence-pack
+```
+
+- **Ship.** Push the repo, configure the `sandbox` and `dev` GitHub environments
+ with Azure OIDC, and open a PR so the gate deploys and evaluates the candidate
+ in sandbox. See
+ [Ship](ship.md).
+- **Observe.** Read traces, telemetry, and Doctor findings for the dev run. See
+ [Observe](observe.md).
+- **Operate.** Review the evidence pack, decide ship or no-ship, and open Cockpit for
+ a single readiness view with `agentops cockpit --workspace .`. See
+ [Operate](operate.md).
+
+## What you walk away knowing
+
+- You can tell an HTTP agent apart from a Foundry prompt agent, and why the
+ GPT-RAG orchestrator is the former.
+- You deployed the GPT-RAG template into a sandbox and a dev environment, and you
+ know why the PR gate deploys and evaluates candidate code in sandbox before
+ anything updates dev.
+- You made the cloned orchestrator yours by re-initializing its git
+ history and starting a fresh repository.
+- You pointed AgentOps directly at the orchestrator endpoint and mapped `ask`
+ and `text` to the real request and response shape.
+- You indexed a sample document, built a smoke dataset from its content, and
+ scored answers on coherence, similarity, and response completeness, knowing why
+ that is smoke and not groundedness.
+- You added grey-box retrieval scoring, returning the answer plus the retrieved
+ context behind an opt-in, gated flag, so the Foundry Groundedness and Retrieval
+ evaluators score the real retrieval and not a static dataset field.
+- You inspected both the per-row eval evidence and the runtime traces, and you
+ know which spans AgentOps emits (`agentops.eval.*`) versus which come from the
+ orchestrator's own runtime telemetry.
+- You added Red Team as a safety gate alongside the eval gate, so CI blocks
+ unsafe behavior, not just quality regressions.
+- You ran local evals against the deployed endpoint and generated a PR gate that
+ blocks regressions before they merge.
+
+## Related tutorials
+
+- [Hosted Agent Tutorial](tutorial-hosted-agent.md): a Foundry Hosted Agent is
+ not an HTTP agent. Foundry runs the agent for you and emits the `invoke_agent`
+ traces server-side, so there is no endpoint to operate yourself.
+- [Prompt Agent Tutorial](tutorial-prompt-agent.md): for a Foundry-managed prompt
+ agent referenced as `name:version`.
diff --git a/docs/tutorial-prompt-agent.md b/docs/tutorial-prompt-agent.md
index c2a7c41e..89d4d039 100644
--- a/docs/tutorial-prompt-agent.md
+++ b/docs/tutorial-prompt-agent.md
@@ -1,398 +1,230 @@
-# Tutorial: Foundry Prompt Agent (sandbox → dev with PR gate)
+# Prompt agent tutorial
Use this tutorial when you want a Foundry-managed prompt agent referenced as
-`name:version`. The example creates a small **Travel Agent** in Foundry and
-then uses AgentOps to add repo-side readiness, a PR gate that catches
-regressions before merge, a `dev` deploy workflow, Doctor evidence, and
-Cockpit.
-
-This path validates the Foundry-native multi-environment route:
-
-- Foundry manages the prompt agent runtime, cloud evaluation execution, traces,
- Rubric evaluator definitions, traces, Guardrails, red-team scans, and
- Operate dashboards in **each environment**.
-- AgentOps manages repo-side readiness: source-controlled prompts, CI gates,
- Doctor blocking, release evidence, threshold enforcement, ASSERT/ACS evidence
- references, and Cockpit.
-
-The toolkit benefit is the **release loop across environments**. You will
-author the prompt in a **sandbox** Foundry project where saves are
-experimentation only and never trigger CI, then let CI prove the prompt
-is safe to merge by staging it as a candidate in the team's **dev**
-Foundry project, evaluating that exact candidate, running Doctor against
-the result, and — only when both pass — promoting the deploy.
-
-Pay special attention to Doctor in this tutorial: it does not only report
-whether thresholds passed, it also catches slow regressions (for example,
-`groundedness` drifting from 5.0 to 4.0) that the threshold gate would
-otherwise miss. When the PR workflow runs Doctor with
-`--severity-fail critical`, those regression findings **block the PR**
-the same way a failed threshold would.
-
-## Repository set used in this tutorial
-
-This tutorial intentionally shows the broader Foundry ecosystem, not only
-AgentOps. The repository / skill set below keeps the CLI, workflow runner,
-toolkit reference, and skill guidance aligned in one cohesive demo
-environment.
-
-| Repository / skill | Role in the journey |
-|---|---|
-| `Azure/agentops` | Provides the AgentOps CLI, workflow generation, Doctor, Cockpit, and release evidence flow. |
-| `microsoft-foundry` skill (Copilot Chat) | External, not bundled with AgentOps. Demonstrates how a skill outside the AgentOps toolkit can guide Foundry project creation. The tutorial gives a portal-first fallback because the skill is optional. |
-| `azd ai agent eval` / `microsoft/ai-agent-evals` | Foundry-native eval paths. AgentOps can wrap azd `eval.yaml` recipes (`execution: azd`) or invoke Foundry cloud eval directly; in both cases AgentOps normalizes threshold evidence and release artifacts. |
-| `microsoft/foundry-toolkit` | Frames the VS Code create/debug experience and the Operate handoff after a prompt version is ready. |
-| `microsoft/azure-skills` | Connects Copilot guidance to Foundry observe, CI/CD, regression, and trace follow-through. |
-| `Azure-Samples/microsoft-foundry-e2e-agent-observability-workshop` | Reference for the Foundry Observe/Optimize/Protect loop: traces, App Insights, Operate Ask AI, evaluations, and red-team follow-through. |
+`name:version`. You build a small Travel Agent in Foundry, then use AgentOps to
+add a PR gate that catches regressions before merge, a dev deploy, Doctor
+evidence, and Cockpit.
+
+You will do four things:
+
+1. **Evaluate** a prompt agent while you experiment in sandbox.
+2. **Ship** the prompt through GitHub so the same reviewed file deploys to dev.
+3. **Observe** the dev run with traces, telemetry, and Doctor findings.
+4. **Operate** with release evidence, thresholds, and a Cockpit summary.
+
+```mermaid
+flowchart LR
+ E["Evaluate Author in sandbox Run evals"]
+ S["Ship Move prompt to git Open PR, deploy to dev"]
+ O["Observe Read traces Run Doctor"]
+ W["Operate Review evidence Make the ship call"]
+ E --> S --> O --> W
+```
+
+The idea is simple: sandbox is for trying things, Git is the source of truth,
+and CI evaluates the PR candidate before anything is promoted to dev. If Doctor
+finds a critical regression, the PR should not ship.
## Before you run the tutorial
-Do this once before a live walkthrough or guided session. The goal is to keep
-the demo focused on the Foundry plus AgentOps flow, not on unexpected
-permission prompts.
+Run through this once before a live walkthrough, grouped by area, so the demo
+stays on the Foundry plus AgentOps flow instead of permission prompts.
+
+**Foundry projects**
+
+- Two projects: a sandbox where you publish the prompt agent and run PR candidates, and a shared dev for the post-merge deploy. You publish only in sandbox; CI bootstraps dev after merge (and later qa and prod).
+- The same model deployment name (for example `gpt-4o-mini`) in every project. A missing deployment in dev breaks the first bootstrap.
+
+**Azure**
+
+- Azure CLI installed and `az login` working on the tenant that owns the projects.
+- Application Insights on the dev project, with Reader granted to the dev project's managed identity. This powers telemetry; sandbox is optional.
+- An Entra app registration with federated credentials, or an admin ready to provide the client, tenant, and subscription id.
+
+**GitHub**
+
+- Push access to the tutorial repo and permission to run GitHub Actions.
+- GitHub environments named `sandbox` and `dev` for Azure auth and Foundry endpoints.
+- `gh auth login` authenticated for the PR commands.
-| Check | Why it matters |
+**Coding agent**
+
+- Your coding-agent CLI (Copilot or similar) signed in before you run AgentOps skills, so it can read the repo and propose the GitHub and Azure setup.
+
+## What happens in this tutorial
+
+One prompt moves through four stages. Use this as a checklist:
+
+| Stage | What it means |
|---|---|
-| Azure CLI is installed and `az login` succeeds with the tenant that owns the Foundry projects. | AgentOps, Foundry SDK calls, and CI setup all need the same Azure identity context. |
-| You can create **two** Foundry projects in the same Azure subscription (or have two existing projects you can use). | The tutorial uses a sandbox project for authoring and experimentation plus a shared dev project for the PR gate. You only need to publish the agent in sandbox — CI auto-bootstraps it in dev (and later qa / prod). |
-| You can publish a prompt agent in the **sandbox** Foundry project. | The tutorial seeds `travel-agent:2` only in sandbox (Foundry portal typically numbers the first published version `:2`, not `:1`). Dev / qa / prod start empty; the prompt-agent deploy workflow creates the first version in those projects automatically using `prompt_agent_bootstrap` defaults plus `prompt_file`. |
-| The **same model deployment name** (for example `gpt-4o-mini`) exists in every Foundry project you plan to deploy to. | `prompt_agent_bootstrap.model` is a single value reused for every environment. If dev does not have that deployment, the first auto-bootstrap fails. |
-| You can create or attach Application Insights for at least the dev Foundry project, and can grant Reader to the dev project's managed identity on that App Insights resource and its backing Log Analytics workspace when workspace-based. | Foundry Traces, the Operate dashboard, trace-to-dataset generation, Doctor, and Cockpit need telemetry to tell the observability story. Sandbox observability is optional. |
-| You can push to the tutorial GitHub repository and run GitHub Actions. | The PR gate only runs after the repo is pushed. |
-| GitHub CLI is authenticated with `gh auth login` if you use the PR commands in this tutorial. | The regression step opens PRs and sends the reader directly to the workflow run. |
-| You can create a GitHub environment named `dev` and add Actions variables/secrets. | The generated workflow uses that environment for Azure auth and the dev Foundry project endpoint. |
-| You can create an Entra app registration with federated credentials, or an admin is ready to provide the client ID, tenant ID, and subscription ID. | The workflow skill can wire OIDC cleanly; without this, CI cannot authenticate to Azure. |
-| Copilot or your coding-agent CLI is signed in before you ask it to run AgentOps skills. | The skill handoff assumes an authenticated coding-agent session that can read the repo and propose GitHub/Azure setup steps. |
+| **Test prompt** | Try the prompt in sandbox and publish a version when it looks ready. |
+| **Move prompt** | Copy the tested instructions into a prompt file in Git, which becomes the source of truth. |
+| **Create dev environment** | Leave dev empty. CI reads the AgentOps config and creates or updates the dev agent. |
+| **Block regressions** | CI evaluates the PR candidate, applies thresholds, and runs Doctor. Serious regressions stop the PR. |
-## Mental model: sandbox, dev, and what crosses environments
+### Why the SHA matters
-Before the hands-on steps, hold this picture in your head:
+Foundry version numbers are local to each project, so sandbox `travel-agent:2`
+may not match the number in dev, qa, or prod. AgentOps compares the prompt
+content instead. It fingerprints each version two ways:
-```
-sandbox Foundry project dev Foundry project
-(authoring + experimentation; (shared environment, PR gate target,
- used by you or the team) where merge deploys land)
- │ │
- │ travel-agent:2 (your first publish │ (empty — no agent here yet;
- │ in sandbox; Foundry portal numbers │ CI auto-creates the agent
- │ it starting from :2) │ on the first deploy via
- │ travel-agent:3,4,5,... (free saves) │ prompt_agent_bootstrap; the
- │ │ number Foundry assigns there
- │ │ is environment-local)
- │ │
- └──── git is the source of truth ─────────►│
- .agentops/prompts/travel-agent.md
- prompt_sha256 + git_sha
-```
+- `prompt_sha256`: a hash of the prompt text. Same text, same hash, in any project.
+- `git_sha`: the git commit that produced that text.
-Two ideas to internalize:
-
-1. **The prompt in `git` is the source of truth.** The file
- `.agentops/prompts/travel-agent.md` is what CI reads and what reviewers
- diff. Each Foundry project's version numbers count its own saves and
- are environment-local.
-2. **You only author the agent in sandbox.** Dev, qa, and prod start
- empty. When the prompt-agent deploy workflow runs against an empty
- environment, it reads `prompt_agent_bootstrap` from `agentops.yaml`
- plus `prompt_file`, then creates the first version of the agent
- automatically in that environment. You never seed dev / qa / prod by
- hand.
-3. **Cross-environment identity is the SHA, not the number.** AgentOps
- embeds `agentops.prompt_sha256` and `agentops.git_sha` into every
- Foundry version it creates, and writes the same identifiers into the
- per-environment deploy artifact `foundry-agent.json`. When you ask
- "is the same prompt running in sandbox, dev, and prod?", you compare
- SHAs, not version numbers. The version numbers will differ.
-
-The longer walkthrough of that identity story is in step 15, when you
-have a real `foundry-agent.json` artifact to open.
-
-## Journey you will exercise
-
-| Step | Main tool | What you do | AgentOps role |
-|---|---|---|---|
-| Create two Foundry projects | Foundry portal (or `microsoft-foundry` skill) | Create `travel-agent-sandbox` (where you author) and `travel-agent-dev` (left empty — CI seeds it). | No AgentOps create/deploy role; AgentOps consumes the published baseline from sandbox and bootstraps dev. |
-| Author in sandbox | Foundry playground | Iterate on the prompt safely in sandbox Foundry. | Optional spot-check via local `agentops eval run`. |
-| Promote the prompt to git | Editor | Copy validated instructions into `.agentops/prompts/travel-agent.md`. | The CI gate reads this file. |
-| First green PR + dev deploy | GitHub Actions + Foundry dev project | Push prompt, open PR, watch CI auto-bootstrap the first version of `travel-agent` in dev from `prompt_agent_bootstrap` (the dev project is still empty at this point), evaluate it, run Doctor; merge; deploy lands in dev. | Runs the gate, bootstrap-on-first-deploy, threshold decision, Doctor blocking step, deploy artifact, and release evidence. |
-| Force a regression | Editor + GitHub Actions | Edit the prompt to a worse version, push, observe BOTH eval threshold failure AND Doctor regression CRITICAL. | Catches the regression at PR time, not after merge. |
-| Fix and redeploy | Editor + GitHub Actions | Restore prompt, push, PR green, merge, deploy. | Records the recovery. |
-| Review readiness | AgentOps Doctor + Cockpit | Check CI, eval, telemetry, evidence, and links. | Turns scattered signals into release blockers, warnings, evidence files, and next actions. |
-
-## 1. Create a clean workspace and install AgentOps
-
-Create a workspace folder and install the toolkit before any other tool
-runs. The skills and CLI commands later in the tutorial all depend on this.
+AgentOps writes both into a small deploy record, `foundry-agent.json`, one per
+environment. To check whether dev and prod run the same prompt, compare these
+fingerprints, not the Foundry version numbers. Step 15 walks through a real
+`foundry-agent.json`. More: [Operate](operate.md).
+
+## 1. Create the workspace
+
+First, create and activate a workspace folder with its own virtual environment:
```powershell
mkdir agentops-prompt-quickstart
cd agentops-prompt-quickstart
python -m venv .venv
.\.venv\Scripts\Activate.ps1
-python -m pip install -U pip
-python -m pip install agentops-accelerator
-agentops --version
```
-For normal usage, prefer the published package above. For this tutorial
-path, install the aligned reference branch so the CLI, generated
-workflows, and tutorial steps stay in sync:
+Then install AgentOps and confirm the CLI:
```powershell
-python -m pip install "agentops-accelerator @ git+https://github.com/Azure/agentops.git@develop"
+python -m pip install -U pip
+python -m pip install "agentops-accelerator[agent]"
+agentops --version
```
-## 2. Install the AgentOps Copilot skills
-
-AgentOps ships a set of Copilot skills that guide eval, dataset, workflow,
-and Doctor flows. Install them now so they are available when you hand off
-to Copilot Chat later.
+## 2. Install the skills
```powershell
-agentops skills install --platform copilot --force
+agentops skills install
```
-That command installs the AgentOps skills (`agentops-eval`,
-`agentops-workflow`, `agentops-config`, `agentops-dataset`, and so on)
-into `.github/skills/` so Copilot can pick them up when you say `/skills`
-in chat.
+This installs the AgentOps skills (`agentops-eval`, `agentops-workflow`,
+`agentops-config`, `agentops-dataset`, and others) into `.github/skills/` so
+Copilot picks them up when you type `/skills` in chat.
-The `microsoft-foundry` skill used in step 3 is **separate and external**
-to AgentOps. If it is not already available in your Copilot Chat session,
-the tutorial falls back to the Foundry portal for the project creation
-step. The intent is intentional: this is where AgentOps and other skills
-meet, not a place where AgentOps imposes a particular skill stack.
+!!! note "About the microsoft-foundry skill"
+ The `microsoft-foundry` skill used in step 3 is separate from AgentOps and is
+ not installed by it. If your Copilot session does not have it, use the Foundry
+ portal path instead. Both paths reach the same result.
-## 3. Create the two Foundry projects
+## 3. Create Foundry projects
-You need two Foundry projects in the same Azure subscription. Use these
-names so the rest of the tutorial reads naturally:
+You need two Foundry projects in the same Azure subscription:
-- `travel-agent-sandbox` — the authoring and experimentation space. Saves
- here never trigger CI. One project is fine whether you are solo or
- working with a small team; everyone with access can iterate here.
-- `travel-agent-dev` — the first shared environment. The PR gate stages
- candidates here, and the dev deploy workflow lands here.
+- `travel-agent-sandbox`: where you author, experiment, and stage PR candidates. Saves here never deploy dev.
+- `travel-agent-dev`: the first shared environment. The dev deploy lands here after merge.
-> **Team scaling.** A single sandbox project works fine for a solo
-> walkthrough and for small teams. If you grow to the point that
-> simultaneous saves collide, or different feature streams need to
-> experiment in isolation, you can split into per-stream sandboxes
-> (`travel-agent-checkout-sandbox`, `travel-agent-search-sandbox`, etc.)
-> or per-developer sandboxes. AgentOps does not care how many sandbox
-> projects exist; only the dev / qa / prod chain is what CI promotes
-> through.
+!!! concept "Why two projects, not one"
+ Sandbox and dev are isolated on purpose. You author and break things freely
+ in sandbox, and dev only ever changes through a merged, gated PR. That
+ one-way promotion is what makes dev trustworthy: nothing reaches it that did
+ not pass the gate, so a shared environment cannot be quietly edited under
+ everyone's feet.
-> **Enterprise provisioning option.** This quickstart creates only the Foundry
-> resources needed for the video path. For a fuller Azure baseline with
-> networking, identity, security, and operations patterns, see
-> [Azure AI Landing Zone](https://aka.ms/ailz).
+!!! note "How many sandboxes"
+ One sandbox is enough for a solo run or a small team. Split into per-stream or
+ per-developer sandboxes only if saves start to collide. CI always promotes
+ through the dev, qa, and prod chain. For a fuller Azure baseline with
+ networking, identity, and operations, see [Azure AI Landing Zone](https://aka.ms/ailz).
-### Path A — Foundry portal (always available)
+### Path A: Foundry portal (always available)
1. Open the [Azure AI Foundry portal](https://ai.azure.com).
-2. Create the first project. Use the same Azure subscription you will
- target with CI.
- - **Project name:** `travel-agent-sandbox`
- - **Region/resource:** any region with the model deployment you plan
- to use.
-3. Repeat for the second project named `travel-agent-dev`. Use the same
- subscription. The two projects can share a resource group or be in
- separate ones, depending on your team's policy.
-4. For each project, copy the project endpoint URL from the project
- overview page. It looks like:
-
- ```text
- https://.services.ai.azure.com/api/projects/travel-agent-sandbox
- https://.services.ai.azure.com/api/projects/travel-agent-dev
- ```
-
- Save both endpoints. You will paste them in step 7 and step 8.
-
-#### Path A follow-up — grant agent-build and data-plane access manually
-
-Creating a project through the portal only assigns you `Foundry User` **at
-the project scope**. In the Foundry UI, creating/building agents can also
-require `Foundry User` on the parent Foundry / AI Services resource. Some
-portal screens still use the previous role name, `Azure AI User`, while the
-Azure RBAC role name is now `Foundry User`. If that role is missing, the portal
-blocks step 4 with:
+2. Create `travel-agent-sandbox` in your target subscription. Pick a region that has the model deployment you plan to use.
+3. Create `travel-agent-dev` in the same subscription.
+4. Copy each project endpoint from its overview page. You paste them in steps 7 and 8.
```text
-You don't have permission to build agents in this project.
-To get access, please ask your administrator to assign you the Azure AI User role.
+https://.services.ai.azure.com/api/projects/travel-agent-sandbox
+https://.services.ai.azure.com/api/projects/travel-agent-dev
```
-You also need `Cognitive Services OpenAI User` for the OpenAI data-plane actions
-that live on the parent AI Services *account* — the chat-completions call that
-backs every AI-assisted evaluator and every cloud-eval grader. Even `Owner` on
-the subscription is not enough: the built-in `Owner` role definition has
-`actions: ["*"]` but `dataActions: []`, so it grants full control plane and zero
-data plane on Cognitive Services accounts.
-
-Skipping the OpenAI role is what causes the eval grader to fail later with::
+Then grant two data-plane roles on the parent AI Services account, once per
+account you build in or evaluate against. Both are required:
- PermissionDenied: The principal `` lacks the required
- data action `Microsoft.CognitiveServices/accounts/OpenAI/deployments/
- chat/completions/action` to perform `POST /openai/deployments/...`
+- `Foundry User` (some portal screens still call it `Azure AI User`): lets you build agents in the Foundry UI.
+- `Cognitive Services OpenAI User`: lets the eval graders call chat completions. `Owner` is not enough, because it grants no data-plane actions.
-Run these assignments once per AI Services account that hosts a Foundry project you
-will build in or evaluate against. Cloud evaluations run server-side: the agent
-call and graders may authenticate as Foundry/Azure AI managed identities, not
-only as your signed-in user. Assigning the OpenAI role only to your user can
-still leave some graders failing with `AuthenticationError`. Replace
-`` with the resource group you chose above, for example
-`rg-agentops-travel-`, and `` with the parent Foundry /
-AI Services account name.
+For how to assign roles, see [Assign Azure roles](https://learn.microsoft.com/azure/role-based-access-control/role-assignments-portal)
+and [Foundry RBAC](https://learn.microsoft.com/azure/ai-foundry/concepts/rbac-azure-ai-foundry).
+The commands below assign both to your user plus the Foundry managed identities
+used by server-side evals:
```powershell
-$subscriptionId = az account show --query id -o tsv
$resourceGroup = ""
$accountName = ""
-$accountScope = az cognitiveservices account show `
- --resource-group $resourceGroup `
- --name $accountName `
- --query id -o tsv
+$accountScope = az cognitiveservices account show --resource-group $resourceGroup --name $accountName --query id -o tsv
$userObjectId = az ad signed-in-user show --query id -o tsv
-# User building agents in Foundry and running local commands / cloud evals.
-az role assignment create `
- --assignee $userObjectId `
- --role "53ca6127-db72-4b80-b1b0-d745d6d5456d" `
- --scope $accountScope
-
-az role assignment create `
- --assignee $userObjectId `
- --role "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" `
- --scope $accountScope
-
-# Foundry/Azure AI managed identities used by server-side agent/evaluator calls.
-az resource list -g $resourceGroup `
- --query "[?identity.principalId!=null].identity.principalId" -o tsv |
- ForEach-Object {
- az role assignment create `
- --assignee-object-id $_ `
- --assignee-principal-type ServicePrincipal `
- --role "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" `
- --scope $accountScope
- }
-```
-
-Repeat the command with the `travel-agent-dev` resource group if the dev
-project lives in a different RG.
+# Foundry User + Cognitive Services OpenAI User for your user.
+az role assignment create --assignee $userObjectId --role "53ca6127-db72-4b80-b1b0-d745d6d5456d" --scope $accountScope
+az role assignment create --assignee $userObjectId --role "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" --scope $accountScope
-> **Give the assignment a few minutes to propagate.** Data-plane role
-> assignments on the AI Services account do **not** take effect
-> instantly — propagation to the Foundry evaluator workers can take
-> several minutes (occasionally up to ~15). The cloud eval runs each
-> grader as an independent worker that authenticates separately, so the
-> **first run right after granting the role may show intermittent
-> `AuthenticationError` on a subset of graders and report
-> `Threshold status: FAILED` even when every threshold is green** (no
-> single row had all graders succeed). This is a grader execution
-> failure, not a quality regression. Wait a few minutes and re-run
-> `agentops eval run` — once propagation finishes, every grader scores
-> and the gate passes.
+# Cognitive Services OpenAI User for the Foundry managed identities.
+az resource list -g $resourceGroup --query "[?identity.principalId!=null].identity.principalId" -o tsv | ForEach-Object {
+ az role assignment create --assignee-object-id $_ --assignee-principal-type ServicePrincipal --role "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" --scope $accountScope
+}
+```
-AgentOps Doctor will detect the missing assignment in a future release,
-but until then this is a manual one-time setup step per new environment.
+!!! warning "Wait for RBAC to propagate"
+ Data-plane assignments on the AI Services account can take several minutes
+ (sometimes up to 15) to reach the evaluator workers. The first
+ `agentops eval run` right after granting can show `AuthenticationError` on a
+ few graders and report `Threshold status: FAILED` even when scores are green.
+ This is a grader execution failure, not a quality regression. Wait a few
+ minutes and re-run.
-### Path B — `microsoft-foundry` skill (if available)
+### Path B: microsoft-foundry skill (if available)
-If your Copilot session already has the external `microsoft-foundry`
-skill, you can drive the same outcome from chat. In Copilot, run:
+If your Copilot session has the external `microsoft-foundry` skill, drive the
+same setup from chat. Run `/skills` to confirm it is listed, then paste the
+prompt below as-is (only change the project names if you want your own suffix):
```text
-/skills
+Create two Azure AI Foundry projects in one subscription for an AgentOps tutorial.
+Names: travel-agent-sandbox and travel-agent-dev. The sandbox is the authoring
+project where I publish the agent prompt; leave dev empty because CI bootstraps it
+on the first deploy. Use the same gpt-4o-mini deployment in both, attach
+Application Insights to the dev project, and grant me Foundry User plus Cognitive
+Services OpenAI User on the AI Services account. Show the plan and the endpoints
+before applying.
```
-If you see `microsoft-foundry` listed, paste the following and let the
-skill propose the changes before applying them:
+!!! note "Pick unique names"
+ Replace any placeholder suffix with something unique to you (initials, handle,
+ or a date) so resource group and project names do not collide when several
+ people run the tutorial in one subscription. The `gpt-4o-mini` deployment name
+ must be identical in both projects. Before continuing, confirm the skill's
+ plan lists `Foundry User` and `Cognitive Services OpenAI User`; if it only
+ created projects, ask it to add those roles.
-```text
-I want to set up two Azure AI Foundry projects in the same subscription
-for an AgentOps tutorial:
-
-Use these Azure container/resource names unless I say otherwise:
-- Resource group: rg-agentops-travel-
-- Azure AI Foundry resource / AI Services account: foundry-agentops-travel-
-- Region: East US 2
-- Model deployment name in both projects: gpt-4o-mini
-
-1. travel-agent-sandbox - the authoring and experimentation space
- (used by me, or shared with my team for iteration). I will publish
- the seed prompt agent here manually in the next step (Foundry will
- typically assign it version :2, since the unpublished draft counts
- as :1).
-2. travel-agent-dev - shared dev environment used by CI as the PR gate
- target and the dev deploy target. Leave this project EMPTY. CI will
- auto-create the first agent version here on the first deploy using
- AgentOps' prompt_agent_bootstrap defaults.
-
-For each project, please:
-- Create the project under the resource group and Foundry resource named above.
-- Make sure the SAME chat-capable model deployment name is available in
- both projects (gpt-4o-mini works). Same name is important: AgentOps
- uses a single bootstrap model value for every environment.
-- Attach or create an Application Insights resource for telemetry,
- starting with the dev project.
-- Grant or verify **Reader** on that Application Insights resource to the
- **managed identity of the `travel-agent-dev` Foundry project**. Foundry's
- trace-to-dataset flow runs as the project identity when it reads traces; the
- Operate dashboard may still render for my signed-in user even when this
- project identity permission is missing. If Application Insights is
- workspace-based, also grant Reader on the backing Log Analytics workspace.
-- Grant or verify `Foundry User` access for my signed-in user on the parent
- Foundry / AI Services account so I can build agents in the
- Foundry UI. Some portal screens still call this role `Azure AI User`.
-- Grant or verify `Cognitive Services OpenAI User` data-plane access for my
- signed-in user and for the Foundry/Azure AI managed identities that will call
- the model deployment during server-side evaluations.
-
-Show me the planned changes and the resulting endpoints before applying.
-```
+## 4. Seed the agent
+
+Author the agent in one place only: the sandbox project. Dev and later qa and
+prod start empty and get bootstrapped by CI on the first deploy.
-Replace `` with a short unique suffix such as your initials,
-GitHub handle, or a date (`pl`, `contoso-dev1`, `video-0604`). This matters
-when multiple people run the tutorial in the same subscription: resource group
-names must be unique within that subscription, Foundry / AI Services resource
-names should be unique enough to avoid Azure naming conflicts, and project names
-must be unique inside the Foundry resource. The model deployment name
-`gpt-4o-mini` does **not** need to be globally unique, but it must be the same
-in both tutorial projects. For a recorded tutorial, one shared resource group is
-easiest because RBAC and cleanup happen in one place; production teams may split
-resource groups by environment.
-
-Before continuing, check that the skill's plan/output explicitly lists
-`Foundry User` (or the previous portal label, `Azure AI User`) for your signed-in
-user and `Cognitive Services OpenAI User` for your signed-in user plus the
-Foundry/Azure AI managed identities. If it only created the projects and model
-deployments, ask the skill to add or verify those role assignments before you
-move to step 4.
-
-## 4. Seed `travel-agent` in the sandbox project
-
-You only author the agent in **one place**: your sandbox Foundry
-project. Dev (and later qa / prod) start empty. The first time the
-prompt-agent deploy workflow runs against an empty environment, it reads
-`prompt_agent_bootstrap` from `agentops.yaml` plus `prompt_file` and
-creates the first version automatically. You do **not** repeat this
-manual step for every environment.
-
-In the **sandbox** project only:
-
-1. Open the [Azure AI Foundry portal](https://ai.azure.com) and select
- the `travel-agent-sandbox` project.
-2. Go to the agents area and create a new prompt-based agent.
-3. Use these values:
+!!! concept "What a Foundry prompt agent is"
+ A prompt agent is hosted configuration, not your code. It is identified by
+ `name:version` and bundles its instructions, its model, and any tools it can
+ call, all living in Foundry. AgentOps versions the instructions as a file in
+ your repo, so the prompt becomes reviewable code while the runtime stays in
+ Foundry. That split is why you can gate a prompt change like any other diff.
+
+In the `travel-agent-sandbox` project:
+
+1. Open the [Foundry portal](https://ai.azure.com) and select `travel-agent-sandbox`.
+2. Create a new prompt-based agent with these values:
| Field | Value |
|---|---|
| Name | `travel-agent` |
- | Model deployment | `gpt-4o-mini` or another chat-capable deployment available in this project |
+ | Model deployment | `gpt-4o-mini` (or another chat-capable deployment in this project) |
| Description | Helps plan short trips and explains tradeoffs. |
-4. Paste these baseline instructions:
+3. Paste these baseline instructions:
```text
You are Travel Agent, a concise travel planning assistant.
@@ -408,92 +240,74 @@ In the **sandbox** project only:
prices, or availability.
```
-5. Save and publish the agent. Foundry typically assigns version `2`
- on first publish (`travel-agent:2`) because the unpublished draft
- counts as `:1`. **Note the exact version Foundry assigned** — you
- will paste this number into `agentops.yaml` in section 9. The dev
- project still has no agent at this point — that is expected.
-
-> **Why not seed dev too?** Forcing the operator to recreate the same
-> prompt agent in every environment is exactly the manual drift problem
-> AgentOps is here to eliminate. Section 9 adds a `prompt_agent_bootstrap`
-> block to `agentops.yaml`; the first PR / deploy run against dev reads
-> those defaults plus `prompt_file` and creates the first version of
-> the agent in dev (the version number Foundry assigns there is
-> environment-local, typically `:1` for an SDK-created first version)
-> with the same metadata trail (`agentops.prompt_sha256`,
-> `agentops.git_sha`). Subsequent runs follow the normal reuse /
-> next-version flow.
-
-> **Prompt-as-code captures only the instructions.** Later in the
-> tutorial you will commit `.agentops/prompts/travel-agent.prompt.md` to git
-> and let CI use it as the prompt source. That file does not capture
-> the model deployment, parameters (temperature, top-p), tools, or
-> other agent settings — those come from `prompt_agent_bootstrap` on
-> the first deploy and stay on the Foundry agent definition afterwards.
-> Use the same model deployment name in every Foundry project so the
-> single `prompt_agent_bootstrap.model` value works everywhere without
-> per-environment tweaks. AgentOps will not detect drift in non-prompt
-> fields between environments.
-
-## 5. Try the agent in the sandbox playground
-
-Open `travel-agent-sandbox` in the Foundry portal, open `travel-agent:2`
-(the version Foundry assigned on first publish), and run a sample in the
-playground:
+4. Save and publish. Foundry usually assigns version `2` (`travel-agent:2`) because the unpublished draft counts as `:1`. Note the exact version; you reference it in step 7.
+
+!!! info "Why dev starts empty"
+ Recreating the same agent in every environment is the manual drift problem
+ AgentOps removes. Step 9 adds a `prompt_agent_bootstrap` block, and the first
+ dev deploy reads it plus the prompt file to create the first dev version,
+ carrying the same `prompt_sha256` and `git_sha` metadata.
+
+!!! note "Prompt-as-code captures instructions only"
+ The committed prompt file holds the instructions, not the model deployment,
+ parameters, or tools. Those come from `prompt_agent_bootstrap` on the first
+ deploy. Use the same model deployment name everywhere so one bootstrap value
+ works for every environment.
+
+## 5. Try the agent
+
+Open `travel-agent-sandbox`, open `travel-agent:2` (the version Foundry
+assigned), and run a sample in the playground:
```text
Plan a 3-day first-time trip to Lisbon for a couple who likes food and history.
```
-This is the sandbox role: you confirm the prompt actually does what you
-want before promoting it to git. Sandbox saves stay local to this project
-and do not affect CI.
+This is the sandbox role: confirm the prompt does what you want before promoting
+it to git. Sandbox saves stay local and never trigger CI.
-A short observability cross-reference: in the same project's
-**Traces** tab you can find this run. If Foundry asks to attach
-Application Insights and you have not connected it yet, you can do that
-now or wait until the closeout step. The detailed observability tour is
-in step 18; for now, just confirm there is at least one trace to look at
-later.
+!!! note "Observability comes later"
+ The same project's Traces tab will show this run. If Foundry asks to attach
+ Application Insights and you have not connected it, do it now or wait for
+ step 18. For now, just confirm there is at least one trace to inspect later.
+ Full tour: [Observe](observe.md).
-## 6. Create the travel eval dataset
+## 6. Create the dataset
-Create the small JSONL dataset that matches the Travel Agent behavior:
+Create a small JSONL dataset that matches the Travel Agent behavior. The
+`expected` values are acceptance criteria, not exact answer strings. For prompt
+agents AgentOps uses judge-based quality and completeness on this shape.
-> **Copilot assist:** If you want help expanding or reviewing these rows, ask
-> Copilot to use `/skills agentops-dataset`. The skill can propose additional
-> edge cases, check that each row has `input` and `expected`, and keep the
-> criteria written as reviewable behavior instead of exact answer strings.
+!!! concept "The dataset is your definition of good"
+ These few rows are the contract your agent has to keep passing. Because an
+ LLM judge reads `expected` as acceptance criteria, you are encoding the
+ behavior you care about, not memorizing one correct sentence. Start small and
+ honest: a handful of rows that capture real requirements is worth more than a
+ large set that nobody trusts.
-```powershell
-New-Item -ItemType Directory -Force .agentops\data | Out-Null
-@'
+```text
+edit .agentops/data/travel-smoke.jsonl
+```
+
+```json
{"input":"Plan a 3-day first-time trip to Lisbon for a couple who likes food and history.","expected":"A concise 3-day Lisbon itinerary with food, history, neighborhoods such as Baixa, Alfama, and Belem, practical notes, and no claim to make live bookings."}
{"input":"Suggest a low-budget weekend in Seattle for a solo traveler who likes coffee and museums.","expected":"A practical weekend Seattle plan with low-budget choices, coffee and museum suggestions, transit or weather notes, and no claim to make live bookings."}
{"input":"I want to visit Tokyo for 5 days with two kids. What should we do?","expected":"A family-friendly 5-day Tokyo itinerary with kid-appropriate activities, transit and pacing notes, and no claim to make live bookings."}
-'@ | Set-Content -Encoding utf8 .agentops\data\travel-smoke.jsonl
```
-The `expected` values here are acceptance criteria, not exact answer
-strings. For prompt agents, AgentOps uses judge-based quality and
-completeness metrics on this shape; token-overlap F1 is better reserved
-for exact-reference model tests.
+!!! note "Copilot assist"
+ Ask Copilot to use `/skills agentops-dataset` to expand or review these rows.
+ It can add edge cases and check that each row has `input` and `expected`,
+ written as reviewable behavior instead of exact answer strings.
-## 7. Initialize AgentOps against the sandbox project
+## 7. Initialize AgentOps
-Sign in to Azure with the same identity that has access to both Foundry
-projects:
+Sign in with the identity that can access both projects, then run the wizard
+against sandbox. AgentOps creates an azd-compatible environment so the same
+workspace can hold multiple environments later.
```powershell
az login
-```
-
-Then run the wizard against the sandbox environment. AgentOps creates an
-azd-compatible environment directory so the same workspace cleanly
-supports multiple environments later.
-
-```powershell
agentops init --azd-env sandbox
```
@@ -501,53 +315,12 @@ Answer the prompts:
| Prompt | Answer |
|---|---|
-| Foundry project endpoint | The **sandbox** project endpoint from step 3 |
-| Agent | `travel-agent:2` (use the exact version Foundry assigned in section 4) |
+| Foundry project endpoint | The sandbox endpoint from step 3 |
+| Agent | `travel-agent:2` (the exact version from step 4) |
| Dataset path | `.agentops/data/travel-smoke.jsonl` |
-If the wizard offers starter defaults such as `Agent [my-agent:1]` or
-`Dataset path [.agentops/data/smoke.jsonl]`, replace them with the
-Travel Agent values above.
-
-Before continuing, verify the saved dataset path. This must point to the
-Travel Agent dataset you created in step 6, not the starter
-`.agentops/data/smoke.jsonl` file:
-
-```powershell
-Select-String -Path agentops.yaml -Pattern '^dataset:'
-```
-
-Expected output:
-
-```text
-dataset: .agentops/data/travel-smoke.jsonl
-```
-
-If it still says `.agentops/data/smoke.jsonl`, fix it now:
-
-```powershell
-(Get-Content agentops.yaml) `
- -replace '^dataset:.*$', 'dataset: .agentops/data/travel-smoke.jsonl' |
- Set-Content -Encoding utf8 agentops.yaml
-```
-
-The interactive path is intentional: you see what each value means, and
-each answer is saved as soon as it validates. Because you passed
-`--azd-env sandbox`, the wizard writes the local Azure values to
-`.azure/sandbox/.env` and sets `defaultEnvironment: sandbox` in
-`.azure/config.json`.
-
-After the command finishes, your workspace looks like this:
-
-```text
-agentops.yaml
-.agentops/
-.agentops/data/travel-smoke.jsonl
-.azure/
-.azure/config.json
-.azure/.gitignore
-.azure/sandbox/.env
-```
+Replace any starter defaults (like `my-agent:1` or `smoke.jsonl`) with the
+Travel Agent values.
`agentops.yaml` should stay small:
@@ -557,335 +330,203 @@ agent: travel-agent:2
dataset: .agentops/data/travel-smoke.jsonl
```
-> **Why `version: 1`?** This is the AgentOps configuration schema version, not
-> the Foundry agent version. Keep it as `1`; the agent version is the suffix in
-> `agent: travel-agent:2`.
->
-> **App Insights — should already be wired from step 3.** Step 3
-> (both Path A and Path B) instructs you to attach an Application
-> Insights resource to the **dev** Foundry project when you create it,
-> so by default this is already done and no manual env variable is
-> needed. AgentOps auto-discovers the connection string through the
-> Azure AI Projects SDK at runtime.
->
-> Verify in 10 seconds: open → **`travel-agent-dev`**
-> project → left rail **Tracing** (sometimes under "Observability" /
-> "Monitoring"). If you see a linked Application Insights resource with
-> a "Copy connection string" button, you are done — skip the optional
-> subsection in section 8.
->
-> Only set `APPLICATIONINSIGHTS_CONNECTION_STRING` manually if the
-> Tracing tab shows "Connect Application Insights" (the resource was
-> not created in step 3), if your identity cannot read the linked
-> resource at runtime, or if you intentionally want telemetry to go to
-> a different resource. Section 8 covers all three cases.
-
-## 8. Add the dev azd environment by hand
-
-The dev project endpoint goes into a second azd environment, but **do
-not** re-run `agentops init --azd-env dev` — that would flip
-`defaultEnvironment` in `.azure/config.json` to `dev` and change which
-project local commands hit by default. Add the dev env manually instead:
-
-```powershell
-New-Item -ItemType Directory -Force .azure\dev | Out-Null
-@'
-AZURE_AI_FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/travel-agent-dev
-'@ | Set-Content -Encoding utf8 .azure\dev\.env
-```
-
-Replace the endpoint with your real dev project endpoint from step 3.
-
-### Optional: also set the dev project's App Insights connection string
-
-In most walkthroughs you can **skip this subsection**. Step 3 already
-attached an Application Insights resource to the **`travel-agent-dev`**
-Foundry project (either you did it manually in Path A or the
-`microsoft-foundry` skill did it in Path B, following the explicit
-"Attach or create an Application Insights resource for telemetry,
-starting with the dev project" instruction in the step 3 prompt), and
-AgentOps auto-discovers that connection string at runtime through the
-Azure AI Projects SDK. No env variable required.
-
-**Quick verification (10 seconds):**
-
-Open → left rail **Admin** → select the
-**`travel-agent-dev`** project → **Connected resources**. Make sure you
-are checking the **dev** project, not the sandbox project you used to
-build the prompt agent. One of two things will be true:
-
-| What you see | What it means | What to do |
-|---|---|---|
-| An `appinsights` row with category `AppInsights` | The resource exists and is connected to the dev project. Auto-discovery will pick it up. | Continue with the trace-to-dataset access check below. |
-| No App Insights row in **Connected resources** | The resource was not connected in step 3. | Click **Add connection**, connect or create an Application Insights resource for the dev project, or paste a connection string manually. |
-
-**If Connected resources does not show App Insights**, the fastest fix is
-to connect one through the Foundry portal itself: click **Add connection**
-and either pick an existing Application Insights resource or create one
-in the same resource group as the dev project. Once an `appinsights` row
-appears under **Connected resources**, you can again skip the manual env
-variable — auto-discovery will pick it up.
-
-**Also verify trace-to-dataset access now.** For the step 18
-trace-sampling flow, the **managed identity of the `travel-agent-dev`
-Foundry project** needs **Reader** on the connected Application Insights
-resource. If the App Insights component is workspace-based, grant the same
-Reader role on the backing Log Analytics workspace too. This is separate from
-your signed-in user's portal access and separate from GitHub OIDC. If you
-connected App Insights manually, open the Application Insights resource in
-Azure Portal → **Access control (IAM)** and add:
-
-| Field | Value |
-|---|---|
-| **Role** | Reader |
-| **Assign access to** | Managed identity |
-| **Managed identity** | `travel-agent-dev` Foundry project |
-
-Then open the Application Insights resource → **Properties** and check
-**Workspace Resource ID**. If it points to a Log Analytics workspace, open that
-workspace and repeat the same **Reader** assignment for the `travel-agent-dev`
-managed identity.
-
-Wait a few minutes for RBAC propagation before creating a dataset from traces.
-
-**Only if you specifically want to override which resource telemetry
-goes to** (advanced case, e.g. you have a dedicated observability
-resource group), grab the connection string and paste it into
-`.azure\dev\.env`. Pick whichever path is easiest:
-
-**Path A — Azure AI Foundry portal (recommended, no Azure Portal
-hopping):**
-
-1. On the **Tracing** tab of `travel-agent-dev`, click the "Copy
- connection string" button next to the linked Application Insights
- resource.
+!!! info "version: 1 and App Insights"
+ `version: 1` is the AgentOps config schema version, not the Foundry agent
+ version. Keep it at `1`. Because step 3 attached Application Insights to the
+ dev project, AgentOps auto-discovers the connection string at runtime through
+ the Azure AI Projects SDK, so no env variable is needed yet. Step 8 covers the
+ manual override.
-**Path B — Azure Portal:**
+Because you passed `--azd-env sandbox`, the wizard writes sandbox values to
+`.azure/sandbox/.env` and sets `defaultEnvironment: sandbox`. Local commands like
+`agentops eval run` use sandbox by default.
-1. Open and search for the Application
- Insights resource attached to your dev Foundry project (it is
- typically created alongside the project and shares its name prefix).
-2. On the **Overview** blade, the right-hand "Essentials" panel shows a
- **Connection String** field. Click the copy icon next to it.
+## 8. Add a dev environment
-**Path C — Azure CLI (one command):**
+Add the dev endpoint as a second azd environment. Do not re-run
+`agentops init --azd-env dev`, because that flips `defaultEnvironment` to dev.
+Create the env file by hand:
-```powershell
-az monitor app-insights component show `
- --app `
- --resource-group `
- --query connectionString -o tsv
+```text
+edit .azure/dev/.env
```
-Once you have the value, append it to `.azure\dev\.env`:
-
```text
-APPLICATIONINSIGHTS_CONNECTION_STRING=
+AZURE_AI_FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/travel-agent-dev
```
-The full string starts with `InstrumentationKey=...` and includes
-`IngestionEndpoint=...`; paste the whole thing on one line.
+Use your real dev endpoint from step 3.
+
+!!! note "No new data-plane roles needed here"
+ You do not re-run the `Foundry User` / `Cognitive Services OpenAI User`
+ grant from step 3. That assignment is scoped to the AI Services **account**,
+ and in this topology sandbox and dev are two projects under the **same**
+ account, so dev is already covered. Re-run the grant only if you put dev on a
+ separate AI Services account. The one dev-specific grant is the Reader role
+ for traces, below.
+
+!!! note "App Insights is optional here"
+ Step 3 already attached Application Insights to the dev project, and AgentOps
+ auto-discovers its connection string, so you can skip manual setup. Only set
+ `APPLICATIONINSIGHTS_CONNECTION_STRING` in `.azure/dev/.env` if you want
+ telemetry to go to a different resource. You can read a connection string
+ with:
+ ```powershell
+ az monitor app-insights component show --app --resource-group --query connectionString -o tsv
+ ```
-Confirm the final topology:
+!!! info "Trace-to-dataset access (needed in step 18)"
+ For step 18, grant the `travel-agent-dev` project's managed identity the
+ Reader role on the connected Application Insights resource, and on its Log
+ Analytics workspace if it is workspace-based. Then wait a few minutes for RBAC
+ to propagate. See [Assign Azure roles](https://learn.microsoft.com/azure/role-based-access-control/role-assignments-portal).
+
+Final topology:
```text
.azure/
-├── config.json # defaultEnvironment: sandbox
-├── .gitignore # excludes /.env
-├── sandbox/
-│ └── .env # sandbox project endpoint
-└── dev/
- └── .env # dev project endpoint
+├── config.json # defaultEnvironment: sandbox
+├── sandbox/.env # sandbox project endpoint
+└── dev/.env # dev project endpoint
```
-`defaultEnvironment: sandbox` means local commands like
-`agentops eval run` use the sandbox project. CI workflows in step 13
-read from `.azure/dev/.env` explicitly so they always target dev.
+`defaultEnvironment: sandbox` means local commands use sandbox. CI workflows read
+`.azure/dev/.env` explicitly so they always target dev.
+
+## 9. Source-control the prompt
-## 9. Promote the prompt to a source-controlled file
+Turn the prompt into code. `agentops prompt pull` reads the published sandbox
+agent and writes its instructions to `.agentops/prompts/travel-agent.md`. It
+prints the resolved endpoint and agent version before writing, and needs
+`--force` to overwrite local edits.
-This step turns the prompt into code. From here on, the prompt that CI
-evaluates and deploys comes from this file in git, not from manual edits
-in the Foundry portal.
+!!! concept "Why the prompt belongs in git"
+ Once the instructions live in a file, the prompt gets everything code gets:
+ diffs, review, history, and a commit SHA that pins exactly which wording
+ shipped. A prompt change becomes a reviewable pull request instead of an
+ invisible edit in a portal. That is the foundation for every gate that
+ follows, because you cannot gate what you cannot see change.
```powershell
agentops prompt pull
```
-AgentOps reads `agent: travel-agent:2`, resolves the current Foundry
-endpoint, validates that the Foundry definition is a prompt agent, and
-writes the instructions to `.agentops/prompts/travel-agent.prompt.md`.
-Before writing, it prints the resolved agent, endpoint, endpoint source,
-and destination file so you can catch the wrong environment before the
-prompt is saved.
-
-By default, the command refuses to overwrite a changed prompt file. Use
-`--force` only when you intentionally want to replace reviewed local
-prompt edits with the current Sandbox version. Use `--out ` if you
-need a different file name, but keep prompt source under
-`.agentops/prompts/` unless your repository has a stronger convention.
-
-Then add `prompt_agent_bootstrap` so CI can auto-create the agent in dev
-(and later qa / prod) on the first deploy. `agentops prompt pull` writes
-`prompt_file` for you when it is missing:
+From here on, the prompt that CI evaluates and deploys comes from this file in
+git, not from manual portal edits. Now point `agentops.yaml` at the file and add
+`prompt_agent_bootstrap` so CI can auto-create the agent in empty environments:
```yaml
version: 1
agent: travel-agent:2
dataset: .agentops/data/travel-smoke.jsonl
-prompt_file: .agentops/prompts/travel-agent.prompt.md
+prompt_file: .agentops/prompts/travel-agent.md
prompt_agent_bootstrap:
model: gpt-4o-mini
description: "Helps plan short trips and explains tradeoffs."
```
-The `agent: travel-agent:2` value is now a **seed pointer**. CI uses it
-to look up the existing agent in the current environment's Foundry
-project:
-
-- If the agent exists at that exact version (the sandbox case, and
- every environment after it has caught up), CI copies the looked-up
- definition (model deployment, name, kind), replaces the instructions
- with the contents of `prompt_file`, and either re-uses the same
- Foundry version (when the prompt is byte-identical) or lets Foundry
- auto-create the next number in that project (when it differs).
-- If the agent does **not** exist at that version (the empty dev / qa /
- prod case on the first deploy, or when the env's version numbering
- has not yet caught up to the seed), CI reads `prompt_agent_bootstrap`
- for the model deployment (and optional `description`,
- `model_parameters`, `tools`) and creates a new version of the agent
- from those defaults plus `prompt_file`. The deploy artifact for that
- run records `action: "bootstrapped"`. Because the SDK auto-increments
- version numbers per project, the bootstrap may fire on the first one
- or two deploys per environment before the env catches up to the seed;
- that is expected. Subsequent deploys follow the reuse-or-create flow
- above and ignore the bootstrap block.
-
-> **Versioning, in one paragraph.** You are not pinning Foundry's
-> version number — you are pinning the prompt. The number that gets
-> created in each Foundry project depends on how many saves that
-> project has accumulated; sandbox, dev, qa, and prod will diverge.
-> What stays identical across environments — and what you cite when
-> traceability matters — is the prompt SHA-256 + the git SHA, both
-> embedded into the Foundry version metadata and into
-> `foundry-agent.json`. You only update `agent:` in `agentops.yaml`
-> when you want to repoint at a different stable seed version in
-> Foundry — not on every prompt change.
-
-> **Keep `project_endpoint` out of `agentops.yaml` for multi-env work.**
-> When `project_endpoint` is set in `agentops.yaml`, it wins over the
-> `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` environment variable that azd
-> environments rely on. That makes every command target the same
-> Foundry project regardless of which env is active, which defeats the
-> sandbox / dev / qa / prod split. The wizard does the right thing by
-> default (it writes the endpoint to `.azure//.env`, not to
-> `agentops.yaml`). If you ever copied the endpoint into `agentops.yaml`
-> manually, delete it now.
-
-## 10. Initialize the azd eval recipe and run the smoke gate
-
-Confirm the eval runner the workflow generator will use:
+For prompt agents, `prompt_agent_bootstrap.model` is also the evaluator model
+deployment AgentOps passes to `azd ai agent eval generate` as `--eval-model`.
+Use a real chat deployment name that exists in the sandbox and dev projects.
+Unlike the HTTP tutorial, you do not set a separate
+`AZURE_OPENAI_DEPLOYMENT` here because the Foundry/azd eval recipe owns the
+server-side evaluator setup.
+
+!!! info "agent: is a seed pointer, not a version pin"
+ CI looks up `travel-agent:2` in the current environment. If it exists, CI
+ copies that definition, swaps in `prompt_file`, and either reuses the same
+ version (identical prompt) or lets Foundry create the next number. If it does
+ not exist (empty dev, qa, or prod), CI reads `prompt_agent_bootstrap` plus
+ `prompt_file` and creates the first version, recording `action: "bootstrapped"`.
+ You change `agent:` only when you want a different stable seed. You are pinning
+ the prompt, not the Foundry number. Detail: [Ship](ship.md).
+
+!!! warning "Keep project_endpoint out of agentops.yaml"
+ If `project_endpoint` is set in `agentops.yaml`, it overrides the
+ per-environment `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` and makes every command hit
+ the same project, defeating the sandbox and dev split. The wizard writes the
+ endpoint to `.azure//.env`. If you copied it into `agentops.yaml`, delete
+ it now.
+
+## 10. Run the smoke gate
+
+Confirm the eval runner the generator will use:
+
+!!! concept "What the smoke gate proves"
+ A smoke gate is the smallest honest test: a few rows, run end to end against
+ the real agent, scored by a judge. It will not catch every regression, and it
+ is not meant to. Its job is to fail fast and loud when something is obviously
+ broken, so you trust green to mean "safe to keep going." You harden it into a
+ real gate in the next step.
```powershell
agentops workflow analyze --format text
```
-For `agent: name:version` plus `prompt_file`, AgentOps detects the
-prompt-agent deploy mode. The recommendation may still show AgentOps
-cloud eval in Foundry before you initialize the azd recipe:
-
-```text
-Recommendation
- deploy prompt-agent
- evaluate AgentOps cloud eval in Foundry
- workflow edits not needed - generated workflow should work as-is
- Copilot skills installed - available for workflow adaptation handoff
-```
-
-That confirms the deployment side is wired correctly. Now let AgentOps
-prepare the native azd eval recipe:
+For `agent: name:version` plus `prompt_file`, AgentOps detects the prompt-agent
+deploy mode and recommends AgentOps cloud eval in Foundry. Now prepare the azd
+eval recipe:
```powershell
agentops eval init
```
-This creates `azure.yaml` and `src/travel-agent/agent.yaml` if they are
-missing, enriches the active `.azure/sandbox/.env` with the Foundry
-metadata azd expects, writes an azd-friendly dataset copy with the
-`query` field derived from your AgentOps `input` values, asks azd to
-generate the eval recipe, and records it in `agentops.yaml`:
+This creates `azure.yaml` and `src/travel-agent/agent.yaml` if missing, enriches
+`.azure/sandbox/.env` with Foundry metadata, writes an azd-friendly dataset copy,
+generates the eval recipe, and records it in `agentops.yaml`:
```yaml
execution: azd
eval_recipe: src/travel-agent/eval.yaml
```
-Use `--force` only when you intentionally want to regenerate an existing
-`eval.yaml`. For the normal flow, run it without `--force`.
-
-Run the gate locally:
+Use `--force` only to regenerate an existing `eval.yaml`. Run the gate locally:
```powershell
agentops eval run
```
-You should see `execution: azd` and `Threshold status: PASSED`. The raw
-azd run details are kept under `.agentops/results/latest/` alongside
-AgentOps' normalized `results.json` and `report.md`.
+You should see `execution: azd` and `Threshold status: PASSED`. Raw azd details
+land under `.agentops/results/latest/` next to the normalized `results.json` and
+`report.md`.
### See the run in the Foundry portal
-`agentops eval run` only prints aggregate pass/fail to the terminal. The
-Foundry portal shows the full per-row, per-evaluator breakdown — useful
-for learning what the judge actually scored and why. Use this anchor
-section any time the tutorial tells you to run an eval.
-
-1. **Open the deep link** — easiest path. Look in
- `.agentops/results/latest/azd_evaluation.json` for the `report_url`
- field. That URL goes straight to the evaluation run in the New
- Foundry experience.
-2. **Or navigate manually** in :
- 1. Pick the `travel-agent-sandbox` project (top selector).
- 2. **Agents** → select **`travel-agent`**.
- 3. Open the **Evaluations** tab.
- 4. Click the most recent run (named after the evaluator, e.g.
- `smoke-core`).
-3. **What to look at on the run page:**
- - **Overall metric results** — the aggregate pass rate per evaluator
- (matches the values AgentOps reports under `aggregate_metrics`).
- - **Detailed metrics results** — one row per dataset sample with the
- pass/fail for `coherence`, `fluency`, and the local rubric
- (`smoke-core`).
-
-> **Tip:** keep this tab open as you iterate. Every new
-> `agentops eval run` creates a new evaluation run in the same list.
-
-## 11. Harden the gate: conversation-aware dataset and rubric
-
-The smoke gate proves the workspace works. Before generating CI, harden
-the same gate with multi-turn rows that line up with future trace replay
-and a rubric that scores the Travel Agent's product behavior.
-
-### Create a synthetic multi-turn dataset
-
-Define a small set of synthetic multi-turn rows. They are not claiming
-the agent already produced the assistant turns verbatim — they define
-controlled conversation scenarios the next response must handle.
-
-> **Copilot assist:** `/skills agentops-dataset` can draft these
-> conversation scenarios. Ask for synthetic multi-turn rows that keep the
-> conversation summary in `input`, preserve the structured turns in
-> `messages`, and write `expected` as acceptance criteria.
-
-Keep the important context inside `input` (the field AgentOps maps to the
-azd `query`) and keep `messages` alongside it so the dataset matches the
-shape of future trace-derived rows.
+The terminal prints only aggregate pass/fail. For the per-row, per-evaluator
+detail, either open the deep link in `.agentops/results/latest/azd_evaluation.json`
+(`report_url`), or in [the portal](https://ai.azure.com) pick the
+`travel-agent-sandbox` project, open Agents, select `travel-agent`, and open the
+Evaluations tab. The two views that matter are **Overall metric results** (the
+aggregate pass rate per evaluator) and **Detailed metrics results** (one row per
+sample). Keep this tab open while you iterate; each `agentops eval run` adds a new
+run here.
-```powershell
-@'
+## 11. Harden the gate
+
+The smoke gate proves the workspace works. Now harden it with multi-turn rows
+and a product-specific rubric before generating CI.
+
+!!! concept "Why multi-turn and a rubric"
+ Real users do not ask one perfect question. They follow up, change their
+ mind, and rely on earlier context, so a single-turn test misses most of what
+ can go wrong. Multi-turn rows exercise memory and coherence across a
+ conversation, and a product-specific rubric tells the judge what good means
+ for your agent instead of a generic notion of helpfulness.
+
+### Add a synthetic multi-turn dataset
+
+These rows define controlled conversation scenarios the next response must
+handle. Keep the context in `input` (the field AgentOps maps to the azd `query`)
+and the structured turns in `messages` so the dataset matches future
+trace-derived rows.
+
+```text
+edit .agentops/data/travel-conversations.jsonl
+```
+
+```json
{"input":"Conversation so far: the user wants to visit Rome with two kids. The assistant asked how many days and what pace they prefer. The user answered: three days, moderate pace, museums and food. Now plan the trip.","expected":"The agent should preserve the family-with-kids constraint, propose a practical three-day Rome itinerary, include transit/rest pacing, and avoid claiming it can book live reservations.","messages":[{"role":"user","content":"We want to visit Rome with two kids."},{"role":"assistant","content":"How many days do you have and what pace do you prefer?"},{"role":"user","content":"Three days, moderate pace, museums and food."}]}
{"input":"Conversation so far: the user needs a low-budget food weekend. The assistant asked whether they are choosing between specific cities. The user answered: Lisbon or Seattle. Now compare those options.","expected":"The agent should compare both destinations, mention budget tradeoffs, food activities, transit/weather notes, and avoid unsupported price or booking claims.","messages":[{"role":"user","content":"I need a low-budget food weekend."},{"role":"assistant","content":"Are you choosing between specific cities?"},{"role":"user","content":"Lisbon or Seattle."}]}
-'@ | Set-Content -Encoding utf8 .agentops\data\travel-conversations.jsonl
```
Point `agentops.yaml` at it:
@@ -895,87 +536,58 @@ dataset: .agentops/data/travel-conversations.jsonl
dataset_kind: multi-turn
```
-Re-init the recipe and run the gate again:
+Re-init the recipe and re-run the gate:
```powershell
agentops eval init --force
agentops eval run
```
-When it passes, `results.json` records `execution: azd`, the evaluator
-list, the multi-turn dataset kind, and the threshold results.
-
-> **See it in the Foundry portal.** Open the new evaluation run using
-> the deep link in `.agentops/results/latest/azd_evaluation.json`
-> (`report_url`) or the manual nav described in
-> [See the run in the Foundry portal](#see-the-run-in-the-foundry-portal).
-> The **Detailed metrics results** table now shows one row per
-> multi-turn sample, so you can compare how the agent handled the Rome
-> and Lisbon/Seattle scenarios independently.
+When it passes, `results.json` records the multi-turn dataset kind and the
+threshold results. Open the new run in Foundry as in
+[See the run in the Foundry portal](#see-the-run-in-the-foundry-portal);
+**Detailed metrics results** now shows one row per scenario.
-> **What did this gate test?** Individual synthetic conversation-context
-> turns, not the Foundry portal **Full conversations** preview. AgentOps
-> uses `messages` to preserve the conversation shape and
-> `dataset_kind: multi-turn` to make the release evidence
-> conversation-aware. For end-to-end full-conversation evaluation, use
-> the optional Foundry path below.
+!!! note "What this tested"
+ Individual synthetic conversation-context turns, not Foundry's Full
+ conversations preview. `messages` preserves the conversation shape and
+ `dataset_kind: multi-turn` makes the evidence conversation-aware. For
+ end-to-end full-conversation review, use the optional Foundry path below.
-### Optional: Full conversations evaluation in the Foundry portal
-
-This is a Foundry-native deeper review path, not a required step in the
-automated release gate. The automated gate for this tutorial stays
-AgentOps + azd + Doctor evidence.
-
-| If you have... | Use this dataset source |
-|---|---|
-| No production conversations yet | Start with the synthetic rows from `.agentops/data/travel-conversations.jsonl`. |
-| A deployed agent with traffic | Use Foundry traces or exported conversation logs, then convert/select those conversations as the Foundry evaluation dataset. |
-| A curated review set from your team | Upload that approved conversation dataset in the format the portal asks for. |
+### Optional: Full conversations in the Foundry portal
-For this tutorial, start with the synthetic file you just created. Later,
-replace that with real Foundry traces or approved conversation logs.
+This is a Foundry-native review path, not part of the automated gate. Start with
+the synthetic file you just created; later swap in real traces or approved
+conversation logs.
-1. Open your Foundry project in .
-2. Go to **Evaluation** and create a new evaluation.
-3. Choose the **Full conversations (preview)** scope.
-4. Select or upload the conversation dataset you want Foundry to evaluate.
-5. Run the evaluation and review the result in Foundry.
+1. Open your project in [the portal](https://ai.azure.com).
+2. Go to Evaluation and create a new evaluation.
+3. Choose the Full conversations (preview) scope.
+4. Select or upload the conversation dataset.
+5. Run it and review in Foundry.
-Reference: [Run evaluations from the Microsoft Foundry portal](https://learn.microsoft.com/azure/foundry/how-to/evaluate-generative-ai-app#create-an-evaluation).
+Reference: [Run evaluations from the Foundry portal](https://learn.microsoft.com/azure/foundry/how-to/evaluate-generative-ai-app#create-an-evaluation).
-### Add the Travel Agent rubric to the release gate
+### Add the Travel Agent rubric
-A normal evaluator checks a general quality signal (coherence, fluency).
-A rubric evaluator is still usually an LLM-as-a-judge evaluation, but the
-judge is guided by product-specific criteria you define for this agent.
+A rubric is a set of named, product-specific criteria, called dimensions, that an
+LLM judge scores. For example, a "safe booking behavior" dimension checks that the
+answer never claims a live booking it cannot make. The local rubric evaluator that
+`agentops eval init` generated is named `smoke-core`, and its dimensions live in a
+JSON file on disk. Concepts: [Evaluation](evaluation.md).
-For the Travel Agent, the rubric asks:
+For the Travel Agent the rubric asks:
| Rubric dimension | What the judge checks |
|---|---|
-| Task success | Did the answer complete the user's travel-planning goal? |
-| Constraint following | Did it preserve constraints such as kids, budget, trip length, and pace? |
-| Safe booking behavior | Did it avoid claiming live bookings, confirmations, or prices it cannot verify? |
-
-The `eval_model` in the generated azd recipe is the judge. The rubric
-file tells it which dimensions to score, and the thresholds in
-`agentops.yaml` decide whether the gate passes.
-
-Fill in two kinds of real names: the rubric evaluator name and the rubric
-dimension names. Do not invent values — both must come from files
-`agentops eval init` already generated on disk.
-
-> **About the auto-generated evaluator.** When you ran `agentops eval
-> init`, azd seeded `src/travel-agent/eval.yaml` with two kinds of
-> evaluators: built-ins like `builtin.coherence` and `builtin.fluency`
-> (general response-quality checks) plus a local rubric evaluator —
-> typically `name: smoke-core` — whose `local_uri` points at a JSON file
-> with rubric dimensions specific to this Travel Agent. That local
-> evaluator is the hook AgentOps `rubrics:` bind to. You will reference
-> its `name:` and its dimension `id`s in the next two steps.
-
-**1. Find the evaluator name.** Open `src/travel-agent/eval.yaml` and
-look under `evaluators:` for the entry with a `local_uri`:
+| Task success | Did the answer complete the travel-planning goal? |
+| Constraint following | Did it keep constraints such as kids, budget, length, and pace? |
+| Safe booking behavior | Did it avoid claiming bookings, confirmations, or prices it cannot verify? |
+
+Use real names from the files `agentops eval init` generated, do not invent them.
+
+**1. Find the evaluator name.** In `src/travel-agent/eval.yaml`, the entry with a
+`local_uri` is your rubric evaluator (here `smoke-core`):
```yaml
evaluators:
@@ -986,12 +598,9 @@ evaluators:
local_uri: evaluators\smoke-core\rubric_dimensions.json
```
-The value you need is the `name:` of that entry. In this example,
-`smoke-core`.
-
-**2. Find the dimension names.** Open the file the `local_uri` points to
-(e.g. `src/travel-agent/evaluators/smoke-core/rubric_dimensions.json`).
-Each object's `id` is a metric name azd will emit:
+**2. Find the dimension ids.** Open the file the `local_uri` points to
+(`src/travel-agent/evaluators/smoke-core/rubric_dimensions.json`). Each `id` is a
+metric name azd will emit:
```json
[
@@ -1000,19 +609,12 @@ Each object's `id` is a metric name azd will emit:
{ "id": "user_satisfaction", "description": "...", "weight": 4 },
{ "id": "adherence_to_constraints", "description": "...", "weight": 3 },
{ "id": "itinerary_clarity", "description": "...", "weight": 2 },
- { "id": "general_quality", "description": "...", "weight": 5,
- "always_applicable": true }
+ { "id": "general_quality", "description": "...", "weight": 5, "always_applicable": true }
]
```
-For this quickstart the three dimensions that map to Task success /
-Constraint following / Safe booking are:
-
-| Dimension intent | Dimension `id` to use |
-|---|---|
-| Task success | `correct_itinerary` |
-| Constraint following | `adherence_to_constraints` |
-| Safe booking behavior | `clear_practical_notes` |
+The three that map to Task success, Constraint following, and Safe booking are
+`correct_itinerary`, `adherence_to_constraints`, and `clear_practical_notes`.
**3. Add `rubrics:` and `thresholds:` to `agentops.yaml`:**
@@ -1038,17 +640,6 @@ thresholds:
fluency: ">=0.6"
```
-> **Why threshold the evaluator, not the dimensions?** `azd ai agent
-> eval` emits one aggregate pass-rate metric per evaluator
-> (`coherence`, `fluency`, `smoke-core`), not one metric per rubric
-> dimension. The dimension `id`s live inside the local rubric file and
-> guide the judge's prompt, but azd does not surface them as separate
-> metrics today, so thresholds bind to the evaluator names azd actually
-> reports. The `rubrics:` block above is still recorded in
-> `results.json` and the release evidence pack as documentation of what
-> the judge was asked to score. Values are pass rates in `0..1` (e.g.
-> `">=0.6"` means at least 60% of rows passed the evaluator).
-
**4. Regenerate the recipe and re-run the gate:**
```powershell
@@ -1056,93 +647,73 @@ agentops eval init --force
agentops eval run
```
-When this passes, the gate enforces both the conversation-context dataset
-and the Travel Agent rubric pass-rate threshold. If a threshold key is
-wrong, AgentOps cannot bind it to an emitted metric — open
-`.agentops/results/latest/results.json` and look at
-`aggregate_metrics` to see exactly which evaluator names azd produced
-for this recipe.
-
-> **See the per-dimension rubric scores in the Foundry portal.** The
-> CLI threshold lives on the `smoke-core` aggregate, but Foundry still
-> records every dimension the judge scored. Open the run as in
-> [See the run in the Foundry portal](#see-the-run-in-the-foundry-portal),
-> scroll to **Detailed metrics results**, find the `smoke-core` column,
-> and click **View rubric details** on any row. The modal shows:
->
-> - The aggregated rubric score (e.g. `0.92 / 1.0`).
-> - The judge's free-text explanation of the overall result.
-> - One row per dimension (`correct_itinerary`, `clear_practical_notes`,
-> `user_satisfaction`, `adherence_to_constraints`,
-> `itinerary_clarity`, `general_quality`) with the individual score
-> (1–5), pass/fail badge, and the judge's reason for that dimension.
->
-> This is the most useful drill-down when you are iterating on the
-> rubric file: it tells you not just *whether* the rubric passed, but
-> *which dimension* drove the result on each sample.
-
-## 12. Add ASSERT and Red Team to the release gate
-
-The eval gate proves quality. Two additional release-readiness signals
-deserve to run inside the same loop:
-
-- **ASSERT** (open-source `assert-ai`) — turns natural-language policies
- into executable behavior tests (prompt injection, jailbreak,
- hallucination, PII leak, unauthorized tool use). Repo:
- .
-- **AI Red Teaming** (Foundry agent, PyRIT-backed) — generates
- adversarial prompts across risk categories (violence, hate, self-harm,
- sexual) and applies attack strategies (base64, rot13, morse) to surface
- safety regressions. Docs:
- .
-
-AgentOps does not reimplement either. It orchestrates them as active CI
-steps, gates the pipeline on their results, and writes normalized JSON
-summaries that the evidence pack ingests automatically.
+!!! info "Why thresholds bind to the evaluator, not each dimension"
+ azd emits one aggregate pass-rate per evaluator (`coherence`, `fluency`,
+ `smoke-core`), not one per dimension. The dimension ids guide the judge's
+ prompt and are recorded in `results.json` as documentation. Values are pass
+ rates in `0..1`, so `">=0.6"` means at least 60% of rows passed. If a threshold
+ key will not bind, check `aggregate_metrics` in
+ `.agentops/results/latest/results.json` for the exact evaluator names. Foundry
+ still shows per-dimension scores under Detailed metrics results, then
+ View rubric details.
+
+## 12. Add ASSERT and Red Team
+
+The eval gate proves quality. Two safety signals belong in the same loop:
+
+!!! concept "Quality is not safety"
+ The eval gate asks whether the answer is good. It does not ask whether the
+ agent stays safe when someone attacks it, and a helpful agent can still be
+ jailbroken. ASSERT turns plain-language safety policies into executable
+ checks, and Red Team generates adversarial prompts to find regressions.
+ Running all three is defense in depth: each one catches a failure the others
+ miss.
+
+- **ASSERT** (open-source `assert-ai`) turns natural-language safety policies into executable behavior tests. For example, it can check the agent refuses a prompt-injection instruction hidden in tool output. Repo: .
+- **AI Red Teaming** (a Foundry agent, PyRIT-backed) generates adversarial prompts across risk categories (violence, hate, self-harm, sexual) and applies attack strategies (base64, rot13, morse) to surface safety regressions. Docs: .
+
+AgentOps runs each as a CI step, gates on the result, and writes normalized JSON
+the evidence pack ingests. ASSERT ships behavior presets; this tutorial uses the
+built-in `travel_planner` preset, which covers tool misuse, constraint
+violations, fabricated details, stereotyping, prompt-injection-via-tool-output,
+and sycophancy.
### Run ASSERT against the Travel Agent
-You have two ways to wire up ASSERT — pick whichever fits your workflow.
+Two ways. Pick one.
-#### Option A — Ask Copilot (recommended once skills are installed)
+#### Option A: ask Copilot (recommended once skills are installed)
-If you installed the AgentOps coding-agent skills in step 4
-(`agentops skills install`), the `agentops-governance` skill knows the full
-recipe — including the real `assert-ai 0.1.0` schema and the built-in
-`travel_planner` behavior preset. In Copilot Chat (or Claude Code), paste this
-prompt:
+If you installed the AgentOps skills (step 2), the `agentops-governance` skill
+knows the full recipe, including the `travel_planner` preset. In Copilot Chat:
```text
-Use the agentops-governance skill to scaffold ASSERT for this workspace.
-Use the built-in travel_planner behavior preset, target the gpt-4o-mini
-Azure deployment, judge with safety-core + alignment presets.
+Use the agentops-governance skill to scaffold ASSERT for this workspace. Use the
+built-in travel_planner behavior preset, target the gpt-4o-mini Azure deployment,
+and judge with the safety-core and alignment presets.
```
-Copilot will install `assert-ai`, create `./assert/eval_config.yaml` against
-the real pipeline schema, and append the `assert:` block to `agentops.yaml`.
-Skip to **LiteLLM environment variables** below.
+Copilot installs `assert-ai`, writes `./assert/eval_config.yaml`, and appends the
+`assert:` block to `agentops.yaml`. Skip to LiteLLM environment variables.
-> Don't have the skill yet? Re-run `agentops skills install --force` to refresh
-> your `.github/skills/` (or `.claude/commands/`) directory. Requires
-> AgentOps **0.3.21 or later** for the corrected ASSERT scaffold.
+!!! note "No skill yet?"
+ Run `agentops skills install --force` to refresh `.github/skills/`. Requires
+ AgentOps 0.3.21 or later for the corrected ASSERT scaffold.
-#### Option B — Run the commands yourself
+#### Option B: run the commands yourself
-Install ASSERT:
+Install ASSERT, then write the config. The `travel_planner` preset matches the
+failure modes this tutorial cares about.
```powershell
pip install assert-ai
```
-`assert-ai 0.1.0` ships with a built-in `travel_planner` behavior preset that
-covers tool misuse, constraint violations, fabricated details, stereotyping,
-prompt-injection-via-tool-output, and sycophancy — exactly the failure modes
-this tutorial cares about. Drop a working `eval_config.yaml` next to your
-project:
+```text
+edit ./assert/eval_config.yaml
+```
-```powershell
-New-Item -ItemType Directory -Force .\assert | Out-Null
-@'
+```yaml
# Real assert-ai 0.1.0 pipeline schema.
suite: travel-agent-v1
run: ci-tutorial
@@ -1182,12 +753,11 @@ pipeline:
preset:
- safety-core
- alignment
-'@ | Set-Content -Encoding utf8 .\assert\eval_config.yaml
```
-> Want to design your own behavior + dimensions instead of using the preset?
-> Run `assert-ai init` — it's an interactive LLM-driven designer that ships
-> with the package and writes a validated YAML.
+!!! note "Design your own instead"
+ `assert-ai init` is an interactive LLM-driven designer that ships with the
+ package and writes a validated YAML if you want custom behavior and dimensions.
Add the `assert:` block to `agentops.yaml`:
@@ -1199,8 +769,8 @@ assert:
#### LiteLLM environment variables
-`assert-ai` invokes models through LiteLLM. For Azure OpenAI deployments,
-LiteLLM expects three env vars in your shell or `.agentops/.env`:
+`assert-ai` calls models through LiteLLM, which for Azure OpenAI expects three
+vars in your shell or `.agentops/.env`:
```powershell
$env:AZURE_API_KEY = ""
@@ -1208,8 +778,8 @@ $env:AZURE_API_BASE = "https://.openai.azure.com"
$env:AZURE_API_VERSION = "2024-10-21"
```
-These can mirror values you already have for `AZURE_OPENAI_API_KEY` and
-`AZURE_OPENAI_ENDPOINT` — LiteLLM just uses different names.
+These can mirror your existing `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT`;
+LiteLLM just uses different names.
#### Run it through AgentOps
@@ -1224,42 +794,37 @@ What AgentOps does for you:
3. Locates the run output under `artifacts/results///`.
4. Parses `metrics.json` and `scores.jsonl` for per-dimension verdicts.
5. Writes a normalized summary at `.agentops/assert/latest.json`.
-6. Exits non-zero (code 2) when ASSERT reports any policy violation,
- unless you pass `--no-gate` or set `assert.fail_on_violations: false`.
+6. Exits non-zero (code 2) on any policy violation, unless you pass `--no-gate` or set `assert.fail_on_violations: false`.
### Run the AI Red Teaming agent
-Same pattern: Copilot can do it, or you can run the commands yourself.
+Same pattern: Copilot or commands.
-#### Option A — Ask Copilot
-
-Paste this prompt into Copilot Chat (or Claude Code):
+#### Option A: ask Copilot
```text
Use the agentops-governance skill to scaffold the Red Team runner for this
-workspace. Target the gpt-4o-mini deployment, fail when attack success rate
-exceeds 20%.
+workspace. Target the gpt-4o-mini deployment and fail when the attack success
+rate exceeds 20%.
```
-#### Option B — Run the commands yourself
+#### Option B: run the commands yourself
-Install Foundry's Red Team SDK (it ships under an extra of
-`azure-ai-evaluation`):
+Install Foundry's Red Team SDK (an extra of `azure-ai-evaluation`):
```powershell
pip install "azure-ai-evaluation[redteam]"
```
-Add the `redteam:` block to `agentops.yaml`. **Start small** — the attack
-matrix is `risk_categories × attack_strategies × num_objectives` and each
-attack costs ~3 LLM calls (adversarial prompt + target + judge), so even
-modest configs take 15+ minutes:
+Add the `redteam:` block to `agentops.yaml`. Start small: the matrix is
+`risk_categories x attack_strategies x num_objectives` and each attack costs about
+3 LLM calls, so even modest configs take 15+ minutes.
```yaml
redteam:
target:
model_deployment: gpt-4o-mini
- # Tutorial-friendly: 2 × 1 × 3 = 6 attacks (~2-3 min).
+ # Tutorial-friendly: 2 x 1 x 3 = 6 attacks (~2-3 min).
# Production gates typically use 4-6 categories, 3-5 strategies, 5-10 objectives.
risk_categories: [violence, hate_unfairness]
attack_strategies: [base64]
@@ -1270,16 +835,12 @@ redteam:
Available `risk_categories`: `violence`, `hate_unfairness`, `self_harm`, `sexual`.
Common `attack_strategies`: `base64`, `rot13`, `morse`, `binary`, `ascii_art`, `flip`.
-> **Foundry account types.** AgentOps auto-detects which project shape the
-> Red Team SDK expects. New (hub-less) Foundry accounts use the
-> `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` URL as a string — the SDK takes the
-> OneDP path and skips AML workspace discovery (which would 404 because
-> hub-less accounts have no AML workspace). Legacy hub-based accounts fall
-> back to the `AZURE_SUBSCRIPTION_ID` + `AZURE_RESOURCE_GROUP` +
-> `AZURE_AI_PROJECT_NAME` triplet. All four vars are written by
-> `agentops init`. Auth uses `DefaultAzureCredential` — `az login` is
-> sufficient. If you see `404 Failed to connect to your Azure AI project`,
-> upgrade to AgentOps 0.3.21+ where the OneDP detection is automatic.
+!!! note "Auth and account type"
+ Auth uses `DefaultAzureCredential`, so `az login` is enough. AgentOps
+ auto-detects whether your project is hub-less (new) or hub-based (legacy) from
+ the vars `agentops init` wrote, so the scan targets the right path. If you hit
+ `404 Failed to connect to your Azure AI project`, upgrade to AgentOps 0.3.21 or
+ later.
#### Run it through AgentOps
@@ -1290,313 +851,150 @@ agentops redteam run
What AgentOps does for you:
1. Verifies the `RedTeam` Python API is importable.
-2. Resolves the target (deployment / agent / endpoint) from the YAML.
-3. Calls `RedTeam.scan(...)` with the configured risk categories,
- strategies, and objective count.
+2. Resolves the target (deployment, agent, or endpoint) from the YAML.
+3. Calls `RedTeam.scan(...)` with the configured risk categories, strategies, and objective count.
4. Aggregates per-category and per-strategy attack-success-rate.
-5. Writes a normalized summary at `.agentops/redteam/latest.json` plus
- the raw SDK payload at `.agentops/redteam/raw_summary.json`.
-6. Exits non-zero (code 2) when overall attack-success-rate exceeds
- `fail_on_attack_success_rate`, unless you pass `--no-gate`.
+5. Writes a normalized summary at `.agentops/redteam/latest.json` plus the raw payload at `.agentops/redteam/raw_summary.json`.
+6. Exits non-zero (code 2) when the attack-success-rate exceeds `fail_on_attack_success_rate`, unless you pass `--no-gate`.
-> **Heads-up.** Both commands hit live Azure services. Run them against a
-> non-production deployment and budget for the cost of the configured
-> objective count.
+!!! warning "These hit live Azure services"
+ Both commands call live models. Run them against a non-production deployment
+ and budget for the configured objective count.
### Pull both into the release evidence pack
-Both runners write to well-known paths the evidence pack auto-discovers
-(via `assert_path` and `redteam_path` resolution). When you produce the
-evidence pack:
+Both runners write to well-known paths the evidence pack auto-discovers (via
+`assert_path` and `redteam_path`). Produce the pack with:
```powershell
agentops doctor --workspace . --evidence-pack
```
-`evidence.json` and `evidence.md` now include the suite/run id, total
-cases, violation counts, attack-success-rate, and SHA-256 hashes for both
-artifacts — without claiming AgentOps invented the verdicts. The verdicts
-come from ASSERT and PyRIT; AgentOps handles orchestration, normalization,
+`evidence.json` and `evidence.md` then include the suite/run id, case and
+violation counts, attack-success-rate, and SHA-256 hashes for both artifacts. The
+verdicts come from ASSERT and PyRIT; AgentOps owns orchestration, normalization,
and gating.
-## 13. Generate the PR + dev deploy workflows
-
-> **Pipeline responsibility.** This tutorial uses `agentops workflow generate`
-> because the workflow is the release-readiness contract: it stages the prompt
-> agent, runs eval thresholds, Doctor checks, and writes release evidence. For a
-> full `azd` / AI Landing Zone app, you can also use `azd pipeline config` to
-> bootstrap the app / infra deployment pipeline, then add AgentOps checks where
-> you need release readiness proof.
+## 13. Generate the workflows
```powershell
agentops workflow generate --kinds pr,dev --deploy-mode prompt-agent --doctor-gate critical --force
```
-This creates two workflow files:
+| Flag | What it does |
+|---|---|
+| `--kinds pr,dev` | Generate both the PR gate and the dev deploy workflows. |
+| `--deploy-mode prompt-agent` | Wire the prompt-agent staging and cloud eval path. |
+| `--doctor-gate critical` | Fail the PR gate only on critical Doctor findings. |
+| `--force` | Overwrite existing workflow files. |
+
+This creates two files:
```text
.github/workflows/agentops-pr.yml
.github/workflows/agentops-deploy-dev.yml
```
-The PR workflow now has two jobs:
-
-1. **`stage-candidate`** — stages an ephemeral Foundry prompt-agent
- candidate in the **dev** Foundry project (not sandbox).
- - On the **very first PR**, dev is still empty. The stage step looks
- up `travel-agent:2` and gets a 404. It then reads
- `prompt_agent_bootstrap` from `agentops.yaml` plus `prompt_file`
- and creates a new version of the agent in dev via the Foundry SDK.
- The SDK assigns the version number per-project — typically `:1` in
- an empty project — so the bootstrapped candidate is normally
- `travel-agent:1`. The stage step reports `action: bootstrapped`.
- - On every subsequent PR, dev's version count gradually catches up to
- the sandbox seed (`:2`). Until it does, the stage step keeps
- bootstrapping. Once dev has `travel-agent:2`, the stage step
- switches to the normal lookup path: it reads `travel-agent:2`'s
- definition, replaces the instructions with `prompt_file`, and
- either re-uses the same version (when the prompt is byte-identical
- to the seed) or lets Foundry auto-create the next number. The
- stage step then reports `reused` or `created`.
- In all cases, the workflow writes
- `.agentops/deployments/agentops.candidate.yaml` pointing at the
- staged candidate.
-2. **`eval`** — runs `agentops eval run` against the candidate, then
- runs Doctor with `--severity-fail critical`. Because the previous step
- moved the gate to a conversation dataset, the workflow is not just checking a
- single smoke response: it runs the Foundry / azd evaluation recipe against the
- multi-turn Travel Agent rows and writes normalized evidence to
- `.agentops/results/latest/results.json`.
-
-> **Why does the PR workflow stage in dev, not sandbox?** The PR gate
-> must evaluate the same target the deploy workflow will use. Sandbox
-> is the author's playground and never receives CI traffic.
->
-> Candidate versions created by PR runs are tagged in Foundry with
-> `agentops:candidate=true` plus `agentops:pr=` and
-> `agentops:created_at=`. Portal viewers can filter the
-> Versions tab on `agentops:candidate` to separate "abandoned PR
-> candidates" from "deployed versions of record". Downstream consumers
-> that resolve `` to "latest" should skip versions carrying
-> `agentops:candidate=true`; the supported pinning mechanism remains
-> `foundry-agent.json`, which always points at the deployed-of-record
-> version. AgentOps uses prompt SHAs and git SHAs as the durable
-> identity, not old candidate version numbers.
-
-The dev deploy workflow stages a candidate (same logic), evaluates it,
-summarizes the deployment via `prompt_deploy summarize`, and uploads
-`.agentops/deployments/foundry-agent.json` as a workflow artifact.
-The deploy gate uses the same conversation-aware `agentops eval run`, so the
-candidate that lands in dev has already passed the gate reviewers saw on the PR.
-
-The `--doctor-gate critical` flag controls the Doctor severity floor in
-the PR template. The table below summarizes the three values:
+The PR workflow has two jobs:
+
+1. `stage-candidate` stages an ephemeral Foundry prompt-agent candidate in the **sandbox** project. A candidate is a throwaway agent version CI creates just to evaluate a PR, not a dev deployment. The step writes `.agentops/deployments/agentops.candidate.yaml`.
+2. `eval` runs `agentops eval run` against the candidate, then Doctor with `--severity-fail critical`. Because the gate now uses the multi-turn dataset, this checks conversation behavior, not a single smoke response.
+
+!!! note "Candidate versions in Foundry"
+ PR candidates are tagged `agentops:candidate=true`, `agentops:pr=`, and
+ `agentops:created_at=`. Filter the Versions tab on
+ `agentops:candidate` to separate abandoned candidates from deployed versions.
+ The durable identity is the prompt SHA and git SHA, not the version number, and
+ `foundry-agent.json` always points at the deployed-of-record version.
+
+The dev deploy workflow stages a candidate the same way, evaluates it with the
+same conversation-aware gate, summarizes via `prompt_deploy summarize`, and
+uploads `.agentops/deployments/foundry-agent.json` as an artifact.
| `--doctor-gate` value | PR Doctor behavior |
|---|---|
-| `critical` (default) | The PR step fails if Doctor reports any critical findings. Use this to catch regressions that pass thresholds but still drift meaningfully (for example, `groundedness` 5.0 → 4.0). |
-| `warning` | The PR step fails on warnings or critical findings. Tighter; useful for late-stage hardening. |
-| `none` | Doctor runs advisory only. The PR step never fails because of Doctor. Use this only if you have a separate scheduled Doctor pipeline that makes the readiness call. |
-
-Deploy templates always run with `--severity-fail critical` regardless of
-`--doctor-gate`. The gate flag affects the PR template only; deploys are
-the last-mile production gate and should always block on critical
-findings.
+| `critical` (default) | PR fails on any critical finding. Catches regressions that pass thresholds but still drift (for example `groundedness` 5.0 down to 4.0). |
+| `warning` | PR fails on warnings or critical findings. Tighter, for late-stage hardening. |
+| `none` | Doctor runs advisory only and never fails the PR. Use only if a separate scheduled Doctor owns the readiness call. |
-## 14. Wire CI: GitHub repository + Azure OIDC + dev environment
+Deploy templates always run `--severity-fail critical` regardless of
+`--doctor-gate`. The gate flag affects only the PR template; deploys are the
+last-mile gate and always block on critical findings.
-The workflows live only on your machine right now. CI will not run until
-the folder is a GitHub repository, pushed to a remote, and connected to
-Azure with OIDC. Use the `agentops-workflow` Copilot skill so the GitHub
-and Azure work happens in chat with explicit prompts and review.
+## 14. Wire CI and OIDC
-You already installed the AgentOps Copilot skills in step 2, so you can
-jump straight to Copilot Chat. If it has been a while since step 2 (for
-example, you upgraded `agentops` in between), re-run
-`agentops skills install --platform copilot --force` to refresh them.
+The workflows are local until the folder is a GitHub repo connected to Azure with
+OIDC. OIDC (OpenID Connect) lets GitHub Actions get short-lived Azure tokens
+through a federated credential, so you store no long-lived secret. Use the
+`agentops-workflow` skill to do the GitHub and Azure work in chat with review.
-Open Copilot in this repo and run:
+You installed the skills in step 2; if it has been a while, refresh with
+`agentops skills install --force`. In Copilot, run `/skills`, confirm
+`agentops-workflow` is loaded, then paste:
```text
-/skills
+Use the AgentOps workflow skill to get the generated PR gate and dev deploy
+workflows running on GitHub Actions for this Foundry prompt-agent project. This
+may be a new folder with no Git repo yet. Scope to the PR gate and dev deploy
+only: create or connect the GitHub repo, make local main track origin/main, wire
+Azure OIDC and the required Actions variables and secrets, and create only the
+sandbox and dev environments. The PR gate must use the sandbox Foundry endpoint;
+the dev deploy must use the dev Foundry endpoint. Verify the OIDC principal has
+Foundry User on both projects and Cognitive Services OpenAI User on the AI
+Services account. Do not set up qa, production, scheduled Doctor, or hosted
+deploys yet. Show me the plan before changing GitHub or Azure.
```
-Confirm `agentops-workflow` is loaded, then paste:
+The skill normally does the following. Call out anything it skips:
-```text
-Use the AgentOps workflow skill to get the generated PR gate plus dev
-deploy workflows running on GitHub Actions for this Foundry prompt-agent
-project.
-
-This may be a brand-new folder with no Git repo or GitHub remote yet.
-Keep the scope to the PR gate and dev deploy only: create or connect the
-GitHub repo if needed, ensure local `main` tracks `origin/main` after the
-first push/connect, wire Azure OIDC and required Actions variables/secrets,
-create only the `dev` environment, verify the OIDC principal has **both**
-Foundry User access on the **dev** Foundry project **and** Cognitive Services
-OpenAI User on the underlying Azure AI Services account that hosts the
-evaluator model (both roles are required — without the OpenAI User role, the
-Foundry cloud graders fail with a 401 and every metric comes back null),
-verify `AZURE_TENANT_ID` is the tenant that owns the Entra app registration
-and its federated credential (not just a subscription `managedByTenants`
-value), and do not set up `qa`, `production`, scheduled Doctor, or hosted
-deployment workflows yet.
-
-I am using trunk-based development with `main` as both my trunk and dev
-branch. The generator's stock dev-deploy trigger is `push: branches:
-[develop]`. Rewrite the `agentops-deploy-dev.yml` (and the matching
-`agentops-pr.yml` `pull_request: branches:` list, if it references
-`develop`) so they fire on `main` instead. The PR gate must run on PRs
-targeting `main`, and the dev deploy must auto-run on push to `main`
-after a merge.
-
-The dev Foundry project endpoint is in `.azure/dev/.env`; the sandbox
-endpoint is local-only and must not be added to CI.
-
-Show me the plan before changing GitHub or Azure, and call out anything
-that needs owner/admin permission.
-```
+- Create or connect the GitHub remote and make local `main` track `origin/main`. If it skips this, run `git branch --set-upstream-to=origin/main main`.
+- Create the `sandbox` and `dev` GitHub environments.
+- Configure OIDC federated credentials between GitHub and Entra ID. Reference: [Configure OIDC in Azure](https://docs.github.com/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-azure).
+- Set `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`, `AZURE_CLIENT_ID`, and `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` in each environment. Use the sandbox endpoint for `sandbox` and the dev endpoint for `dev`. Add `APPLICATIONINSIGHTS_CONNECTION_STRING` where available. Make sure `AZURE_TENANT_ID` is the tenant that owns the app registration, not a subscription `managedByTenants` value.
+- Assign the OIDC principal the required roles. The eval step returns all-null metrics if either is missing: Foundry User on the sandbox and dev projects, and Cognitive Services OpenAI User on the AI Services account that hosts the evaluator model. See [Foundry RBAC](https://learn.microsoft.com/azure/ai-foundry/concepts/rbac-azure-ai-foundry).
+
+The PR workflow reads the sandbox endpoint from the `sandbox` GitHub environment.
+The deploy workflow reads the dev endpoint from the `dev` GitHub environment.
+
+### Point the workflows at main
+
+This tutorial uses trunk-based development with `main` as the trunk. The
+generator emits GitFlow defaults that fire on `develop`, so retarget both files.
+Ask the skill to do it, or edit by hand:
+
+1. Open `.github/workflows/agentops-deploy-dev.yml`. Find the `push:` trigger and change its `branches:` list from `[develop]` to `[main]`.
+2. Open `.github/workflows/agentops-pr.yml`. Find the `pull_request:` trigger. If its `branches:` list includes `develop`, change it to `[main]`.
+3. Save both files. The PR gate now runs on PRs targeting `main`, and the dev deploy runs on every push to `main` after a merge.
-The workflow skill will normally do the following, but call out anything
-it skips:
-
-- Create/connect the GitHub remote and ensure local `main` tracks
- `origin/main` (`git branch -vv` should show `[origin/main]`). If the skill
- skips this, run `git branch --set-upstream-to=origin/main main` before the
- later tutorial steps that use `git pull`.
-- Create the `dev` GitHub environment.
-- Configure OIDC federated credentials between GitHub and Entra ID.
-- Check the repository's subject claim prefix before the credential is created.
- Run `gh api repos///actions/oidc/customization/sub` and read
- `sub_claim_prefix`. Accounts with immutable IDs send
- `repo:@/@:environment:dev` rather than
- `repo:/:environment:dev`, and Entra matches the subject
- literally, so the wrong format fails the first run with `AADSTS700213`.
- Creating both subjects as separate credentials on the same app registration
- works on either kind of account. See
- [`ci-github-actions.md`](ci-github-actions.md#federated-credential-subject-check-sub_claim_prefix-first).
-- Set Actions variables `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`,
- `AZURE_CLIENT_ID`, `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` (the dev
- endpoint), and `APPLICATIONINSIGHTS_CONNECTION_STRING` if available.
-- Verify `AZURE_TENANT_ID` against the app registration / federated
- credential tenant before the first run. A subscription can be associated
- with another tenant through `managedByTenants`; do not copy that tenant id
- into the GitHub environment unless the app registration and federated
- credential are actually visible there.
-- **Rewrite the dev deploy trigger to `main`.** The generator emits the
- stock GitFlow defaults (`pull_request: branches: [develop, "release/**",
- main]` on `agentops-pr.yml`, `push: branches: [develop]` on
- `agentops-deploy-dev.yml`). For this trunk-on-`main` tutorial the
- skill should rewrite both so the PR gate fires on PRs into `main` and
- the deploy fires on push to `main`. If the skill skips this rewrite,
- open the two YAML files in `.github/workflows/` and edit the
- `branches:` lists by hand before opening the first PR.
-- Verify the OIDC principal has **two** Azure RBAC roles before the first
- run. Both are required and the eval step fails silently (every metric
- returns `null`) if only one is in place:
- - **Foundry User** on the dev Foundry project — Reader alone is not
- enough for the data-plane calls the prompt-agent staging and eval steps
- make.
- - **Cognitive Services OpenAI User** on the underlying Azure AI Services
- account that hosts the evaluator model deployment. Foundry
- `azure_ai_evaluator` graders impersonate the OIDC principal to call
- OpenAI; without this role they fail with a 401 `PermissionDenied`. The
- AgentOps cloud-results parser lifts that error into `results.json` so
- you can see the cause in the artifact, but the workflow still fails
- the gate.
-
-## 15. First green PR → merge → dev deploy
-
-This is the happy path. Before the regression step, you need a clean
-green baseline so the rolling-history Doctor checks (regression, drift)
-have something to compare against.
-
-The workflow skill in step 14 already committed your local changes,
-pushed `main` to the GitHub remote, and dispatched first verification
-runs of **both** `agentops-pr.yml` and `agentops-deploy-dev.yml` (via
-`workflow_dispatch`, after asking you to approve) so the CI wiring is
-verified end-to-end. Open the repo's **Actions** tab and confirm both
-runs reached the eval stage:
-
-- `agentops-pr.yml` — `Stage Foundry prompt candidate (PR)` and
- `AgentOps eval (PR gate)` jobs both ran.
-- `agentops-deploy-dev.yml` — `stage-candidate`, `eval`, and the
- `Mark candidate as deployed` step all ran (the deploy job uses
- `prompt_deploy summarize`, not a real Foundry promotion — it writes
- the deployment record artifact + workflow summary).
-
-It is **expected** for one or both of these first runs to exit
-`threshold_failed` (`exit 2`) when the dev Foundry project starts
-empty: the bootstrap path creates a fresh `travel-agent:1` (and, on
-the next run, `:2`) in dev and evaluates it against the seed
-`agentops.yaml` thresholds, which can miss on first contact. That is
-by design, not a CI wiring failure. What you are really verifying at
-this point is the plumbing — OIDC, Foundry RBAC, the evaluator
-deployment, the staging step, the deploy summary writer — and that
-dev now contains a bootstrapped version of the agent.
-
-`agentops-deploy-dev.yml` will fire **again** automatically when you
-merge the baseline PR at the end of this section, because the skill
-rewrote its trigger from `develop` to `main` in step 14.
-
-If you want to wait on the first PR-workflow verification run from the
-terminal instead of the Actions UI:
+## 15. First green PR
+
+You need a clean green baseline so the rolling-history Doctor checks (regression,
+drift) have something to compare against.
+
+The step 14 skill already committed your changes, pushed `main`, and dispatched
+first verification runs of both workflows. Open the repo's Actions tab and
+confirm both reached the eval stage:
+
+- `agentops-pr.yml`: the stage and eval jobs ran.
+- `agentops-deploy-dev.yml`: stage, eval, and the deploy summary step ran. The deploy step writes the record artifact via `prompt_deploy summarize`, it is not a real Foundry promotion.
+
+!!! info "First runs may fail thresholds, that is expected"
+ When dev starts empty, the bootstrap path creates a fresh `travel-agent:1`
+ (then `:2`) and evaluates it against the seed thresholds, which can miss on
+ first contact. You are verifying plumbing here: OIDC, Foundry RBAC, the
+ evaluator deployment, staging, and the deploy summary writer. Foundry version
+ numbers are per-project, so dev's numbers will not match sandbox; the durable
+ identity is `prompt_sha256` plus `git_sha`. Detail: [Ship](ship.md).
+
+To watch the first PR run from the terminal:
```powershell
$prBranch = gh pr view --json headRefName --jq '.headRefName'
$runId = gh run list --workflow agentops-pr.yml --branch $prBranch --event pull_request --limit 1 --json databaseId --jq '.[0].databaseId'
-gh run view $runId --web
gh run watch $runId --exit-status
```
-What you should see in the **first** PR workflow run, after the
-skill's verification dispatches have already touched dev:
-
-1. **Stage Foundry prompt candidate (PR)** job runs first. The
- `prompt_deploy stage` step looks up `travel-agent:2` in the dev
- project. Three outcomes are possible depending on what the skill's
- verification dispatches produced:
- - `action: reused` — dev already has `travel-agent:2` with the
- same instructions as the seed (no new version created).
- - `action: created` — dev has the seed version but with different
- instructions, so Foundry auto-creates the next number (likely
- `travel-agent:3`).
- - `action: bootstrapped` — dev still does not have `travel-agent:2`
- (only `:1`, because the bootstrap can fire `:1` and `:2`
- back-to-back over two runs). The step reads
- `prompt_agent_bootstrap` plus `prompt_file` and creates the next
- SDK-assigned version, then uses it as the candidate.
-2. **AgentOps eval (PR gate)** job runs second. It evaluates the
- candidate using cloud eval. Doctor runs with
- `--severity-fail critical`; advisory findings are listed but do not
- fail the job. The first one or two PR runs against a fresh dev
- project can still fail thresholds while bootstrap catches up. After
- that, normal reuse / create flow takes over and the baseline PR
- should go green.
-
-Successive PR runs walk the same three branches above until dev's
-version count catches up to the seed (`travel-agent:2`). Once it does,
-every PR run hits the normal lookup path:
-
-- If `prompt_file` is byte-identical to the seed's instructions: the
- stage step reports `reused` and uses `travel-agent:2` as the
- candidate (no new version created).
-- If `prompt_file` differs: Foundry auto-creates the next number
- (likely `travel-agent:3`) and the stage step reports `created`.
-
-> **Why the bootstrap can fire one or two times per environment.**
-> Foundry portal saves and SDK creates can start numbering at different
-> values. The portal counts unpublished drafts (so `:1` is consumed
-> before you publish), while the SDK starts at `:1` in an empty
-> project. As long as you have not yet introduced a hand-authored seed
-> into a new environment, the first one or two CI runs there will keep
-> bootstrapping until the environment's version count reaches the seed
-> value. After that, normal reuse / create flow takes over. This is
-> fine — `prompt_sha256` + `git_sha` are the durable identity, not the
-> per-project version numbers.
-
-Now open a feature branch, modify a non-functional file (or just rerun
-the workflow), open a PR, and merge it once green:
+Now open a baseline PR and merge it once green:
```powershell
git switch -c chore/agentops-baseline
@@ -1605,27 +1003,14 @@ git push -u origin chore/agentops-baseline
gh pr create --base main --head chore/agentops-baseline --title "Baseline AgentOps run" --body "First green PR to establish history."
```
-Open the PR in GitHub. The PR check runs the same staging + eval flow.
-Whether this baseline PR goes green on the first try depends on how
-many bootstrap rounds the dev project has already absorbed (from the
-skill's verification dispatches plus any failed PRs). Once bootstrap
-catches up to the seed and the prompt is stable, the PR goes green —
-re-run the workflow on the PR if needed. Then merge.
-
-After the merge, the **AgentOps deploy (dev)** workflow runs
-automatically on `main` (the skill rewrote its trigger from `develop`
-to `main` in step 14 because this tutorial uses trunk-based flow).
-This is the **second** deploy-dev run for this repo — the first was
-the skill's verification dispatch in step 14. It stages the candidate
-(by this point most likely `action: reused` or `created`), evaluates
-it, runs `prompt_deploy summarize` to write the dev deployment summary,
-and uploads the deployment artifact.
-
-Open the deploy run and download the `foundry-agent-dev-deployment`
-artifact. Inside, open `foundry-agent.json`. In the **steady-state**
-case (the most common — the seed `travel-agent:2` already exists in
-dev and matches the prompt the PR shipped), the file looks like
-this — note the actual field names AgentOps writes:
+Whether this goes green on the first try depends on how many bootstrap rounds dev
+has absorbed. Once bootstrap catches up to the seed and the prompt is stable, the
+PR goes green; re-run the workflow if needed, then merge.
+
+After the merge, the dev deploy runs automatically on `main` (you retargeted it
+in step 14). Open the deploy run, download the `foundry-agent-dev-deployment`
+artifact, and open `foundry-agent.json`. In the steady-state case (dev already
+has `travel-agent:2` matching the shipped prompt) it looks like this:
```json
{
@@ -1649,67 +1034,33 @@ this — note the actual field names AgentOps writes:
}
```
-In the steady-state, `source_agent` and `candidate_agent` are
-**identical** (`travel-agent:2`) because the dev project already had
-`travel-agent:2` with the same instructions as the PR's `prompt_file`,
-so `prompt_deploy stage` reported `action: reused` and nothing new
-was created. The `prompt_file` and `eval_config` paths are absolute
-because they are resolved inside the GitHub Actions runner workspace
-(`/home/runner/work///...`).
-
-`action` will be one of:
-
-- **`reused`** — dev already had `travel-agent:2` with byte-identical
- instructions. No new Foundry version was created. (Steady-state and
- most-common case.)
-- **`created`** — dev had `travel-agent:2` but with **different**
- instructions, so Foundry auto-created the next number (e.g.
- `travel-agent:3`). `candidate_agent` would then be `travel-agent:3`.
-- **`bootstrapped`** — dev did not yet have `travel-agent:2` at all,
- so the stage step fell back to `prompt_agent_bootstrap` defaults
- plus `prompt_file` and asked the SDK to create the first version.
- In a fresh, empty dev project the SDK starts at `:1`, so you would
- see `candidate_agent: "travel-agent:1"` and `candidate_version: "1"`
- while `source_agent` still reports the seed (`travel-agent:2`). The
- two numbers stay different until subsequent runs catch dev up to
- the seed.
-
-That `prompt_sha256` + `git_sha` pair is what the mental-model diagram
-at the start of the tutorial referred to as **cross-environment
-identity**. When you later add qa and prod deploys, each environment
-will have its own `foundry-agent.json` with possibly different
-`candidate_agent` version numbers but the **same** `prompt_sha256` and
-`git_sha` whenever they are running the same release.
-
-> **Foundry version numbers may differ between the PR and the deploy.**
-> The PR workflow and the deploy workflow each stage independently
-> against whatever the current seed (`travel-agent:2`) looks like at the
-> moment they run. If the seed's instructions did not change between PR
-> and merge, both runs typically reuse or create the same version. If
-> another PR was staged in between, the version numbers may interleave.
-> AgentOps deduplicates against the seed, not against all prior
-> candidate versions, so two distinct PRs with the same prompt content
-> can each create their own version. The durable identifier is
-> `prompt_sha256`, not the integer suffix.
-
-## 16. Regression PR — eval gate AND Doctor blocking
-
-Now exercise the value of running Doctor as a critical PR gate. You will
-intentionally ship a worse prompt and observe **two independent failure
-modes** in the same PR:
-
-- The eval thresholds may fail because `response_completeness` drops
- below the configured floor.
-- Doctor's `regression.` checks fire because the relevant
- metric (commonly `coherence`, `response_completeness`, or
- `groundedness`) drops meaningfully from the rolling baseline. Because
- the PR workflow runs Doctor with `--severity-fail critical`, those
- findings fail the Doctor step on their own.
-
-The two gates are independent; either is sufficient to block the PR.
-This is why `--doctor-gate critical` matters: in cases where the eval
-thresholds are loose enough that a regression slips through, Doctor
-still catches it.
+!!! info "Reading foundry-agent.json"
+ `action` is one of: `reused` (dev had the seed with byte-identical
+ instructions, no new version), `created` (seed existed but instructions
+ differed, so Foundry made the next number, for example `travel-agent:3`), or
+ `bootstrapped` (dev had no seed yet, so the SDK created the first version,
+ usually `:1`). `source_agent` and `candidate_agent` match in steady state. The
+ `prompt_sha256` plus `git_sha` pair is the cross-environment identity: when you
+ add qa and prod, each gets its own `foundry-agent.json` with possibly different
+ version numbers but the same SHAs whenever they run the same release. Detail:
+ [Operate](operate.md).
+
+## 16. Catch a regression
+
+Now ship a worse prompt and watch two independent gates fail in the same PR:
+
+!!! concept "Two gates that fail independently"
+ Thresholds and Doctor catch different problems. Thresholds enforce an
+ absolute floor: a row scored below the line fails, full stop. Doctor watches
+ for drift, a meaningful drop from your rolling baseline, even when the score
+ is still above the floor. Keeping both means a slow erosion that never trips
+ the floor still gets caught before it reaches dev.
+
+- The eval thresholds may fail because `response_completeness` drops below the floor.
+- Doctor's `regression.` checks fire when a metric (often `coherence`, `response_completeness`, or `groundedness`) drops meaningfully from the rolling baseline. With `--severity-fail critical`, those findings fail the Doctor step on their own.
+
+Either gate alone is enough to block the PR. That is why `--doctor-gate critical`
+matters: if the thresholds are loose, Doctor still catches the drift.
```powershell
git fetch origin
@@ -1717,66 +1068,50 @@ $branch = "feature/regress-travel-agent-step16-$((Get-Date).ToString('yyyyMMddHH
git switch -c $branch origin/main
```
-Edit `.agentops/prompts/travel-agent.md` to this intentionally vague
-version:
+Edit the prompt to this intentionally vague version:
+
+```text
+edit .agentops/prompts/travel-agent.md
+```
```text
Answer travel questions in one vague sentence. Do not include day-by-day
plans, practical notes, constraints, or booking caveats.
```
-Commit and push:
+Commit, push, and open the PR:
```powershell
git add .agentops\prompts\travel-agent.md
git commit -m "Intentional regression: vague travel prompt"
git push -u origin $branch
gh pr create --base main --head $branch --title "Test AgentOps regression gate" --body "Evaluates an intentionally regressed travel-agent prompt."
+gh pr view --web
```
-Watch the PR check:
+In the run summary you should see staging succeed (the vague prompt differs from
+the seed, so Foundry creates a new version), the eval gate fail on
+`response_completeness`, and the Doctor step report `regression.` as
+critical. In Foundry, open the dev project's Evaluations and compare the
+regressed run with the baseline; scores are visibly lower.
-```powershell
-gh pr view --web
-```
+!!! note "Doctor not flagging regression yet?"
+ The `regression.` checks need a small history. The step 15 baseline
+ plus this run is usually enough; if you skipped the baseline, re-run the green
+ workflow once on `main` to seed history, then push the regression branch again.
-In the GitHub run summary, you should see:
-
-- **Stage Foundry prompt candidate (PR)** succeeds. The vague prompt
- differs from the seed, so Foundry creates a new version (the number
- depends on how many candidates have been staged in dev so far —
- do not depend on a specific number).
-- **AgentOps eval (PR gate)** likely fails. The summary table shows
- failed thresholds, typically on `response_completeness` — the bad
- prompt still produces fluent travel text, but it stops satisfying
- the day-by-day plan / practical notes / booking caveat criteria.
-- The **Run AgentOps Doctor** step runs with `--severity-fail critical`
- and reports `regression.` as critical. Even if the eval
- thresholds had marginally passed, this step would still fail the job.
-
-In Foundry, navigate to the dev project, open **Evaluations**, and
-compare the regressed run side-by-side with the baseline run from step
-13. The pass rate and overall metric scores should be visibly lower on
-the regressed run.
-
-> **What if Doctor does not flag regression yet?** The
-> `regression.` checks need at least a small history of prior
-> runs to compute the baseline. The baseline run in step 15 plus this
-> regression run should be enough, but if you skipped the baseline,
-> Doctor may only emit lower-severity findings. Re-run the green
-> workflow once on `main` to seed history, then push the regression
-> branch again.
-
-The lesson: this PR is blocked at PR time, before any reviewer touches
-it, and the reason is in the GitHub run summary — not buried in a
-post-deploy production alert.
+The point: the PR is blocked at PR time, before a reviewer looks, and the reason
+is in the run summary, not a post-deploy production alert.
## 17. Fix and redeploy
Restore the prompt to the good version:
-```powershell
-@'
+```text
+edit .agentops/prompts/travel-agent.md
+```
+
+```text
You are Travel Agent, a concise travel planning assistant.
Help users plan short leisure trips. Always include:
@@ -1788,7 +1123,6 @@ Help users plan short leisure trips. Always include:
Ask one clarifying question only when the destination, duration, or
traveler preference is missing. Do not invent booking confirmations,
prices, or availability.
-'@ | Set-Content -Encoding utf8 .agentops\prompts\travel-agent.md
```
Commit and push:
@@ -1799,82 +1133,58 @@ git commit -m "Restore travel agent prompt"
git push
```
-The same PR re-runs (no new PR needed). The eval should pass again and
-Doctor's regression findings should clear because the candidate's
-metrics return to the rolling baseline. Merge. The dev deploy workflow
-records the restored prompt as the dev deployment with a new
-`foundry-agent.json` artifact that has the SHA of the recovered prompt.
-
-The learning loop is the point: the prompt source of truth is in git,
-the PR workflow exercises it as a candidate in dev, Doctor catches
-regressions that thresholds alone miss, and the merge promotes through
-the deploy workflow. None of those gates require the developer to
-remember to look at a dashboard.
-
-## 18. Observability checkout: traces into continuous evaluation
-
-Take a short tour of the Foundry runtime view, then turn the same production
-signal into evaluation coverage. This is the bridge from "what happened in
-real traces" to "what should keep getting evaluated."
-
-1. Open the `travel-agent-dev` project in the Foundry portal.
-2. Open the `travel-agent` agent and switch to the **Traces** tab. If
- Application Insights is not yet connected, connect or create the
- resource now.
-3. Find a recent eval or playground run in **Conversations** or
- **Responses** and click the **Trace ID**. Inspect spans, latency,
- model calls, and the input/output panes.
-4. Switch to **Operate → Overview** and use **Ask AI** for a dashboard-level
- summary. Example:
+The same PR re-runs, no new PR needed. The eval passes again and Doctor's
+regression findings clear as metrics return to the rolling baseline. Merge, and
+the dev deploy records the restored prompt with a new `foundry-agent.json`
+carrying the recovered prompt's SHA.
- ```text
- Help me identify any issues or anomalies in my agent metrics for
- the last 24 hours.
- ```
+The learning loop is the point: the prompt lives in git, the PR exercises it as a
+sandbox candidate, Doctor catches regressions thresholds alone miss, and the
+merge promotes through deploy, none of it requiring anyone to watch a dashboard.
+
+## 18. Traces to evaluation
-5. Now use the traces as evaluation signal. In the project, open
- **Data Generation**, then select **Create dataset → From traces**.
-6. In **Create dataset**, configure:
+Tour the Foundry runtime view, then turn the same production signal into
+evaluation coverage. This is the bridge from "what happened in real traces" to
+"what should keep getting evaluated."
+
+!!! concept "Closing the loop"
+ Pre-merge gates only test what you thought to ask. Production traces show what
+ users actually did, including cases your dataset never imagined. By turning
+ real traces into new eval rows, you feed live behavior back into the gate, so
+ the agent that ships keeps teaching the gate that guards it. That feedback
+ loop is what makes the system improve instead of just hold the line.
+
+1. Open `travel-agent-dev`, open the `travel-agent` agent, and switch to the Traces tab. Connect Application Insights if prompted.
+2. Open a recent run in Conversations or Responses, click the Trace ID, and inspect spans, latency, model calls, and the input/output panes.
+3. Switch to Operate, then Overview, and use Ask AI for a summary, for example: `Help me identify any issues or anomalies in my agent metrics for the last 24 hours.`
+4. Turn traces into a dataset: open Data Generation, then Create dataset, then From traces, and configure:
| Field | Value |
|---|---|
- | **Dataset usage** | `Evaluation` |
- | **Name** | `travel-agent-traces-step18` |
- | **Agent** | `travel-agent` |
- | **Date range** | Last day or last 7 days |
- | **Maximum samples** | At least `15` |
-
- Leave **Intelligent sampling** enabled when the time-range UI shows it.
- Foundry will filter noisy traces, deduplicate near-identical prompts, and
- select a representative sample instead of evaluating every request.
-
- If the dialog shows **Setup incomplete: Assign the Foundry project's managed
- identity the Reader role on Application Insights**, click **Resolve** if you
- have permission. Otherwise ask an Azure admin to grant **Reader** on the
- connected Application Insights resource to the **managed identity of the
- `travel-agent-dev` Foundry project**. If Application Insights is
- workspace-based, grant Reader on its backing Log Analytics workspace too.
- Then wait a few minutes for RBAC to propagate and reopen the dialog.
-7. Select **Create** and track the background job on the **Data Generation**
- tab. When it finishes, open the generated dataset from the **Data** tab and
- preview the rows. This is the evaluation-ready sample created from real
- traces.
-8. If the portal offers to start an evaluation from the completed job, open it
- and confirm the generated dataset is selected. You do not need to finish a
- new eval for this tutorial step; the point is to see how Foundry turns
- traced behavior into a dataset you can evaluate continuously.
-
-> **Public preview.** Trace-to-dataset generation and intelligent sampling are
-> currently preview Foundry features. If your region or project does not show
-> **Create dataset → From traces**, continue with step 19 and treat this section
-> as a product tour.
-
-Optional KQL deep dive: query the evaluation metrics Foundry emits as
-`gen_ai.evaluation.result` events. These land in the **`AppEvents`** table, which
-only resolves in the **Log Analytics workspace** that backs your Application
-Insights resource — not in the App Insights *scoped* Logs blade. Open
-**Monitor → Logs** (or the connected Log Analytics workspace), set **Time range**
-to **Set in query** (the query below uses `ago(30d)`), and run:
+ | Dataset usage | `Evaluation` |
+ | Name | `travel-agent-traces-step18` |
+ | Agent | `travel-agent` |
+ | Date range | Last day or last 7 days |
+ | Maximum samples | At least `15` |
+
+ Leave Intelligent sampling enabled. Foundry filters noisy traces, deduplicates
+ near-identical prompts, and picks a representative sample.
+5. Select Create, track the job on the Data Generation tab, then preview the rows from the Data tab. That is an evaluation-ready sample built from real traces.
+
+!!! info "If trace-to-dataset is blocked or missing"
+ If the dialog asks you to assign the project's managed identity the Reader role
+ on Application Insights, click Resolve if you can, or ask an admin to grant
+ Reader (also on the backing Log Analytics workspace if it is workspace-based),
+ then wait a few minutes for RBAC to propagate. Trace-to-dataset and intelligent
+ sampling are preview features; if your region does not show From traces, treat
+ this section as a product tour. More: [Observe](observe.md).
+
+Optional KQL deep dive: Foundry emits evaluation metrics as
+`gen_ai.evaluation.result` events in the `AppEvents` table, which resolves only in
+the Log Analytics workspace behind your Application Insights, not the App Insights
+scoped Logs blade. Open Monitor, then Logs, set the time range to Set in query,
+and run:
```kusto
AppEvents
@@ -1892,9 +1202,7 @@ AppEvents
| take 20
```
-Each row is one conversation with its average score and a `Metrics` bag holding
-every evaluator score side by side. For a per-day rollup of average scores by
-evaluator, pivot instead:
+For a per-day rollup of average scores by evaluator, pivot instead:
```kusto
AppEvents
@@ -1908,16 +1216,12 @@ AppEvents
| order by Day desc
```
-> **Empty results?** Telemetry can be sparse, so `Last 24 hours` / `Last 7 days`
-> may return nothing. Widen the time range (`ago(30d)` with **Set in query**, or
-> **Last 30 days**) and confirm you are in the **Log Analytics workspace**, where
-> `AppEvents` resolves.
-
-Foundry gives you the runtime trace view and trace-sampled evaluation datasets;
-AgentOps Doctor checks that telemetry and release evidence are wired into the
-readiness story.
+!!! note "Empty results?"
+ Telemetry can be sparse, so Last 24 hours or Last 7 days may return nothing.
+ Widen the range (`ago(30d)` with Set in query, or Last 30 days) and confirm you
+ are in the Log Analytics workspace where `AppEvents` resolves.
-## 19. Sync local evidence and create the release evidence pack
+## 19. Build the evidence pack
```powershell
agentops eval run
@@ -1927,144 +1231,98 @@ code .agentops\agent\report.md
code .agentops\release\latest\evidence.md
```
-`agentops eval run` runs against the **sandbox** project by default
-(because `defaultEnvironment` in `.azure/config.json` is `sandbox`).
-That gives Doctor a current local snapshot to layer on top of the
-CI-side results.
+`agentops eval run` uses the sandbox project by default (because
+`defaultEnvironment` is `sandbox`), giving Doctor a current local snapshot.
+`agentops doctor --evidence-pack` can take a few minutes; it checks Azure auth,
+Foundry discovery, Azure Monitor, local eval history, and repo workflow evidence.
+Read the output in this order:
-`agentops doctor --workspace . --evidence-pack` can take a few minutes
-in a fresh workspace because it checks Azure auth, Foundry discovery,
-Azure Monitor / App Insights, local eval history, and repo workflow
-evidence. Read the output in this order:
-
-| Output | How to explain it |
+| Output | What it means |
|---|---|
-| `AgentOps pre-flight 4 ok` | The workspace, Azure auth, Foundry project, and App Insights discovery checks are all usable. |
-| `Wrote` | The local Doctor diagnostic report was generated. |
-| `Release readiness: blocked` | The command succeeded, but the current evidence has findings that block release readiness. |
-| `Evidence pack` / `Evidence report` | These are the release-review artifacts to open or attach to the PR / release discussion. |
-| `Findings: N (M critical ...)` | The severity rollup; critical items are what you discuss first. |
-| `Finding summary` | The terminal triage list. |
-
-In a fresh tutorial workspace it is normal to see warnings for scheduled CI
-(you only generated `pr` and `dev`), continuous evaluation, qa/prod
-deploys, explicit thresholds, or red-team/governance evidence. Treat those as the
-hardening backlog. The eval gates and the dev deploy loop are
-production-ready.
-
-You will likely also see **two critical findings** here, and that is expected
-in this tutorial:
+| `AgentOps pre-flight 4 ok` | Workspace, Azure auth, Foundry project, and App Insights checks are usable. |
+| `Release readiness: blocked` | The command succeeded, but the current evidence has blocking findings. |
+| `Evidence pack` / `Evidence report` | The release-review artifacts to open or attach to the PR. |
+| `Findings: N (M critical ...)` | The severity rollup; discuss critical items first. |
+
+In a fresh workspace, warnings for scheduled CI, continuous evaluation, qa/prod
+deploys, or governance evidence are normal; treat them as the hardening backlog.
+The eval gates and the dev deploy loop are production-ready. Doctor check
+reference: [Doctor checks](doctor-checks.md) and
+[Doctor explained](doctor-explained.md).
+
+You will likely also see two critical findings, and that is expected:
| Critical finding | Why it shows up |
|---|---|
-| `latency.p95_production` | App Insights p95 latency exceeds the 5s default (a prompt agent reasoning over each request runs ~9–12s). |
-| `errors.production_rate` | Your own tutorial traffic (including the earlier `az login` / token retries) pushed the production error rate above the 5% default. |
+| `latency.p95_production` | App Insights p95 exceeds the 5s default (a prompt agent reasoning per request runs ~9-12s). |
+| `errors.production_rate` | Your own tutorial traffic (including `az login` token retries) pushed the error rate above the 5% default. |
+
+These come from real telemetry of your own test traffic, not the candidate's eval
+gate (which passed). A real release would investigate latency and errors before
+promoting. To relax them for a demo, raise
+`checks.latency.p95_threshold_seconds` and `checks.errors.rate_threshold` in
+`.agentops/agent.yaml`; these are separate from the eval-gate thresholds.
+
+!!! note "Governance evidence (optional)"
+ Run `/skills agentops-governance` to draft or review pointers to ASSERT
+ policies, ACS contracts, Guardrail notes, and red-team indexes. AgentOps
+ records the paths, SHA-256 hashes, and ACS coverage in
+ `.agentops/release/latest/evidence.json`; the controls still run in their
+ owning tools.
-These criticals come from **real production telemetry of your own test
-traffic**, not from the release candidate's eval gate (which passed). They are
-honest signals: a real release would investigate latency and errors before
-promoting. For the tutorial they simply demonstrate that Doctor reads live
-runtime data. If you want to relax them for a demo, raise the Doctor thresholds
-in `.agentops/agent.yaml` (`checks.latency.p95_threshold_seconds` and
-`checks.errors.rate_threshold`) — these are separate from the `agentops.yaml`
-eval-gate thresholds.
-
-If you want to show the governance evidence path in the video, keep it as a
-short optional callout:
+## 20. Open Cockpit
```powershell
-/skills agentops-governance
+agentops cockpit --workspace .
```
-Use that skill to draft or review pointers to ASSERT policies, ACS contracts,
-Guided Guardrail review notes, and red-team evidence indexes. AgentOps records
-the paths, SHA-256 hashes, and ACS checkpoint coverage in
-`.agentops\release\latest\evidence.json`; ASSERT execution, ACS enforcement,
-Guardrail setup, and red-team scans still happen in their owning tools.
+Cockpit starts a read-only local server at `http://127.0.0.1:8090` (Ctrl+C to
+stop). It reflects the active azd environment (`sandbox`). To inspect dev, stop
+Cockpit, set `defaultEnvironment: dev` in `.azure/config.json` (or export
+`AZURE_ENV_NAME=dev`), and rerun.
-## 20. Open Cockpit
+Read the page top to bottom and confirm each card:
-```powershell
-agentops cockpit --workspace .
-```
+| Section | What to confirm |
+|---|---|
+| **Foundry connection** | Project `travel-agent-sandbox`, tenant resolved, Agent `travel-agent:2`. |
+| **Observability readiness** | Trace setup and sampling status from the latest Doctor analysis. |
+| **AgentOps Doctor** | The same rollup as step 19: 2 critical plus warnings. |
+| **Local eval history** | Your step 19 `agentops eval run` as the latest entry. |
+| **Quality metrics** | coherence, fluency, similarity, response_completeness trend cards. |
+| **Production telemetry** | App Insights p95 (~11.7s) and error rate (~12%), the source of the two criticals. |
+| **CI/CD Pipelines** | The `pr` and `dev` workflows; qa, prod, and scheduled absent (expected). |
+| **Next actions** | The prioritized backlog Cockpit derives from open findings. |
+
+Cockpit runs no checks and mutates nothing; it renders the `results.json`, Doctor
+report, and evidence pack you already produced, and links out to Foundry and
+Azure Monitor for live runtime data.
+
+## What you walk away knowing
+
+You built a complete prompt-agent release loop and now understand:
+
+- How sandbox, Git, and dev divide the work: sandbox is for authoring and PR candidates, Git is the source of truth, and dev receives reviewed prompts after merge.
+- Why AgentOps identifies a release by its `prompt_sha256` and `git_sha`, not by per-project Foundry version numbers.
+- How the PR gate stages a candidate in sandbox, runs a conversation-aware eval, and blocks on eval thresholds or Doctor criticals.
+- How a regression is caught at PR time by two independent gates, and how the fix flows straight back through deploy.
+- How traces become continuous-evaluation datasets, and how Doctor, the evidence pack, and Cockpit turn signals into a ship or no-ship call.
+
+What you built: two Foundry projects, a source-controlled prompt with a rubric and
+multi-turn dataset, generated PR and dev deploy workflows on GitHub Actions with
+OIDC, and release evidence you can attach to any PR.
+
+### Where to go next
-Cockpit starts a read-only local web server and prints
-`http://127.0.0.1:8090`. Open that URL in your browser; press `Ctrl+C`
-in the terminal to stop it. It reflects the **active azd environment**
-(`sandbox`, from `defaultEnvironment` in `.azure/config.json`) — there is
-no URL switch. To inspect `dev` instead, stop Cockpit, point the active
-env at `dev` (set `defaultEnvironment: dev` in `.azure/config.json`, or
-export `AZURE_ENV_NAME=dev`), then rerun the command.
+- Add qa and prod deploys: `agentops workflow generate --kinds qa,prod --deploy-mode prompt-agent --force`. Each environment needs its own Foundry project and bootstraps on the first run or two.
+- Add a scheduled Doctor workflow: `agentops workflow generate --kinds doctor --force`.
+- Grow the gate with vetted production traces: `agentops eval promote-traces`.
+- Add ASSERT, ACS, Guardrail review, and red-team evidence with `/skills agentops-governance` when your release process is ready for those controls.
+- Running a different kind of agent? See the [Hosted Agent Tutorial](tutorial-hosted-agent.md) for a Foundry Hosted Agent, or the [HTTP Agent Tutorial](tutorial-http-agent.md) for an agent you run behind a URL.
-Read the page top to bottom and confirm each card against what you built:
+## Repos and skills used
-| Section | What to confirm in this run |
+| Repository / skill | Used for |
|---|---|
-| **Foundry connection** | Foundry project = `travel-agent-sandbox`, your Azure tenant is resolved (`az login`), and Agent = `travel-agent:2`. |
-| **Open in Foundry** | The deep-links open your sandbox project in the correct tenant. |
-| **Observability readiness** | Trace setup / sampling status pulled from the latest Doctor analysis. |
-| **AgentOps Doctor** | The same finding rollup you saw in step 19 — **2 critical** (`latency.p95_production`, `errors.production_rate`), plus warnings. |
-| **Local eval history** | Your `agentops eval run` from step 19 appears as the latest entry. |
-| **Quality metrics** | coherence / fluency / similarity / response_completeness trend cards from your runs. |
-| **Production telemetry** | App Insights p95 latency (~11.7s) and error rate (~12%) — the source of the two criticals. |
-| **CI/CD Pipelines** | The `pr` and `dev` workflows you generated are listed; `qa`/`prod`/scheduled are absent (expected). |
-| **Next actions** | The prioritized backlog Cockpit derives from the open findings. |
-
-Cockpit does not run checks or mutate anything — it renders the latest
-`results.json`, Doctor report, and evidence pack you already produced, and
-links out to Foundry / Azure Monitor for live runtime data.
-
-## Success criteria
-
-You are done when:
-
-- Two Foundry projects exist (`travel-agent-sandbox`, `travel-agent-dev`).
- Sandbox has a hand-published `travel-agent` seed (normally `:2` after
- first publish in the portal). Dev started empty and was bootstrapped
- by CI on the first one or two deploys; the version number in dev is
- environment-local.
-- `.azure/` has both `sandbox` and `dev` environment directories, with
- `defaultEnvironment: sandbox` for local commands.
-- The prompt lives in `.agentops/prompts/travel-agent.md` and
- `agentops.yaml` references it via `prompt_file`.
-- `agentops workflow analyze` selects AgentOps cloud eval in Foundry
- with `deploy: prompt-agent`.
-- `agentops workflow generate --kinds pr,dev --deploy-mode prompt-agent
- --doctor-gate critical --force` produced a PR workflow that stages a
- candidate in the dev project and a dev deploy workflow that records
- the deployment.
-- You ran a green PR + dev deploy at least once. The deploy artifact
- `foundry-agent.json` exists with a `prompt_sha256` and `git_sha`.
-- You pushed an intentional regression. The PR was blocked twice — once
- by the eval threshold gate and once by Doctor's
- `--severity-fail critical` step. You can explain that either gate is
- sufficient on its own.
-- You restored the prompt, the PR returned to green, the merge ran the
- dev deploy again, and the new `foundry-agent.json` shows the recovered
- prompt's SHA.
-- `agentops doctor --evidence-pack` writes
- `.agentops/release/latest/evidence.md`, and the GitHub run summary
- shows its Doctor finding summary.
-- Optional safety runners are either skipped (no Doctor noise) or wired in:
- `assert:` to run `agentops assert run`, and `redteam:` to run
- `agentops redteam run`. Both write normalized JSON under `.agentops/` that
- the evidence pack ingests automatically. Pre-existing `assert_path`,
- `acs_path`, `redteam_path` references for evidence-only hash/status are
- still honored.
-- Cockpit opens and links the repo-side readiness view back to Foundry
- for both sandbox and dev.
-
-Where to go next:
-
-- Add `qa` and `prod` deploy workflows with
- `agentops workflow generate --kinds qa,prod --deploy-mode prompt-agent --force`.
- Each environment needs its own Foundry project; the first one or two
- CI runs there will bootstrap the agent via `prompt_agent_bootstrap`
- just as dev did.
-- Add the scheduled Doctor workflow with
- `agentops workflow generate --kinds doctor --force`.
-- Promote vetted production traces into the regression dataset with
- `agentops eval promote-traces` to grow the gate over time.
-- Use `/skills agentops-governance` to add ASSERT, ACS, Guardrail review, and
- red-team evidence artifacts when your release process is ready for those
- controls.
+| `Azure/agentops` | CLI, workflows, PR gate, Doctor, Cockpit. |
+| `microsoft-foundry` skill (optional) | Guides Foundry project creation in Copilot Chat. Portal fallback included. |
diff --git a/docs/tutorials.md b/docs/tutorials.md
new file mode 100644
index 00000000..1ab3eb0d
--- /dev/null
+++ b/docs/tutorials.md
@@ -0,0 +1,48 @@
+---
+hide:
+ - toc
+---
+
+# Tutorials
+
+Each tutorial is a hands-on, end-to-end walkthrough of the full AgentOps loop,
+evaluate, ship, observe, and operate, on one kind of agent. Pick the one that
+matches how your agent runs. They all teach the same sandbox to PR gate story,
+so once you finish one, the others will feel familiar.
+
+Not sure which fits? If your agent is authored and hosted in Foundry as a
+prompt referenced by `name:version`, start with the prompt agent tutorial. If
+Foundry runs your agent code as a hosted runtime behind an endpoint, use the
+hosted agent tutorial. If your agent runs as an HTTP service you operate behind
+a URL, use the HTTP agent tutorial.
+
+
+
+
+### :material-robot-happy: Prompt agent tutorial
+For a **Foundry-managed prompt agent** referenced as `name:version`. Build a small
+Travel Agent, add a PR gate, and read the evidence in the Cockpit.
+
+[Start the prompt agent tutorial :material-arrow-right:](tutorial-prompt-agent.md){ .md-button--pill }
+
+
+
+### :material-cloud-check: Hosted agent tutorial
+For a **Foundry Hosted Agent** that Foundry runs for you. Deploy it, read the
+server-side `invoke_agent` traces, and gate the deployed endpoint.
+
+[Start the hosted agent tutorial :material-arrow-right:](tutorial-hosted-agent.md){ .md-button--pill }
+
+
+
+### :material-web: HTTP agent tutorial
+For an agent that runs as an **HTTP service behind a URL**. The example is a RAG
+orchestrator (GPT-RAG). Deploy it, make the repo yours, and gate the live endpoint.
+
+[Start the HTTP agent tutorial :material-arrow-right:](tutorial-http-agent.md){ .md-button--pill }
+