diff --git a/migrate/plugins/migration-to-aws/atx/agent-advisor/README.md b/migrate/plugins/migration-to-aws/atx/agent-advisor/README.md new file mode 100644 index 00000000..4690aae1 --- /dev/null +++ b/migrate/plugins/migration-to-aws/atx/agent-advisor/README.md @@ -0,0 +1,43 @@ +# ATX Custom bundle — agent-advisor + +The plugin keeps the **full** skill under `skills/agent-advisor/`, including the Migration Plan +stage that reads the sibling `gcp-to-aws` engine. ATX Custom has no sibling skills to read, so the +transformation published there must be self-contained. + +Rather than fork the prose — which drifts, as this repo has learned more than once — the ATX +bundle is a **strict subset** of the canonical skill plus exactly one generated file: + +```text +transformation_definition.md = preamble.md + skills/agent-advisor/SKILL.md (frontmatter stripped) +``` + +Every other file in the bundle is byte-identical to its canonical source. There is no second copy +of any instruction to keep in sync. + +| File | Role | +| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `preamble.md` | The only ATX-specific text: executor differences (no interactive tools, no subagent dispatch, `${CLAUDE_PLUGIN_ROOT}` fallback), the headless input contract, the exit criteria, and the required `validation_summary.md` format | +| `manifest.txt` | Whitelist of canonical files that ship. A canonical file that is neither listed nor matched by an exclusion rule fails `atx:check` — new files must be classified, never silently dropped | + +## Commands + +```bash +mise run atx:build # write the bundle to .tmp/atx-bundle/agent-advisor/, then verify +mise run atx:check # verify only (part of `mise run lint`) +``` + +`atx:build` writes a directory of plain text and `.py` files with no build step and no install: +point whatever runs it at `transformation_definition.md` and give it that directory as the root every +relative path resolves against. Publishing to a transformation registry is a deliberate manual step, +and the registry's own CLI owns it — nothing in this repo does it for you. + +## What `atx:check` guarantees + +1. **Manifest ↔ canonical agree** — no dangling entries, no unclassified canonical files. +2. **No sibling-skill loads** — a resolvable path into another skill (`${CLAUDE_PLUGIN_ROOT}/skills/`, `$GCP_BASE`) is an error everywhere except `references/phases/migration-plan/`, whose Step -1 capability gate resolves the stage to `not_applicable` when the engine is absent. This check is what makes "the ATX build does not depend on gcp-to-aws" a machine-verified property. +3. **The capability gate exists** — `migration-plan.md` must carry its marker, so the bundle degrades honestly instead of trying to read files that are not there. +4. **The phase graph closes inside the bundle** — every `_file:` fragment and `_advances_to:` target resolves within the shipped set. +5. **Runtime scripts load on a bare host** — no top-level third-party import. Lazy imports are reported as warnings, since they only bind on a code path that may be disabled (the live model probe) or provisioned by `uv`. + +Warnings are tracked, not fatal: prose that names a sibling skill or a Claude Code slash command +still works, but it dangles for an ATX reader and should be reworded to be environment-neutral. diff --git a/migrate/plugins/migration-to-aws/atx/agent-advisor/manifest.txt b/migrate/plugins/migration-to-aws/atx/agent-advisor/manifest.txt new file mode 100644 index 00000000..75f3a88b --- /dev/null +++ b/migrate/plugins/migration-to-aws/atx/agent-advisor/manifest.txt @@ -0,0 +1,77 @@ +# ATX bundle manifest — agent-advisor +# +# Paths are relative to skills/agent-advisor/. Every file listed here is copied +# BYTE-IDENTICAL into the ATX bundle; the bundle is a strict subset of the +# canonical skill plus one generated entry file (transformation_definition.md). +# +# A canonical file that is neither listed here nor matched by an exclusion rule +# in tools/atx-bundle/build.ts fails atx:check — new files must be classified. + +references/decision-refs/agentcore.md +references/decision-refs/batch.md +references/decision-refs/cost-levers.md +references/decision-refs/ecs.md +references/decision-refs/eks.md +references/decision-refs/freshness.md +references/decision-refs/lambda-microvms.md +references/decision-refs/lambda.md +references/decision-refs/managed-alternatives.md +references/decision-refs/model-selection.md +references/decision-refs/poc-shapes.md +references/decision-refs/temporal.md +references/decision-refs/workload-classes.md +references/diagram/build-diagram.md +references/handoff/handoff-migration.md +references/models/anthropic-bedrock-2026-07-21.json +references/models/openai-bedrock-2026-07-21.json +references/output-templates/recommendation-doc.md +references/phases/add-capabilities/add-capabilities-assemble.md +references/phases/add-capabilities/add-capabilities.md +references/phases/clarify/clarify-assemble.md +references/phases/clarify/clarify-business.md +references/phases/clarify/clarify-technical.md +references/phases/clarify/clarify.md +references/phases/confirm/confirm-assemble.md +references/phases/confirm/confirm.md +references/phases/design/design-assemble.md +references/phases/design/design.md +references/phases/discover/discover-assemble.md +references/phases/discover/discover.md +references/phases/estimate/estimate-assemble.md +references/phases/estimate/estimate.md +references/phases/generate/generate-assemble.md +references/phases/generate/generate-report.md +references/phases/generate/generate.md +references/phases/intake/intake-assemble.md +references/phases/intake/intake.md +references/phases/migration-plan/migration-plan-assemble.md +references/phases/migration-plan/migration-plan-gcp-constraints.md +references/phases/migration-plan/migration-plan.md +references/phases/model-recommend/model-recommend-assemble.md +references/phases/model-recommend/model-recommend.md +references/phases/poc/poc-assemble.md +references/phases/poc/poc-report.md +references/phases/poc/poc.md +references/report-help-banner.md +references/report-shell.md +references/runtimes/agentcore.json +references/runtimes/ecs.json +references/runtimes/eks.json +references/runtimes/lambda_microvms.json +references/runtimes/lambda.json +references/vendored/dsl/INTERPRETER.md +references/vendored/README.md +scripts/anthropic_model_recommendation.py +scripts/build_diagram.py +scripts/model_recommendation.py +scripts/openai_model_recommendation.py +scripts/pyproject.toml +scripts/schemas/model-recommendation-input.json +scripts/schemas/model-recommendation.json +scripts/schemas/model-verification.json +scripts/schemas/scoring-result.json +scripts/scoring.py +scripts/uv.lock +scripts/verify_model_path.py +SKILL.md +scripts/schemas/seed.json diff --git a/migrate/plugins/migration-to-aws/atx/agent-advisor/preamble.md b/migrate/plugins/migration-to-aws/atx/agent-advisor/preamble.md new file mode 100644 index 00000000..8b36c404 --- /dev/null +++ b/migrate/plugins/migration-to-aws/atx/agent-advisor/preamble.md @@ -0,0 +1,97 @@ +# AWS Agent Advisor — ATX Custom transformation + +This transformation runs an **advisory** flow: it reads the target repository and produces a set +of assessment documents. It is **not** a code rewrite. A run that modifies application source is +a failed run, even if the application still builds. + +The instructions after this preamble are the skill's own contract (`SKILL.md` and the phase files +it loads). This preamble records only what differs in the ATX execution environment. Where the two +disagree, **this preamble wins**. + +## Environment contract + +1. **No interactive tools.** There is no `AskUserQuestion` tool and no human to answer. Wherever a + phase file says to ask the user, resolve the value instead: + - Read `.agent-advisor/seed.json` if the repository provides one, then `CLAUDE.md` / + `AGENTS.md` at the repository root. + - If a needed value is in neither, append the question and the value you assumed — with the + reason — to `.agent-advisor/UNANSWERED.md`, and continue. + - Never block waiting for input, and never invent a requirement that contradicts a value you + were given. +2. **No subagent dispatch.** Any phase whose frontmatter declares `_exec: { _agent: ... }` + (currently only Estimate) runs **inline** in this session. Its fragments and assembler execute + exactly as written; only the isolation is dropped. +3. **`${CLAUDE_PLUGIN_ROOT}` does not resolve.** Use the fallback that `SKILL.md` documents: every + path is relative to this transformation's own directory — `./scripts/...`, + `./references/runtimes/...`, `./references/decision-refs/...`, `./references/phases/...`. +4. **Scoring comes from the engine, never from your judgement.** Run `scripts/scoring.py` and + `scripts/model_recommendation.py` exactly as the phase files instruct. Prefer `uv run`; if `uv` + is not installed, invoke them with `python3` — all six runtime scripts are standard-library + only. Hand-written scores are a failed run. +5. **MCP servers are optional here.** The `awsknowledge` and `temporal-docs` servers may be absent. + Take the documented fallback path (cached values plus an honest freshness note per + `references/decision-refs/freshness.md`) as the **normal** path, not an exception. +6. **The full Migration Plan stage is unavailable in this bundle**, but the POC is not. + The plan is generated by a sibling engine that does not ship here, so that stage resolves to + `not_applicable` (marked `migration_plan_unavailable = "engine_absent"`) with a note to the + user. The POC gate is still offered, and the POC is **design-backed** — built from + `design.json` and labelled "not plan-backed". Everything through the recommendation is + unaffected. +7. **Gate answers come from the run, not from a person.** Both optional gates (Migration Plan, + POC) are decided by `.agent-advisor/seed.json` under a `gates` key — e.g. + `{"gates": {"poc": "accept"}}`. When the seed is silent, **decline** both gates and finish + after the recommendation. Never decide a gate on the user's behalf beyond that default. +8. **Do not touch AWS unless the repository explicitly authorises it.** No deployments and no live + Bedrock model probe by default; `verification.probe_status` stays `not_run` and account + availability stays provisional. + +## Output contract + +Write every artifact under `.agent-advisor//` in the target repository. Leave them in +place — do not clean up the run directory. Application source must be byte-identical when you +finish. + +Exit criteria — all of: + +`context-signals.json`, `answers.json`, `scoring-result.json`, `model-recommendation-input.json`, +`model-recommendation.json`, `confirm.json`, `design.json`, `estimate.json`, `diagram.md`, +`recommendation.md`, `mini-brief.md`, `recommendation-report.html` + +## Required validation summary format + +**Your FINAL MESSAGE of the run must be exactly the block below — nothing before it, nothing after +it.** Benchmark harnesses capture that final message verbatim as `validation_summary.md` and grade +it against this structure, so a friendly free-form wrap-up scores zero even when every criterion +actually passed. Do not summarise, do not add findings, do not add pleasantries: emit the block. + +Also write the same block to `$RUN_DIR/validation_summary.md` so the result is auditable from the +run directory itself. + +The structure, in both places: composed of `## OVERALL STATUS`, `## EXIT CRITERIA RESULTS`, and +`## UNMET CRITERIA` exactly as follows: + +```markdown +## OVERALL STATUS: COMPLETE + +## EXIT CRITERIA RESULTS + +- **Criterion:** + **Status:** PASS + **Evidence:** + +- **Criterion:** + **Status:** FAIL + **Evidence:** + +## UNMET CRITERIA + + +``` + +`## OVERALL STATUS:` takes `COMPLETE` only when every criterion is PASS; otherwise `INCOMPLETE`. +One entry per exit-criteria artifact plus one per validation command that ran. + +--- + + diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/.env.example b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/.env.example new file mode 100644 index 00000000..726125c3 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/.env.example @@ -0,0 +1,2 @@ +ANTHROPIC_API_KEY=replace-me +ANTHROPIC_MODEL=claude-3-7-sonnet-latest diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/.gitignore b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/.gitignore new file mode 100644 index 00000000..e55a50a1 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/.gitignore @@ -0,0 +1,7 @@ +.agent-advisor/ +.agent-advisor-*/ +.env +.pytest_cache/ +.venv/ +__pycache__/ +*.pyc diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/CLAUDE.md b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/CLAUDE.md new file mode 100644 index 00000000..c96d9a2b --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/CLAUDE.md @@ -0,0 +1,35 @@ +# Agent Advisor Test Context + +This repository is a static migration-assessment fixture for `agent-advisor`. +Do not rewrite the source application unless the user explicitly asks. + +## Workload + +- Existing Anthropic customer-support agent using the first-party Messages API. +- Source model: `claude-3-7-sonnet-latest`. +- Sessions normally last 10-20 minutes. +- Traffic is bursty, with fewer than 20 concurrent sessions. +- The application uses a custom tool loop and Anthropic platform features. +- The team prefers low operational overhead and has no Kubernetes requirement. + +## Model Requirements + +- Preserve the Anthropic Messages API for the first migration. +- Priority: balanced. +- Required capabilities: tool use and extended thinking. +- Required context: at most 200,000 tokens. +- Expected output: at most 16,000 tokens. +- No Bedrock Guardrails, invocation logging, or multi-model Converse requirement. +- Hypothetical target region: `us-east-1`. + +## AWS Access + +The user does not have a target AWS account yet. Claude Code's own authentication +does not count as a customer target account. Do not run AWS CLI commands or a live +model probe. Keep account availability provisional and verification `not_run`. + +## Test Boundary + +Run the recommendation flow and generate its local assessment artifacts. Do not +deploy resources. At optional Migration Plan or POC gates, wait for the user's +explicit choice. diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/README.md b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/README.md new file mode 100644 index 00000000..e60c9060 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/README.md @@ -0,0 +1,153 @@ +# Anthropic Agent Advisor Demo + +This fixture exercises the local `agent-advisor` Anthropic-to-Bedrock model +recommendation flow without requiring an AWS account or an Anthropic API key. + +For the complete change summary and operator procedure, read +[the Claude Code test SOP](../../../../doc/2026-07-20-agent-advisor-anthropic-recommendation-changes-and-test-sop.md). + +The source intentionally contains Claude 3.7 migration surfaces: + +- Anthropic Messages API and a custom tool loop +- extended thinking with `budget_tokens` +- legacy sampling parameters +- assistant prefill for JSON output +- prompt caching +- a first-party server tool +- Files API and Message Batches helpers + +The application is for static assessment. Unit tests do not call Anthropic. + +## Prerequisites + +- Claude Code +- `uv` +- This repository checked out on the feature branch + +## Load the Current Local Plugin + +From this directory: + +```bash +./run-claude.sh +``` + +The script starts Claude Code with the current checkout: + +```bash +claude --plugin-dir ./local-plugin +``` + +`local-plugin/skills/agent-advisor` is a symbolic link to the current checkout's +skill. The wrapper intentionally excludes unrelated plugin skills, so their +validation state cannot affect this focused test. This is preferable to +marketplace installation while the feature branch is not published. + +Confirm Claude Code can discover the focused plugin: + +```bash +./run-claude.sh plugin details agent-advisor-local-test +``` + +The component inventory must show `Skills (1) agent-advisor`. + +## Scenario A: Messages Continuity + +Start the flow with: + +```text +Use the agent-advisor skill to assess this existing Anthropic agent and recommend +the AWS agent architecture and Bedrock model/API path. I do not have an AWS +account, so do not run a live probe or deploy anything. +``` + +Use the requirements already recorded in `CLAUDE.md`. When asked: + +- choose the technical audience +- keep the Messages API continuity requirement +- decline the optional live probe because no target AWS account exists +- decline Migration Plan and POC if the goal is only to test recommendation + +Expected primary model decision: + +```text +decision_status: recommended +api_path: mantle_messages +primary_model: anthropic.claude-opus-4-8 +invocation_model_id: anthropic.claude-opus-4-8 +verification.probe_status: not_run +``` + +Sonnet 4.6 ranks first for balanced workloads but is unavailable on Mantle in the +dated catalog, so Opus 4.8 is the first compatible Messages-path candidate. + +Validate the artifact: + +```bash +uv run python scripts/check_recommendation.py \ + .agent-advisor//model-recommendation.json default +``` + +## Scenario B: Hard Path Conflict + +Use a fresh run directory and prompt: + +```text +Use agent-advisor for this project. We must preserve the Anthropic Messages API, +and we also require Bedrock Guardrails. Stop when the model/path conflict is +presented. We do not have an AWS account. +``` + +Expected result: + +```text +decision_status: decision_required +primary_model: null +api_path: null +decision_options: mantle_messages and runtime_converse +``` + +Validate with: + +```bash +uv run python scripts/check_recommendation.py \ + .agent-advisor//model-recommendation.json conflict +``` + +## Scenario C: Runtime Governance + +Use a fresh run directory and prompt: + +```text +Use agent-advisor for this project. Messages compatibility is not required. +Require Bedrock Guardrails and invocation logging, allow Global CRIS, and use +balanced model priority. Do not run a live probe because there is no AWS account. +``` + +Expected primary model decision: + +```text +decision_status: recommended +api_path: runtime_converse +primary_model: anthropic.claude-sonnet-4-6 +invocation_model_id: global.anthropic.claude-sonnet-4-6 +verification.probe_status: not_run +``` + +Validate with: + +```bash +uv run python scripts/check_recommendation.py \ + .agent-advisor//model-recommendation.json runtime +``` + +## Published Plugin Installation + +After this branch is published to the marketplace, Claude Code installation is: + +```text +/plugin marketplace add awslabs/startups --sparse migrate/plugins +/plugin install migration-to-aws@startups +``` + +The marketplace version may not contain this feature until publication. diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/agent-capabilities.json b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/agent-capabilities.json new file mode 100644 index 00000000..3c396a9b --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/agent-capabilities.json @@ -0,0 +1,15 @@ +{ + "agent": "customer-support", + "features": { + "mcp_connectors": [ + "crm", + "order-management" + ], + "server_tools": [ + "web_search" + ], + "files_api": true, + "message_batches": true, + "prompt_caching": true + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/local-plugin/.claude-plugin/plugin.json b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/local-plugin/.claude-plugin/plugin.json new file mode 100644 index 00000000..e99fa753 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/local-plugin/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "agent-advisor-local-test", + "version": "0.1.0", + "description": "Local test wrapper for the current agent-advisor checkout", + "author": { + "name": "Amazon Web Services" + }, + "license": "Apache-2.0", + "keywords": [ + "agent-advisor", + "anthropic", + "bedrock", + "local-test" + ] +} diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/local-plugin/skills/agent-advisor b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/local-plugin/skills/agent-advisor new file mode 120000 index 00000000..f8a63488 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/local-plugin/skills/agent-advisor @@ -0,0 +1 @@ +../../../../skills/agent-advisor \ No newline at end of file diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/pyproject.toml b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/pyproject.toml new file mode 100644 index 00000000..b354f71e --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "agent-advisor-anthropic-demo" +version = "0.1.0" +description = "Static Anthropic agent fixture for agent-advisor testing" +requires-python = ">=3.11" +dependencies = [ + "anthropic>=0.80", +] + +[dependency-groups] +dev = [ + "pytest>=8", +] + +[tool.pytest.ini_options] +pythonpath = ["src"] +testpaths = ["tests"] diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/run-claude.sh b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/run-claude.sh new file mode 100755 index 00000000..6d9f4da4 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/run-claude.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLUGIN_DIR="$PROJECT_DIR/local-plugin" + +cd "$PROJECT_DIR" +exec claude --plugin-dir "$PLUGIN_DIR" "$@" diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/scripts/check_recommendation.py b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/scripts/check_recommendation.py new file mode 100644 index 00000000..2787b467 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/scripts/check_recommendation.py @@ -0,0 +1,77 @@ +"""Check the primary recommendation produced by the demo scenarios.""" + +import argparse +import json +import pathlib + + +EXPECTED = { + # Sonnet 5 (2026-06-30) supports Mantle Messages, so balanced+Messages no + # longer escalates to Opus. + "default": { + "decision_status": "recommended", + "api_path": "mantle_messages", + "primary_model": "anthropic.claude-sonnet-5", + "invocation_model_id": "anthropic.claude-sonnet-5", + "probe_status": "not_run", + }, + "conflict": { + "decision_status": "decision_required", + "api_path": None, + "primary_model": None, + "invocation_model_id": None, + "probe_status": "not_applicable", + }, + "runtime": { + "decision_status": "recommended", + "api_path": "runtime_converse", + "primary_model": "anthropic.claude-sonnet-5", + "invocation_model_id": "global.anthropic.claude-sonnet-5", + "probe_status": "not_run", + }, +} + + +def check(path, scenario): + artifact = json.loads(path.read_text()) + workload_id = artifact["primary_unit"] + recommendation = artifact["workloads"][workload_id] + actual = { + "decision_status": recommendation["decision_status"], + "api_path": recommendation["api_path"], + "primary_model": recommendation["primary_model"], + "invocation_model_id": recommendation["invocation_model_id"], + "probe_status": recommendation["verification"]["probe_status"], + } + expected = EXPECTED[scenario] + mismatches = { + key: {"expected": expected[key], "actual": actual[key]} + for key in expected + if actual[key] != expected[key] + } + if scenario == "conflict": + paths = { + option["api_path"] for option in recommendation["decision_options"] + } + if paths != {"mantle_messages", "runtime_converse"}: + mismatches["decision_options"] = { + "expected": ["mantle_messages", "runtime_converse"], + "actual": sorted(paths), + } + if mismatches: + print(json.dumps(mismatches, indent=2)) + return 1 + print(f"PASS scenario={scenario} workload={workload_id}") + return 0 + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("artifact", type=pathlib.Path) + parser.add_argument("scenario", choices=sorted(EXPECTED)) + args = parser.parse_args() + return check(args.artifact, args.scenario) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/seeds/README.md b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/seeds/README.md new file mode 100644 index 00000000..38f12fb6 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/seeds/README.md @@ -0,0 +1,76 @@ +# Run seeds for this fixture + +Each `*.seed.json` here is one benchmark scenario: the machine-readable answers a non-interactive +run needs, validated against `skills/agent-advisor/scripts/schemas/seed.json`. + +Stage a seed by copying it to the run root the skill looks in: + +```bash +mkdir -p .agent-advisor && cp seeds/default.seed.json .agent-advisor/seed.json +``` + +`.agent-advisor/` is gitignored (it holds run output), which is why the seeds live here and are +copied in at staging time rather than committed in place. + +## Why a seed and not just `CLAUDE.md` + +`CLAUDE.md` is prose: it describes the workload well, but the agent has to _interpret_ it into +Clarify's enum values, and that interpretation is not deterministic. Measured on this fixture with +no seed — same repo, same prose, same deterministic engine: + +| Run | agentcore | lambda_microvms | +| --------------- | --------- | --------------- | +| local rehearsal | 41 | 39 | +| platform run 1 | 43 | 38 | +| platform run 3 | 44 | 41 | + +Same verdict every time (`agentcore`), different margins — because the dimensions `CLAUDE.md` does +not state (`isolation`, `idle_resume`, `deployment_preference`) were re-derived on each run. The +seed pins them, so a repeated run is byte-comparable. + +`CLAUDE.md` stays useful for context and boundaries; it just no longer carries the job of supplying +enum values. + +## Seeds + +| Seed | Scenario | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `default.seed.json` | The documented Scenario A: existing Anthropic Messages-API agent, technical audience, Messages continuity, both gates declined, no target AWS account (`probe: decline`). Expected verdict `agentcore`; expected model path `mantle_messages` / `anthropic.claude-sonnet-5` (asserted by `scripts/check_recommendation.py default`). | + +## Measured with the seed + +Six runs of the ATX bundle over this fixture, same seed, three at a time in parallel (2026-08-04; +`advisor/` = `mise run atx:build` output). Runs 1-3 exposed two drift causes, runs 4-6 confirm the +fixes: + +| | Runs 1-3 | Runs 4-6 | +| ------------------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| `scoring-result.json` | **byte-identical** (sha256 `0fcf9967…`) | **byte-identical** — same digest, so the fixes changed nothing about the score | +| verdict | `co_recommend` over agentcore 40 / lambda_microvms 38, ecs 28, eks 24 | unchanged | +| seeded `region` shape | reshaped in 1 of 3 | object preserved in 3 of 3 | +| scoring dimensions | 2 of 3 agreed | 3 of 3 agreed | +| model decision | identical: `mantle_messages` / `anthropic.claude-sonnet-5` | identical | +| `detected` features | 11 / 11 / 10, two of them false positives | 10 / 10 / 10, the same 10 | +| `blocks` / `tuning` / `deltas` | 7/3/7, 8/2/8, 7/2/7 | 7/2/7 in all three | + +The margins stopped moving, which was the point — compare the three-way spread in the table above. +The residues all had causes worth fixing, and all are fixed in the skill rather than in the seed: + +- **The seeded `region` object was reshaped** into `region` + `regions` siblings — same values, + different shape. That violates "a seeded value is copied verbatim", so `clarify.md` Step 2.5 now + says it outright, and the postcondition names both seed lookup locations instead of only + `$RUN_DIR/seed.json` (the reason the rule read as inapplicable: this fixture's seed lives at the + run root). +- **The source scan disagreed on two features** — one run marked `structured_output` `detected` with + no such call in `src/`, another marked `tokenizer_rebaseline` `detected` though nothing counts + tokens. Those false positives are what moved the findings counts. `model-recommend.md` Step 3 now + requires a citable source location for `detected`, with `absent` as the status for anything not + citable. +- **`source_paths` listed evidence files as call sites** — two runs listed the two files that call + the SDK, one also listed `tools.py` and `agent-capabilities.json`. Step 1 now defines the field as + call sites only, sorted. + +A seed cannot pin the source scan, and should not: reading the repository is the work being graded. +`agent-capabilities.json` in this fixture is the answer key for what the scan ought to find +(`server_tools`, `files_api`, `message_batches`, `prompt_caching`, MCP connectors) — a scan that +misses those, or claims a feature this app does not use, is the thing to grade. diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/seeds/default.seed.json b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/seeds/default.seed.json new file mode 100644 index 00000000..c1637a1e --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/seeds/default.seed.json @@ -0,0 +1,33 @@ +{ + "entry_point": "migrate", + "audience": "technical", + "system": { + "multi_cloud": "no", + "compliance": [], + "region": { "scope": "single", "regions": ["us-east-1"] }, + "existing_cluster": "none", + "platform_fit": "none", + "ops_preference": "minimal" + }, + "units": { + "support-agent": { + "workload_class": "agent_session", + "session_duration": "15min_to_8hr", + "traffic_pattern": "bursty", + "session_state": "stateless", + "isolation": "not_needed", + "memory_needs": "session_only", + "ops_preference": "minimal", + "compute_tier": "light", + "idle_resume": "none", + "launch_concurrency": "low", + "multi_agent": "no", + "deployment_preference": "either", + "framework": "custom", + "existing_cluster": "none", + "platform_fit": "none" + } + }, + "gates": { "migration_plan": "decline", "poc": "decline" }, + "probe": "decline" +} diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/__init__.py b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/__init__.py new file mode 100644 index 00000000..d8050595 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/__init__.py @@ -0,0 +1 @@ +"""Customer support agent demo.""" diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/agent.py b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/agent.py new file mode 100644 index 00000000..60497fd0 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/agent.py @@ -0,0 +1,102 @@ +"""Legacy Anthropic Messages agent used as a migration assessment fixture.""" + +import json +import os + +from anthropic import Anthropic + +from support_agent.tools import execute_tool + + +MODEL = os.getenv("ANTHROPIC_MODEL", "claude-3-7-sonnet-latest") +MAX_TOOL_ROUNDS = 4 + +SYSTEM_PROMPT = [ + { + "type": "text", + "text": ( + "You are a customer support agent. Use tools before making claims " + "about orders, and return concise answers." + ), + "cache_control": {"type": "ephemeral"}, + } +] + +TOOLS = [ + { + "name": "lookup_order", + "description": "Look up an order by order ID.", + "input_schema": { + "type": "object", + "properties": {"order_id": {"type": "string"}}, + "required": ["order_id"], + "additionalProperties": False, + }, + }, + { + "name": "search_help_center", + "description": "Search internal support articles.", + "input_schema": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + "additionalProperties": False, + }, + }, + { + "type": "web_search_20250305", + "name": "web_search", + "max_uses": 2, + }, +] + + +def _blocks(response): + return [ + block.model_dump() if hasattr(block, "model_dump") else block + for block in response.content + ] + + +def run_support_agent(question, force_json=False): + """Run the legacy first-party Anthropic agent loop.""" + client = Anthropic() + messages = [{"role": "user", "content": question}] + if force_json: + # Legacy assistant prefill is intentionally present for migration scanning. + messages.append({"role": "assistant", "content": "{"}) + + for _ in range(MAX_TOOL_ROUNDS): + response = client.messages.create( + model=MODEL, + system=SYSTEM_PROMPT, + messages=messages, + tools=TOOLS, + max_tokens=4096, + thinking={"type": "enabled", "budget_tokens": 1024}, + temperature=0.2, + top_p=0.9, + ) + tool_uses = [ + block for block in response.content if block.type == "tool_use" + ] + if not tool_uses: + text = [ + block.text for block in response.content if block.type == "text" + ] + return "\n".join(text) + + messages.append({"role": "assistant", "content": _blocks(response)}) + tool_results = [] + for tool_use in tool_uses: + result = execute_tool(tool_use.name, tool_use.input) + tool_results.append( + { + "type": "tool_result", + "tool_use_id": tool_use.id, + "content": json.dumps(result), + } + ) + messages.append({"role": "user", "content": tool_results}) + + raise RuntimeError("tool loop exceeded its configured round limit") diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/platform_features.py b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/platform_features.py new file mode 100644 index 00000000..46d97dac --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/platform_features.py @@ -0,0 +1,9 @@ +"""First-party Anthropic platform dependencies that require migration planning.""" + + +def upload_reference_document(client, file_handle): + return client.beta.files.upload(file=file_handle) + + +def submit_nightly_batch(client, requests): + return client.messages.batches.create(requests=requests) diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/tools.py b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/tools.py new file mode 100644 index 00000000..9011fcd6 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/src/support_agent/tools.py @@ -0,0 +1,34 @@ +"""Local tools used by the support agent.""" + + +ORDERS = { + "A-100": {"status": "shipped", "carrier": "Parcel Express"}, + "A-200": {"status": "processing", "carrier": None}, +} + +ARTICLES = { + "refund": "Refunds are available within 30 days of delivery.", + "shipping": "Standard shipping normally takes 3-5 business days.", +} + + +def lookup_order(order_id): + return ORDERS.get(order_id, {"status": "not_found", "carrier": None}) + + +def search_help_center(query): + normalized = query.lower() + matches = [ + {"topic": topic, "content": content} + for topic, content in ARTICLES.items() + if topic in normalized or normalized in content.lower() + ] + return {"matches": matches} + + +def execute_tool(name, arguments): + if name == "lookup_order": + return lookup_order(arguments["order_id"]) + if name == "search_help_center": + return search_help_center(arguments["query"]) + raise ValueError(f"unsupported tool: {name}") diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/tests/test_tools.py b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/tests/test_tools.py new file mode 100644 index 00000000..203b41d3 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/tests/test_tools.py @@ -0,0 +1,19 @@ +import pytest + +from support_agent.tools import execute_tool, lookup_order, search_help_center + + +def test_lookup_order(): + assert lookup_order("A-100") == { + "status": "shipped", + "carrier": "Parcel Express", + } + + +def test_search_help_center(): + assert search_help_center("refund")["matches"][0]["topic"] == "refund" + + +def test_unknown_tool_is_rejected(): + with pytest.raises(ValueError, match="unsupported tool"): + execute_tool("delete_order", {}) diff --git a/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/uv.lock b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/uv.lock new file mode 100644 index 00000000..5836c620 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/uv.lock @@ -0,0 +1,430 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "agent-advisor-anthropic-demo" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "anthropic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [{ name = "anthropic", specifier = ">=0.80" }] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8" }] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.117.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0d/8f71d535edb0d438f023bd825fb65f67c14fa88a2bd6b75f292a58a63de4/anthropic-0.117.0.tar.gz", hash = "sha256:98107f2b76439641e0ae2a1754087534b8f178dbab99d6eb1bc4b7bc8c744496", size = 989933, upload-time = "2026-07-16T19:36:13.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/4c/917d21d6619a4475cdafc6d13a69fdb3b901ddac57e76caca5a25c117b6d/anthropic-0.117.0-py3-none-any.whl", hash = "sha256:451a0a6905f11dff7663d13e4ee5dbf909eb8942b1d049803c7b937a13ac47ec", size = 998327, upload-time = "2026-07-16T19:36:11.225Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/SKILL.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/SKILL.md index 1f69e7fa..5c5a04d5 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/SKILL.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/SKILL.md @@ -57,8 +57,17 @@ phase (the one carrying `_init: true`). On a warm start, `current_phase` in - **Run root**: `.agent-advisor/` — `$RUN_DIR` is this skill's name for the run directory (`.agent-advisor/[MMDD-HHMM]/`). Intake's own prose performs the `_init` bootstrap. - **State shape**: § State file below (advisor-specific keys such as `entry_point`, - `audience`, `recommendation_reviewed`, `migration_plan_ctx`); the shared state schema is - not vendored. + `audience`, `recommendation_reviewed`, `migration_plan_ctx`, `migration_plan_unavailable`); + the shared state schema is not vendored. +- **Run seed (optional)**: `$RUN_DIR/seed.json`, else `.agent-advisor/seed.json` at the run root + (schema `scripts/schemas/seed.json`) supplies + machine-readable answers for a non-interactive run — the Clarify dimensions, the two gate + answers, the POC mode, the live-probe answer, and a `co_recommend` tie-break. It is the + HIGHEST-precedence source for every value it carries (clarify.md Step 2.5), which is what makes + a repeated run's score comparable: the deterministic engine gets byte-identical input. A gate + the seed omits is declined; a dimension the seed omits falls through to detection, then prose, + then an `assumed` value that MUST be recorded in `$RUN_DIR/UNANSWERED.md`. With no seed, the + interactive flow is unchanged. - **Resolved statuses**: `skipped` (routing resolved the phase without running it), plus `not_applicable` for `migration_plan` only. - **Conditional backbone routing**: the entry-point routing below. When a routing rule @@ -95,8 +104,11 @@ this file (`INTERPRETER.md` § Skill bindings, § Backbone vs sidebar). `references/decision-refs/poc-shapes.md`). Gate 2 is only offered when `migration_plan` ∈ {completed, skipped, not_applicable} — or `in_progress` on build_deploy only (Stage 2 failed/aborted; fallback POC from design.json per migration-plan.md failure handling); - for entry point `migrate`, only when `migration_plan == "completed"` (a migrate-POC - without a plan has nothing to implement). + for entry point `migrate`, only when `migration_plan == "completed"` (the POC implements the + plan) OR when the stage resolved `not_applicable` with `migration_plan_unavailable == + "engine_absent"` — a standalone deployment that does not bundle the migration engine, where + Gate 2 is offered by migration-plan.md Step -1 and the POC is design-backed. A migrate-POC + with no plan for any OTHER reason (the user declined) has nothing to implement. - Persisting Gate 2 as `phases.poc = "in_progress"` BEFORE poc.md loads makes the confirmation resumable: if the session breaks between the "yes" and the load, the interpreter re-enters `poc` without re-asking. (A declared deviation from diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/decision-refs/freshness.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/decision-refs/freshness.md index a481238b..3224ff06 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/decision-refs/freshness.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/decision-refs/freshness.md @@ -97,3 +97,7 @@ Choose the wording that matches what actually happened: - If the MCP was not called / unavailable: > _Generated ``. AWS Knowledge MCP not called this run; all facts are cached values — > verify against AWS docs before committing._ + +The footer is a summary, not the only place a date belongs. A cached number quoted in the body — +a service limit, a scaling ceiling, a price anchor — carries its own snapshot date at the point of +use, so a reader who reads one section is not relying on a footer they may never reach. diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/decision-refs/model-selection.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/decision-refs/model-selection.md index ec3ff206..5549a4bd 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/decision-refs/model-selection.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/decision-refs/model-selection.md @@ -5,51 +5,63 @@ Distilled from the migration-to-aws plugin's Q16/Q17 tables and `ai-model-lifecy - `.../shared/ai-model-lifecycle.md`. Last aligned: 2026-06-30. -This is a **coarse, family-level** mapping. Exact model IDs, pricing, TCO, EOL dates, and -regional availability come from **migration-to-aws (the llm-to-bedrock skill)** — never quote dollar figures -here. The deterministic implementation lives in `skills/agent-advisor/scripts/scoring.py::_select_model`; the drift -test in `skills/agent-advisor/scripts/test_scoring.py` locks this list against the source lifecycle file (a source -model going Legacy/EOL fails CI). +The tables below remain a **coarse compatibility hint** for old scoring-result consumers. +The authoritative deterministic implementation is the +`skills/agent-advisor/scripts/model_recommendation.py` orchestrator dispatching to per-provider +modules; each module consumes a dated capability catalog, +selects model and API path per workload, and emits compatibility deltas plus verification +status. Runtime scoring does not own model selection. Model choice is **independent of runtime choice** — it never changes which runtime is selected. -## Priority baseline (Q16) +## Joint model/path decision (per workload, per provider) + +The per-workload `(model, api_path)` decision is deterministic and lives in code, not in this +file — it is dispatched by source provider so the path vocabulary and rules differ per provider: -| priority | model | -| ------------------ | ------------------------------------------------------------- | -| quality | Claude Sonnet 4.6 (Opus 4.7 for the most demanding reasoning) | -| balanced / unknown | Claude Sonnet 4.6 | -| speed | Claude Haiku 4.5 (Nova Micro/Lite for cost-optimized speed) | -| cost | Claude Haiku 4.5 or Nova Micro | +- **Anthropic** → `scripts/anthropic_model_recommendation.py` + `references/models/anthropic-bedrock-2026-07-21.json` + (paths: `mantle_messages`, `runtime_converse`, `runtime_invoke`). +- **OpenAI** → `scripts/openai_model_recommendation.py` + `references/models/openai-bedrock-2026-07-21.json` + (paths: `mantle_openai_responses`, `runtime_converse`; Chat Completions reshapes to Responses). +- **Azure OpenAI / other** → generic `provider_module_pending` until a dedicated module exists. -## Specialized-feature override (Q17 — HARD override, beats priority) +Do not restate the per-provider path rules here — the dated catalogs and the engine are +authoritative (a table here would drift). The engine applies these provider-neutral principles: -Ask for the ONE most critical feature. It overrides the priority baseline (and, on the migrate -path, the source-model family mapping). +- **Conflicts are not precedence.** When two hard requirements cannot share one path (e.g. API + continuity plus runtime-only governance), emit `decision_required` with both tradeoff options + and rerun after the user resolves the requirement — never silently let one side win. +- **A dated snapshot is not a permanent claim.** Catalog data is point-in-time — e.g. Sonnet 4.6 + was runtime-only in the `2026-06-24` snapshot, and six days later Sonnet 5 launched with full + Mantle Messages support, changing the default recommendation. Every recommendation + carries `region`, `catalog_verified_at`, and a required live `(model, path)` probe; + recommendation acceptance does not prove account invocability. +- **Keep model identities separate.** Logical model identity, path-specific model ID, and the + resolved invocation/CRIS ID are distinct fields. Paths differ in SDK, model-ID form, IAM + action, structured-output mechanism, errors, and quotas. -| feature | primary model | notes / alternates | -| ----------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| tool_use | Claude Sonnet 4.6 | best-in-class tool use | -| long_context | Llama 4 Scout (10M native) | Claude Sonnet 4.6 for shorter long-context; Nova 2 Pro (1M) only if GA | -| extended_thinking | Claude Sonnet 4.6 (extended thinking) | Opus 4.7 for hardest | -| rag | Claude Sonnet 4.6 + Bedrock Knowledge Bases | Titan Embeddings v2 | -| multimodal | Claude Sonnet 4.6 (vision) | add Stability AI if also _generating_ images | -| image_generation | Stability AI — Stable Image Core (cost) / Ultra (quality) | separate capability, not a text-model swap; see llm-to-bedrock | -| speech | Amazon Nova 2 Sonic (speech-to-speech) | Transcribe (STT) / Polly (TTS) for one-directional; separate capability, see llm-to-bedrock | -| embedding | Amazon Titan Embeddings v2 | | -| none / unknown | (no override — priority baseline stands) | | +## Priority baseline (Q16) -**Active models only** (per the source lifecycle file). Do NOT recommend Nova Sonic v1 (→ Nova 2 -Sonic) or Nova Canvas v1 (→ Stability AI) — both are Legacy/excluded. +The `priority` answer (quality / balanced / speed / cost / unknown) sets the candidate +**ordering**, not a single model — the engine then applies hard capability and path filters. +The authoritative order is `anthropic_model_recommendation.py::_PRIORITY_ORDER` (per-provider catalogs +supply the actual models); it is not restated here to avoid drift. -## Cost/speed conflict +## Specialized features (Q17) -If a feature override selects a specialized model that contradicts a `cost`/`speed` priority -(e.g. cost + speech → Nova 2 Sonic), note in the recommendation that the specialized model may -not be the cheapest/fastest option, and that detailed pricing is downstream. Informational only. +The `model_features` answer collected in Clarify (legal values: +`tool_use | long_context | extended_thinking | rag | multimodal | image_generation | speech | +embedding | none | unknown`) feeds Model Recommend as a **hard capability filter**, not an +override — a candidate that lacks catalog evidence for a required feature fails closed +(`decision_required`), and a common feature such as tool use does not force a bigger model +when a cheaper candidate satisfies it. -## Migrate path +Three of these are **separate capabilities, not text-model swaps** — they become their own +target contracts (`additional_targets` / a separate workload) instead of changing the text +model: `image_generation` (image model/service), `speech` (STT/TTS services), `embedding` +(a Bedrock embedding model). Detailed pricing/TCO → migration-to-aws (llm-to-bedrock skill). -Record the source model (`migration_from`). The feature override wins; only fall back to the -coarse source→family mapping (gpt4o → Claude Sonnet 4.6 family, gemini_flash → Nova Lite, etc.) -when there is no feature override. Detailed pricing/TCO → migration-to-aws (llm-to-bedrock skill). +The selectable model pool is drift-guarded against the sibling skill's lifecycle registry at +`skills/gcp-to-aws/references/shared/ai-model-lifecycle.md` (a Legacy/EOL model entering the +pool fails CI; the check lives in `test_scoring.py` and skips gracefully when the sibling +skill is not present). diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/models/anthropic-bedrock-2026-07-21.json b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/models/anthropic-bedrock-2026-07-21.json new file mode 100644 index 00000000..66c4ff6f --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/models/anthropic-bedrock-2026-07-21.json @@ -0,0 +1,154 @@ +{ + "schema_version": 2, + "provider": "anthropic", + "verified_at": "2026-07-21", + "verified_region": "us-east-1", + "source": "AWS Bedrock userguide Anthropic model cards (retrieved 2026-07-21) for Claude Sonnet 5; internal migration reference, Anthropic path (2026-06-24 live probes) for the path metadata and the Opus 4.8 / Haiku 4.5 entries", + "disclaimer": "Point-in-time capability data. Re-probe every selected model and API path in the target account and region before implementation. Claude Sonnet 5 (launched 2026-06-30, after the reference snapshot) replaces Sonnet 4.6 in the recommendation pool: per its AWS model card it supports Messages on bedrock-mantle as well as Converse/Invoke on bedrock-runtime, with 1M context / 128K output and Global+geo CRIS. Claude Fable 5 / Mythos 5 are deliberately NOT pool candidates: they require the account-level provider_data_share data-retention opt-in (prompts shared with the provider), which is a compliance decision, not a default recommendation.", + "paths": { + "mantle_messages": { + "sdk": "anthropic[bedrock]", + "client": "AnthropicBedrockMantle", + "iam_action": "bedrock-mantle:CreateInference", + "model_id_strategy": "clean_model_id", + "supports_cris": false, + "strengths": [ + "Preserves the Anthropic Messages API", + "Preferred for newest Anthropic beta features" + ], + "constraints": [ + "Bedrock Guardrails and runtime invocation logging are not available", + "Use a forced tool without strict for portable structured output", + "Availability must be probed in the target account and region" + ] + }, + "runtime_converse": { + "sdk": "boto3", + "client": "bedrock-runtime.Converse", + "iam_action": "bedrock:InvokeModel", + "model_id_strategy": "cris_inference_profile", + "supports_cris": true, + "strengths": [ + "Supports Bedrock governance, Guardrails, and invocation logging", + "Provides a multi-model request surface" + ], + "constraints": [ + "Newer Claude models generally require a Global or geography-scoped CRIS inference profile", + "Anthropic-specific request fields may require additionalModelRequestFields", + "Native structured-output mechanisms are model and region dependent; forced tool without strict is the portable default", + "Availability must be probed in the target account and region" + ] + }, + "runtime_invoke": { + "sdk": "boto3", + "client": "bedrock-runtime.InvokeModel", + "iam_action": "bedrock:InvokeModel", + "model_id_strategy": "cris_inference_profile", + "supports_cris": true, + "strengths": [ + "Preserves the provider-native Bedrock request body", + "Supports Bedrock runtime governance" + ], + "constraints": [ + "Uses the Bedrock Anthropic version field rather than the first-party HTTP header", + "Newer Claude models generally require a Global or geography-scoped CRIS inference profile", + "Native structured-output mechanisms are model and region dependent; forced tool without strict is the portable default", + "Availability must be probed in the target account and region" + ] + } + }, + "models": { + "claude_opus_4_8": { + "display_name": "Claude Opus 4.8", + "family": "claude_opus", + "version": "4.8", + "class": "premium", + "context_window": 1000000, + "output_token_ceiling": 128000, + "capabilities": [ + "tool_use", + "extended_thinking", + "vision", + "long_context" + ], + "paths": { + "mantle_messages": { + "available": true, + "model_id": "anthropic.claude-opus-4-8", + "requires_cris": false + }, + "runtime_converse": { + "available": true, + "model_id": "anthropic.claude-opus-4-8", + "requires_cris": true + }, + "runtime_invoke": { + "available": true, + "model_id": "anthropic.claude-opus-4-8", + "requires_cris": true + } + } + }, + "claude_sonnet_5": { + "display_name": "Claude Sonnet 5", + "family": "claude_sonnet", + "version": "5.0", + "class": "balanced", + "context_window": 1000000, + "output_token_ceiling": 128000, + "capabilities": [ + "tool_use", + "extended_thinking", + "vision", + "long_context" + ], + "evidence": "AWS model card model-card-anthropic-claude-sonnet-5 (2026-07-21): launch 2026-06-30, Active; Messages/Converse/Invoke checked; bedrock-mantle + bedrock-runtime checked; 1M context, 128K max output; adaptive thinking always on; image input checked; Geo (us/eu/au) and Global CRIS supported", + "paths": { + "mantle_messages": { + "available": true, + "model_id": "anthropic.claude-sonnet-5", + "requires_cris": false + }, + "runtime_converse": { + "available": true, + "model_id": "anthropic.claude-sonnet-5", + "requires_cris": true + }, + "runtime_invoke": { + "available": true, + "model_id": "anthropic.claude-sonnet-5", + "requires_cris": true + } + } + }, + "claude_haiku_4_5": { + "display_name": "Claude Haiku 4.5", + "family": "claude_haiku", + "version": "4.5", + "class": "fast", + "context_window": 200000, + "output_token_ceiling": 64000, + "capabilities": [ + "tool_use", + "vision" + ], + "paths": { + "mantle_messages": { + "available": true, + "model_id": "anthropic.claude-haiku-4-5", + "requires_cris": false + }, + "runtime_converse": { + "available": true, + "model_id": "anthropic.claude-haiku-4-5-20251001-v1:0", + "requires_cris": true + }, + "runtime_invoke": { + "available": true, + "model_id": "anthropic.claude-haiku-4-5-20251001-v1:0", + "requires_cris": true + } + } + } + } +} diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/models/openai-bedrock-2026-07-21.json b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/models/openai-bedrock-2026-07-21.json new file mode 100644 index 00000000..f86925a9 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/models/openai-bedrock-2026-07-21.json @@ -0,0 +1,257 @@ +{ + "schema_version": 2, + "provider": "openai", + "verified_at": "2026-07-21", + "verified_region": "us-east-2", + "source": "AWS Bedrock userguide OpenAI model cards + models-api-compatibility (retrieved 2026-07-21); internal migration reference, OpenAI path (feature-level probes, 2026-06-20)", + "disclaimer": "This is the OpenAI-to-Bedrock MIGRATION target catalog, not a list of OpenAI models: it holds the verified landing candidates for BOTH paths. Mantle (generation 'reasoning') carries the OpenAI GPT-5.x models, which per the AWS model cards are Responses-only and Mantle-only. runtime_converse (generation 'bedrock_native') carries the Claude candidates used when runtime governance (Guardrails/invocation logging) is required: the source OpenAI tier maps to the matching Claude tier — GPT-5.6 Sol -> Claude Opus 4.8, GPT-5.6 Terra / GPT-5.5 / GPT-5.4 -> Claude Sonnet 5, GPT-5.6 Luna -> Claude Haiku 4.5 — with capability-evidence fallback between tiers. Factual note: the gpt-oss open-weight models (openai.gpt-oss-120b-1:0 / -20b) DO support Converse/Invoke/Chat Completions on bedrock-runtime (AWS API-compatibility matrix 2026-07-21); they are not listed as recommendation candidates because they sit a capability class below the GPT-5.x/Claude frontier tier — add them with dated evidence if a family-continuity governance path is wanted. Only models with dated evidence are listed; entries name their evidence. Only the reference-probed entry (gpt-5.4) has feature-level Mantle behavior probes — verify feature behavior per model before relying on it. Numeric limits stay 'unknown' unless sourced. Re-probe every selected model and API path in the target account and region before implementation.", + "paths": { + "mantle_openai_responses": { + "sdk": "openai", + "client": "OpenAI(base_url=bedrock-mantle)", + "endpoint": "https://bedrock-mantle.{region}.api.aws/openai/v1", + "api": "responses", + "iam_auth": "bedrock_bearer_token_or_sigv4", + "model_id_strategy": "clean_openai_model_id", + "supports_cris": false, + "strengths": [ + "Preserves the OpenAI SDK and minimizes client rewrite", + "GPT-5.x on Bedrock Mantle is served through the Responses API" + ], + "constraints": [ + "GPT-5.x on Mantle rejects Chat Completions; a Chat Completions source must reshape to Responses (reference-verified on gpt-5.4; the gpt-5.6-sol model card also lists Chat Completions unsupported)", + "Geo/Global cross-region inference is not supported for OpenAI models per the AWS model cards (in-region only)", + "Availability must be probed in the target account and region" + ] + }, + "mantle_openai_chat": { + "sdk": "openai", + "client": "OpenAI(base_url=bedrock-mantle)", + "endpoint": "https://bedrock-mantle.{region}.api.aws/openai/v1", + "api": "chat_completions", + "iam_auth": "bedrock_bearer_token_or_sigv4", + "model_id_strategy": "clean_openai_model_id", + "supports_cris": false, + "strengths": [ + "OpenAI-compatible Chat Completions surface for models that accept it" + ], + "constraints": [ + "Not available for the GPT-5.x models listed here: rejected on gpt-5.4 (internal reference probe) and unsupported on the gpt-5.6-sol model card", + "Availability and model support must be probed in the target account and region" + ] + }, + "runtime_converse": { + "sdk": "boto3", + "client": "bedrock-runtime.Converse", + "endpoint": "https://bedrock-runtime.{region}.amazonaws.com", + "api": "converse", + "iam_action": "bedrock:InvokeModel", + "model_id_strategy": "cris_inference_profile", + "supports_cris": true, + "strengths": [ + "Bedrock-native multi-model request surface", + "Supports Bedrock Guardrails, invocation logging, and CloudWatch" + ], + "constraints": [ + "Uses a Bedrock-native model, not a GPT-5.x model (the GPT-5.x models on Bedrock are Mantle/Responses-only per their model cards; only the gpt-oss open-weight models support Converse and they are not frontier-tier); the OpenAI SDK integration must be rewritten to boto3 Converse", + "Bedrock-native models generally require a Global or geography-scoped CRIS inference profile", + "Availability must be probed in the target account and region" + ] + } + }, + "models": { + "openai_gpt_5_6_sol": { + "display_name": "OpenAI GPT-5.6 Sol (on Bedrock)", + "family": "openai_gpt_5", + "generation": "reasoning", + "version": "5.6", + "class": "reasoning", + "context_window": 272000, + "output_token_ceiling": "unknown", + "capabilities": [ + "reasoning", + "tool_or_function_calling", + "image_input_vision" + ], + "capability_source": "AWS model card 2026-07-21 (launch 2026-07-13, Active): frontier reasoning/agentic, image+text input checked; tool calling via the Responses API's built-in tool use (models-api-compatibility). Structured output/streaming behavior not feature-probed on Mantle — verify before relying.", + "paths": { + "mantle_openai_responses": { + "available": true, + "model_id": "openai.gpt-5.6-sol", + "requires_cris": false, + "evidence": "AWS model card model-card-openai-gpt-56-sol (2026-07-21): bedrock-mantle + Responses checked; Chat Completions/Invoke/Converse unchecked; Geo/Global inference not supported" + }, + "mantle_openai_chat": { + "available": false, + "model_id": "openai.gpt-5.6-sol", + "requires_cris": false, + "evidence": "AWS model card: Chat Completions unchecked for this model" + } + } + }, + "openai_gpt_5_5": { + "display_name": "OpenAI GPT-5.5 (on Bedrock)", + "family": "openai_gpt_5", + "generation": "reasoning", + "version": "5.5", + "class": "reasoning", + "context_window": 272000, + "output_token_ceiling": "unknown", + "capabilities": [ + "reasoning", + "tool_or_function_calling" + ], + "capability_source": "AWS model card 2026-07-21 (launch 2026-06-01, Active): advanced coding/agentic tasks; tool calling via the Responses API's built-in tool use. Feature-level Mantle behavior not probed — verify before relying.", + "paths": { + "mantle_openai_responses": { + "available": true, + "model_id": "openai.gpt-5.5", + "requires_cris": false, + "evidence": "AWS model card model-card-openai-gpt-55 (2026-07-21): bedrock-mantle Programmatic Access with Responses sample code; Geo/Global inference not supported" + } + } + }, + "openai_gpt_5_6_terra": { + "display_name": "OpenAI GPT-5.6 Terra (on Bedrock)", + "family": "openai_gpt_5", + "generation": "reasoning", + "version": "5.6", + "class": "reasoning", + "context_window": "unknown", + "output_token_ceiling": "unknown", + "capabilities": [ + "reasoning", + "tool_or_function_calling" + ], + "capability_source": "AWS model card 2026-07-21: balanced performance at half GPT-5.5 cost; tool calling via the Responses API's built-in tool use. Context window not captured from the card — treat as unknown.", + "paths": { + "mantle_openai_responses": { + "available": true, + "model_id": "openai.gpt-5.6-terra", + "requires_cris": false, + "evidence": "AWS model card model-card-openai-gpt-56-terra (2026-07-21): bedrock-mantle Programmatic Access with Responses sample code; Geo/Global inference not supported" + } + } + }, + "openai_gpt_5_6_luna": { + "display_name": "OpenAI GPT-5.6 Luna (on Bedrock)", + "family": "openai_gpt_5", + "generation": "reasoning", + "version": "5.6", + "class": "reasoning", + "context_window": "unknown", + "output_token_ceiling": "unknown", + "capabilities": [ + "reasoning", + "tool_or_function_calling" + ], + "capability_source": "AWS model card 2026-07-21: fastest/lowest-cost OpenAI tier for high-volume tasks; tool calling via the Responses API's built-in tool use. Context window not captured from the card — treat as unknown.", + "paths": { + "mantle_openai_responses": { + "available": true, + "model_id": "openai.gpt-5.6-luna", + "requires_cris": false, + "evidence": "AWS model card model-card-openai-gpt-56-luna (2026-07-21): bedrock-mantle Programmatic Access with Responses sample code; Geo/Global inference not supported" + } + } + }, + "openai_gpt_5_4": { + "display_name": "OpenAI GPT-5.4 (on Bedrock)", + "family": "openai_gpt_5", + "generation": "reasoning", + "version": "5.4", + "class": "reasoning", + "context_window": "unknown", + "output_token_ceiling": "unknown", + "capabilities": [ + "tool_or_function_calling", + "structured_output_json", + "reasoning", + "streaming" + ], + "capability_source": "internal reference smoke tests and its reasoning-API migration notebook (Responses path, 2026-06 probes) — the only entry with feature-level Mantle behavior probes (tools, structured output, sampling); limits not stated", + "paths": { + "mantle_openai_responses": { + "available": true, + "model_id": "openai.gpt-5.4", + "requires_cris": false, + "evidence": "internal reference smoke-test default Mantle model id; client.responses.create verified live" + }, + "mantle_openai_chat": { + "available": false, + "model_id": "openai.gpt-5.4", + "requires_cris": false, + "evidence": "internal reference verified gotcha: GPT-5.x on Mantle is Responses-only; Chat Completions rejected" + } + } + }, + "anthropic_claude_sonnet_5": { + "display_name": "Claude Sonnet 5 (Converse tier match for GPT-5.6 Terra / 5.5 / 5.4)", + "family": "anthropic_claude", + "generation": "bedrock_native", + "version": "5", + "class": "runtime_native", + "context_window": 1000000, + "output_token_ceiling": 128000, + "capabilities": [ + "tool_or_function_calling", + "reasoning", + "image_input_vision" + ], + "capability_source": "AWS model card model-card-anthropic-claude-sonnet-5 (2026-07-21): Converse checked, 1M context / 128K output, adaptive thinking always on, image input checked (tool_use/thinking/vision mapped to this provider's feature vocabulary). Balanced Claude tier — the Converse target for GPT-5.6 Terra, GPT-5.5, GPT-5.4, and unmatched/legacy sources.", + "paths": { + "runtime_converse": { + "available": true, + "model_id": "anthropic.claude-sonnet-5", + "requires_cris": true, + "evidence": "AWS model card (2026-07-21): Converse on bedrock-runtime checked; Geo (us/eu/au) and Global CRIS supported" + } + } + }, + "anthropic_claude_opus_4_8": { + "display_name": "Claude Opus 4.8 (Converse tier match for GPT-5.6 Sol)", + "family": "anthropic_claude", + "generation": "bedrock_native", + "version": "4.8", + "class": "runtime_native", + "context_window": 1000000, + "output_token_ceiling": 128000, + "capabilities": [ + "tool_or_function_calling", + "reasoning", + "image_input_vision" + ], + "capability_source": "anthropic-bedrock-2026-07-21 catalog (runtime_converse available; tool_use/extended_thinking/vision mapped to this provider's feature vocabulary). Frontier Claude tier — the Converse target for GPT-5.6 Sol.", + "paths": { + "runtime_converse": { + "available": true, + "model_id": "anthropic.claude-opus-4-8", + "requires_cris": true, + "evidence": "anthropic-bedrock-2026-07-21 catalog runtime_converse entry (requires CRIS profile)" + } + } + }, + "anthropic_claude_haiku_4_5": { + "display_name": "Claude Haiku 4.5 (Converse tier match for GPT-5.6 Luna)", + "family": "anthropic_claude", + "generation": "bedrock_native", + "version": "4.5", + "class": "runtime_native", + "context_window": 200000, + "output_token_ceiling": 64000, + "capabilities": [ + "tool_or_function_calling", + "image_input_vision" + ], + "capability_source": "anthropic-bedrock-2026-07-21 catalog (runtime_converse available; tool_use/vision mapped to this provider's feature vocabulary; no extended-thinking evidence, so reasoning-requiring workloads fall back to a higher Claude tier). Fast/low-cost Claude tier — the Converse target for GPT-5.6 Luna.", + "paths": { + "runtime_converse": { + "available": true, + "model_id": "anthropic.claude-haiku-4-5-20251001-v1:0", + "requires_cris": true, + "evidence": "anthropic-bedrock-2026-07-21 catalog runtime_converse entry (requires CRIS profile)" + } + } + } + } +} diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/output-templates/recommendation-doc.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/output-templates/recommendation-doc.md index fe97b26e..bf7edc6a 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/output-templates/recommendation-doc.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/output-templates/recommendation-doc.md @@ -46,7 +46,11 @@ ## 9. Bedrock model - + ## 10. Cost magnitude diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/clarify/clarify.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/clarify/clarify.md index a410a1d7..f666ba38 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/clarify/clarify.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/clarify/clarify.md @@ -15,7 +15,7 @@ _assemble: _produces: - answers.json - scoring-result.json -_advances_to: confirm +_advances_to: model-recommend _preconditions: - _check_phase_completed: discover _on_failure: _halt_and_inform @@ -30,6 +30,8 @@ _postconditions: _on_failure: _halt_and_inform - _assert: "answers.json has the nested shape {entry_point, answers:{...}} carrying the legacy mirror (top-level entry_point and answers = primary unit's fully-merged dims), AND system/primary_unit/units with every unit's dimensions fully resolved (inheritance applied) and every unit carrying its workload_class; the scope gate passed — at least one agent_session unit exists (a purely non-agent system halted with _halt_and_inform BEFORE primary selection); primary_unit is an agent_session unit; every collected key uses a legal value from clarify.md Step 3; scoring-result.json was written by scoring.py (not hand-scored) with one result per agent_session unit under units{} AND a top-level verdict mirrored from the primary; every unit carries a provenance map naming each dimension's source" _on_failure: _halt_and_inform + - _assert: "provenance is honest and reproducible: every value is one of seed|detected|asked|inherited|adapter|interview|assumed; when a seed was found at either lookup location (Step 2.5: $RUN_DIR/seed.json, else .agent-advisor/seed.json), every dimension it supplies is marked `seed` and carries the seed's value verbatim — byte-equal JSON, same shape, so an object-valued dimension such as `region` stays the same object and is never flattened, renamed, or split into sibling keys (a seeded dimension re-derived from prose or code, or rewritten into another shape, is a failure); no dimension is marked `asked` unless a human answered it in this session or run prose answered it; every `assumed` dimension has a matching entry in $RUN_DIR/UNANSWERED.md naming the question, the assumed value, and the reason" + _on_failure: _halt_and_inform --- # Phase: Clarify — Adaptive Questions @@ -155,6 +157,54 @@ single workload), treat it as `agent_session` and proceed. (Activities that classify as `agent_session`) is never falsely halted for having only a `temporal_worker_poll` seed.** +## Step 2.5 — Load the run seed, then resolve every dimension by precedence + +**Find the seed, in this order:** `$RUN_DIR/seed.json` (staged for this specific run), then +`.agent-advisor/seed.json` at the run root (the usual case — a repository that ships a seed cannot +know the run id in advance). The first one found wins; ignore the other. Schema: +`scripts/schemas/seed.json`. A seed supplies +machine-readable answers for the dimensions Step 3 would otherwise ask a human for. It exists so a +non-interactive run — a benchmark seed, a CI regression, an ATX transformation — feeds the +deterministic engine byte-identical input every time. Prose describes context; the seed supplies +enum values. + +**Validate it before trusting it.** An illegal enum value or unknown key is a hard error: say which +key is wrong and halt (`_halt_and_inform`). Never silently drop a malformed seed and fall through — +that would make a run look reproducible while quietly re-deriving values. + +**Resolve every dimension in Step 3's list by this precedence, highest first:** + +| Source | `provenance` value | When | +| ------------------------------------------------ | ------------------ | ------------------------------------------------------------ | +| `seed.json` | `seed` | the key is present in the seed | +| Discover / `context-signals.json` | `detected` | code evidence pins the value | +| `CLAUDE.md` / `AGENTS.md` prose at the repo root | `asked` | the prose answers it unambiguously and no human is available | +| The user, via AskUserQuestion (Step 3) | `asked` | a human is available | +| Temporal Tier-2 adapter table | `adapter` | the unit came from the adapter | +| Primary unit (delta questions) | `inherited` | the unit did not mention the dimension | +| No source at all | `assumed` | see below | + +**Copy a seeded value, do not re-express it.** A `seed` dimension is written into `answers.json` +byte-equal to the seed, keeping the seed's JSON shape. `region` is the one that invites rewriting: +the seed carries `"region": {"scope": ..., "regions": [...]}` and that whole object is what lands in +`system` and in each unit — never flattened to `"region": "single"` plus a sibling `"regions"`, and +never renamed. Reshaping a seeded value defeats the point of the seed: two runs then disagree on the +input even though both "used the seed". + +**`assumed` carries an obligation.** When a dimension has no source, you may pick the value a +careful reader of the repository would pick — but you MUST append the question, the value you +assumed, and the reason to `$RUN_DIR/UNANSWERED.md`, and mark that dimension `assumed` in +provenance. An `assumed` dimension with no `UNANSWERED.md` entry is a phase failure. Never invent a +value that contradicts the seed or the prose, and prefer the explicit `unknown` enum over a guess +when the dimension has one and the evidence is genuinely absent. + +**`asked` means a question was actually answered** — by a human in chat, or by prose written for +this run. It is never correct to mark a dimension `asked` in a run where nothing was asked and no +prose covered it; that is what `assumed` is for. + +Dimensions resolved here are settled: Step 3 asks ONLY about what is still missing, and skips +entirely when the seed (plus detection and prose) covers everything. + ## Step 3 — Ask the core questions (AskUserQuestion, batched) **First batch (ask these up front — they set the tone for the whole recommendation):** @@ -214,9 +264,9 @@ Write `$RUN_DIR/answers.json` as: { "entry_point": "", "answers": {}, - "system": {, "provenance": {"": "detected|asked|inherited|adapter|interview"}}, + "system": {, "provenance": {"": "seed|detected|asked|inherited|adapter|interview|assumed"}}, "primary_unit": "", - "units": { "": {"workload_class": "", , "provenance": {"": "detected|asked|inherited|adapter|interview"}} } + "units": { "": {"workload_class": "", , "provenance": {"": "seed|detected|asked|inherited|adapter|interview|assumed"}} } } ``` @@ -239,11 +289,21 @@ COMPLETE (inheritance already applied — a reader never chases the primary to r **Provenance (additive):** dimension values stay flat; each unit's entry AND the `system` block carry a sibling `provenance` map naming where each dimension's value came from — -`detected` (Discover/context-signals), `asked` (user answered in chat), `inherited` -(unmentioned in a delta question, inherited from the primary unit), `adapter` (seeded from -the Temporal Tier-2 adapter table), `interview` (no-code interview answer). Consumers that -ignore `provenance` keep working unchanged. Single-unit collapse: provenance is all -`detected`/`asked` — a harmless additive key. +`seed` (from `seed.json`, per Step 2.5), `detected` (Discover/context-signals), `asked` (a +human answered in chat, or run prose answered it), `inherited` (unmentioned in a delta +question, inherited from the primary unit), `adapter` (seeded from the Temporal Tier-2 adapter +table), `interview` (no-code interview answer), `assumed` (no source — REQUIRES a matching +entry in `$RUN_DIR/UNANSWERED.md`). Consumers that ignore `provenance` keep working unchanged. + +Provenance is the run's honesty record, not decoration: a `seed` value is reproducible, an +`assumed` value is not, and the difference is what makes a repeated run's score comparable. + +A system-level dimension mirrored down into a unit (the "inheritance already applied" rule above) +keeps the provenance it had in `system`: a `region` the seed supplied under `system` reads `seed` in +the unit too, because the seed is still where the value came from. `inherited` means something +narrower — the value came from the _primary unit_ because this unit's delta answers did not mention +the dimension. Note in `provenance_notes` which system dimensions were mirrored, so a reader can +tell the two apart without diffing against the seed. ## Step 5 — Run the scoring engine @@ -286,9 +346,9 @@ from answers.json — no dependency on context-signals.json, which may not exist Non-agent units are NOT scored — Design resolves them from `references/decision-refs/workload-classes.md`. -## Step 6 — Write state and continue to Confirm +## Step 6 — Write state and continue to Model Recommend -Set `phases.clarify` = completed (leave `phases.confirm` = pending). Do NOT jump to Design. -The state machine now routes to **Confirm** (`references/phases/confirm/confirm.md`), which -confirms the deployment model / services / co_recommend pick and writes `confirm.json` — Design and -the diagram require it. +Set `phases.clarify` = completed (leave `phases["model-recommend"]` and `phases.confirm` = +pending). Do NOT jump to Confirm or Design. The state machine now routes to **Model Recommend** +(`references/phases/model-recommend/model-recommend.md`), which creates the per-workload +Bedrock model/path contract before Confirm asks the user to accept runtime and model together. diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/confirm/confirm-assemble.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/confirm/confirm-assemble.md index b73961f1..c5c34eee 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/confirm/confirm-assemble.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/confirm/confirm-assemble.md @@ -2,7 +2,7 @@ _assemble: assemble-confirm _of_phase: confirm _reads: - - winner-specific follow-up answers (collected inline in confirm.md) + - winner-specific runtime and model/path confirmations (collected inline in confirm.md) _produces: - confirm.json --- @@ -16,4 +16,6 @@ _produces: > the phase: it is the single creator of `confirm.json`, and its postconditions > (declared on the phase) are the phase's completion gate. See `confirm.md` > § Step 5 for the confirm.json shape (`deployment_model`, `agentcore_services`, -> `chosen_runtime` when co_recommend, `tool_choices`). +> `chosen_runtime` when co_recommend, `tool_choices`, and accepted `model_decision`). +> `model_decision.accepted` records strategy acceptance; `verification_status` +> independently records whether the exact model/path was invocable in the target account. diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/confirm/confirm.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/confirm/confirm.md index 5ab1f5f9..317c3368 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/confirm/confirm.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/confirm/confirm.md @@ -1,28 +1,33 @@ --- _phase: confirm _title: "Confirm — Winner-specific follow-ups" -_requires_phase: clarify +_requires_phase: model-recommend _input: - scoring-result.json - answers.json + - model-recommendation.json _assemble: _file: phases/confirm/confirm-assemble.md _produces: - confirm.json _advances_to: design _preconditions: - - _check_phase_completed: clarify + - _check_phase_completed: model-recommend _on_failure: _halt_and_inform - _check_file_exists: scoring-result.json _on_failure: _unrecoverable - _validate_json: scoring-result.json _on_failure: _unrecoverable + - _check_file_exists: model-recommendation.json + _on_failure: _unrecoverable + - _validate_json: model-recommendation.json + _on_failure: _unrecoverable _postconditions: - _check_file_exists: confirm.json _on_failure: _halt_and_inform - _validate_json: confirm.json _on_failure: _halt_and_inform - - _assert: "confirm.json has one units[] entry per agent_session unit (each with its own confirmed deployment_model, agentcore_services, tool_choices, and chosen_runtime when that unit's verdict was co_recommend), plus top-level fields mirroring the primary unit; a single-unit run may use the flat top-level shape alone; confirm.json carries resolved_runtimes{: } for every unit (the per-unit rule/Tier-1 picks used by the platform gate); confirm.json records platform_decision {mode, platform} — asked only when the resolved runtimes diverge, silent split otherwise; when the gate was asked, platform_decision.offer records the superset and per-unit sacrifices; every temporal_worker_poll unit's Tier-1 pick (including any user AskUserQuestion choice for Tier-1 rules 1/4) is persisted in resolved_runtimes so Design consumes it verbatim and never re-asks" + - _assert: "confirm.json has one units[] entry per agent_session unit (each with its own confirmed deployment_model, agentcore_services, tool_choices, model_decision, and chosen_runtime when that unit's verdict was co_recommend), plus top-level fields mirroring the primary unit; every model-bearing unit's model_decision copies primary_model, api_path, invocation_model_id, and the independent verification_status from model-recommendation.json plus model-verification.json when present unless the user requested changed requirements and the deterministic engine was rerun; accepted means the recommendation strategy was accepted and never implies verification_status == passed; a single-unit run may use the flat top-level shape alone; confirm.json carries resolved_runtimes{: } for every unit (the per-unit rule/Tier-1 picks used by the platform gate); confirm.json records platform_decision {mode, platform} — asked only when the resolved runtimes diverge, silent split otherwise; when the gate was asked, platform_decision.offer records the superset and per-unit sacrifices; every temporal_worker_poll unit's Tier-1 pick (including any user AskUserQuestion choice for Tier-1 rules 1/4) is persisted in resolved_runtimes so Design consumes it verbatim and never re-asks" _on_failure: _halt_and_inform --- @@ -32,7 +37,9 @@ Runs after scoring, before Design. Only asks what the winning runtime needs. ## Step 1 — Read the scoring result -Read `$RUN_DIR/scoring-result.json`. +Read `$RUN_DIR/scoring-result.json` and `$RUN_DIR/model-recommendation.json`. If +`$RUN_DIR/model-verification.json` exists, read it too; it is the live account-invocability +result, not a replacement recommendation. **Per-unit confirm (multi-unit systems).** `scoring-result.json.units` carries one scored result per `agent_session` unit — each has its OWN `verdict`, `deployment_model`, and @@ -90,6 +97,10 @@ co_recommend tie in Step 4 — so no runtime pick skips its services/tool_choice user to pick one FOR THIS UNIT. Record the pick as this unit's `chosen_runtime` (Step 5). Then run Step 2/3 for the pick. Each co_recommend unit is broken independently — two agent units can land on different runtimes. + **Non-interactive runs:** when `$RUN_DIR/seed.json` supplies `chosen_runtime` (clarify.md + Step 2.5), take the tie-break from it verbatim and say so instead of asking. With no seed and + nobody to ask, break the tie toward the highest-scoring runtime, and record the tie, the pick, + and the reason in `$RUN_DIR/UNANSWERED.md` — a silently broken tie hides a real decision. - no_viable_runtime: show `blocking_constraints`; ask which constraint can relax; if one changes, rewrite `$RUN_DIR/answers.json` with the changed value and re-run scoring by executing **clarify.md Step 5's exact bash block** (the PYTHONPATH multi-unit loop that reads @@ -106,6 +117,25 @@ co_recommend tie in Step 4 — so no runtime pick skips its services/tool_choice ## Step 5 — Write confirm.json and state +Before writing, confirm each model-bearing unit's model and API path alongside its runtime. +Show that unit's path-specific model ID, invocation ID or CRIS TODO, compatibility, +architecture impacts, `[BLOCKS]`, `[TUNE]`, evaluation/rollout gates, and probe status. If any +entry is still `decision_required`, return to Model Recommend and resolve it; do not confirm a +null model/path. The user may: + +- accept the recommendation, including the work needed to clear listed blocks; or +- change a requirement. In that case update `model-recommendation-input.json`, rerun + `model_recommendation.py`, and present the new result. + +Do not directly edit `primary_model`, `api_path`, or `invocation_model_id`, and do not let +runtime scoring or a downstream migration engine replace them. Accepting the strategy and +proving target-account access are independent. Record each accepted result as +`model_decision: {"primary_model": "...", "api_path": "...", "invocation_model_id": "... | +null", "accepted": true, "verification_status": "not_run | passed | failed | +needs_resolution"}` in the matching unit. Use `model-verification.json` when present; +otherwise use the recommendation's `verification.probe_status`. Model-less units omit +`model_decision`. + Write `$RUN_DIR/confirm.json` with a `units` object — one entry per `agent_session` unit confirmed in Steps 2–4, keyed by unit id — plus top-level fields mirroring the PRIMARY unit (backward compatibility; single-unit runs are exactly today's flat shape). @@ -129,7 +159,14 @@ mirror the primary unit's entry. "support-chat": { "deployment_model": "framework_on_runtime", "agentcore_services": ["identity", "memory"], - "tool_choices": { "memory": "native" } + "tool_choices": { "memory": "native" }, + "model_decision": { + "primary_model": "anthropic.claude-sonnet-4-6", + "api_path": "runtime_converse", + "invocation_model_id": "global.anthropic.claude-sonnet-4-6", + "verification_status": "passed", + "accepted": true + } }, "triage-agent": { "deployment_model": "harness", diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/design/design-assemble.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/design/design-assemble.md index a26e8dc1..6a341486 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/design/design-assemble.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/design/design-assemble.md @@ -22,4 +22,12 @@ _produces: > (verdict, chosen_runtime, deployment_model, agentcore_services, > model_recommendation, scores, eliminated, the gate notes, handoff_required). -The assembler produces ONE `units[]` entry per inventory unit: agent units derive their verdict, deployment_model, agentcore_services, and model_recommendation from their scoring result plus confirm overrides; non-agent units derive their verdict from workload-classes rules (with rationale citing the rule id like "W2: batch → AWS Batch"). It then assembles the `platform` block from confirm.json's platform_decision and the units' coupling. Finally, it writes the primary unit's fields at design.json's top level (legacy mirror). +The assembler produces ONE `units[]` entry per inventory unit: agent units derive their verdict, +deployment_model, and agentcore_services from scoring plus confirm, while model_recommendation +comes only from the matching model-recommendation.json entry plus the accepted model_decision; +the complete model contract includes parity/compatibility, architecture impacts, +evaluation/rollout gates, and the separate live verification result when present; +non-agent units derive their verdict from workload-classes rules (with rationale citing the rule +id like "W2: batch → AWS Batch"). It then assembles the `platform` block from confirm.json's +platform_decision and the units' coupling. Finally, it writes the primary unit's fields at +design.json's top level (legacy mirror). diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/design/design.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/design/design.md index 070b023d..7c476217 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/design/design.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/design/design.md @@ -5,6 +5,7 @@ _requires_phase: confirm _input: - scoring-result.json - confirm.json + - model-recommendation.json _knowledge: - { file: references/decision-refs/workload-classes.md, _when: "any unit has workload_class != agent_session" } - { file: references/decision-refs/temporal.md, _when: "any unit has workload_class == temporal_worker_poll or the temporal context is detected" } @@ -16,16 +17,16 @@ _advances_to: estimate _preconditions: - _check_phase_completed: confirm _on_failure: _halt_and_inform - - _check_file_exists: [scoring-result.json, confirm.json] + - _check_file_exists: [scoring-result.json, confirm.json, model-recommendation.json] _on_failure: _unrecoverable - - _validate_json: [scoring-result.json, confirm.json] + - _validate_json: [scoring-result.json, confirm.json, model-recommendation.json] _on_failure: _unrecoverable _postconditions: - _check_file_exists: design.json _on_failure: _halt_and_inform - _validate_json: design.json _on_failure: _halt_and_inform - - _assert: "design.json has one units[] entry per inventory unit, a platform block consistent with confirm.platform_decision, and top-level legacy fields mirroring the primary unit; design.json has top-level verdict, chosen_runtime, deployment_model, agentcore_services, model_recommendation, and carries scores + eliminated (and blocking_constraints when present) copied verbatim from scoring-result.json; handoff_required is true iff ANY unit's effective_runtime needs a compute handoff — one of ecs, eks, fargate, or batch (not just the primary/winning runtime; AgentCore/Lambda/Lambda MicroVMs are self-contained); when temporal units exist, design.json has a temporal block recording the Way, per-queue Tier 1 rule ids, and Serverless Workers labeled PRE-RELEASE regardless of any docs label; Workflow orchestration code is never rewritten; every unit carries a key_change line derived from its runtime's service card; every non-agent unit's verdict equals the runtime its workload-classes rule maps to (W1→eks/ecs; W2→batch; W3/W4→lambda; W5/W6→fargate) — verdict and workload_class are never contradictory; every unit carries an effective_runtime equal to platform.runtime when platform.mode is consolidated, else its own resolved runtime (a co_recommend unit resolves to its confirm chosen_runtime) — effective_runtime is always a concrete runtime enum, never the literal co_recommend" + - _assert: "design.json has one units[] entry per inventory unit, a platform block consistent with confirm.platform_decision, and top-level legacy fields mirroring the primary unit; design.json has top-level verdict, chosen_runtime, deployment_model, agentcore_services, model_recommendation, and carries scores + eliminated (and blocking_constraints when present) copied verbatim from scoring-result.json; every model-bearing unit's model_recommendation is derived from the matching model-recommendation.json workload entry and accepted confirm.model_decision, including model_identity, model, api_path, invocation_model_id, source, source_analysis, feature_assessment, compatibility, architecture_impacts, additional_targets (separate-modality target contracts, carried verbatim when present), blocks, tuning, migration_deltas, evaluation, rollout, provisional verification, and live_verification when model-verification.json exists; no model was independently selected from scoring-result.json; handoff_required is true iff ANY unit's effective_runtime needs a compute handoff — one of ecs, eks, fargate, or batch (not just the primary/winning runtime; AgentCore/Lambda/Lambda MicroVMs are self-contained); when temporal units exist, design.json has a temporal block recording the Way, per-queue Tier 1 rule ids, and Serverless Workers labeled PRE-RELEASE regardless of any docs label; Workflow orchestration code is never rewritten; every unit carries a key_change line derived from its runtime's service card; every non-agent unit's verdict equals the runtime its workload-classes rule maps to (W1→eks/ecs; W2→batch; W3/W4→lambda; W5/W6→fargate) — verdict and workload_class are never contradictory; every unit carries an effective_runtime equal to platform.runtime when platform.mode is consolidated, else its own resolved runtime (a co_recommend unit resolves to its confirm chosen_runtime) — effective_runtime is always a concrete runtime enum, never the literal co_recommend" _on_failure: _halt_and_inform --- @@ -35,12 +36,17 @@ Assembles the recommendation from the scoring result + Confirm choices + service ## Step 1 — Read inputs -Read `$RUN_DIR/scoring-result.json` and `$RUN_DIR/confirm.json`. The winning runtime is +Read `$RUN_DIR/scoring-result.json`, `$RUN_DIR/model-recommendation.json`, and +`$RUN_DIR/confirm.json`. The winning runtime is `confirm.chosen_runtime` if present (co_recommend pick), else `scoring-result.verdict`. Prefer `confirm.deployment_model` and `confirm.agentcore_services` over the scoring-result defaults (Confirm is the user-confirmed set). (Clarify's scope gate guarantees at least one agent_session unit, so a scored top-level verdict always exists — there is no zero-agent branch.) +If `$RUN_DIR/model-verification.json` exists, read it and attach each workload's matching +record as `live_verification`. Do not infer `passed` from `confirm.model_decision.accepted`; +acceptance and target-account invocability are separate. + ## Step 2 — Load the winning runtime's service card Load ALL THREE files (each is required; do not skip any — Step 4's lock-in check depends on @@ -165,11 +171,25 @@ observed this run. Assemble per unit: -- `agent_session` units: verdict/deployment_model/services/model from that unit's - scoring result + that unit's confirm overrides — read `confirm.json.units[]` +- `agent_session` units: verdict/deployment_model/services from that unit's scoring result + + that unit's confirm overrides; model/path from + `model-recommendation.json.workloads[]` + the accepted + `confirm.json.units[].model_decision` — read `confirm.json.units[]` (deployment_model, agentcore_services, chosen_runtime, tool_choices) for THIS unit, not a global top-level value. For a single-unit run, fall back to confirm.json's top-level fields. Each agent unit's confirmed runtime/services are independent — never copy the primary unit's. + Write the compatibility `model_recommendation` object as + `{"model": primary_model, "model_identity": ..., "reasoning": rationale joined for display, + "api_path": api_path, "invocation_model_id": ..., "source": ..., "source_analysis": ..., + "feature_assessment": ..., "alternatives": ..., "compatibility": ..., + "architecture_impacts": ..., "additional_targets": ... (copy verbatim when the recommendation + has them; a separate-modality target with status unresolved must NOT be turned into a runnable + model id), "blocks": ..., "tuning": ..., "migration_deltas": ..., + "evaluation": ..., "rollout": ..., "verification": ..., "live_verification": ... | null}`. + This preserves current report consumers while carrying the complete advisor contract. + `verification` is the catalog/probe requirement from the recommendation; + `live_verification` is only populated from the separate probe artifact. Never read the + scoring result's deprecated coarse model mirror for this field. **Every non-agent unit's runtime comes from `confirm.json.resolved_runtimes[]` VERBATIM** — Confirm already resolved and (for Temporal rules 1/4) user-confirmed each one, so Design must NOT re-run temporal.md Tier 1 or workload-classes.md and risk a different pick than the one behind diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/discover/discover.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/discover/discover.md index 16e03135..6eaed02b 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/discover/discover.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/discover/discover.md @@ -31,7 +31,17 @@ In the provided path, look for: - **Framework** (imports / requirements.txt / package.json): `strands`, `langgraph` / `langchain`, `crewai` / `autogen`, `openai` (Agents SDK), else `custom` / `none`. -- **Model provider**: openai / anthropic / google-genai / bedrock mentions. +- **Model call inventory**: for every model-bearing unit, record exact provider, raw model-ID + strings, SDK/package, API surface, and source paths. Provider vocabulary: + `anthropic | openai | azure_openai | google_genai | bedrock | none | unknown`. API surfaces + include Anthropic `messages`, OpenAI `chat_completions` / `responses`, and Bedrock + `converse` / `invoke_model`. Do not collapse exact IDs such as + `claude-3-7-sonnet-latest` into a coarse `claude` family. +- **Anthropic migration-sensitive features**: `budget_tokens`; `temperature` / `top_p` / + `top_k`; assistant-prefill messages; refusal handling; explicit or framework-default + `max_tokens`; structured output; prompt caching; server tools; Files API / URL sources; + Message Batches; fallbacks; conversation-state IDs; Skills/MCP connector/managed-agent + dependencies. Record observed feature codes and source paths for Model Recommend. - **Session/timeout hints**: timeout configs, long-running loops, queue/HITL patterns. - **Multi-tenant hints**: per-user/tenant scoping, separate contexts. - **Compute hints**: GPU instance types, heavy compute (compilation, ML inference). @@ -109,6 +119,14 @@ Write `$RUN_DIR/context-signals.json` mapping detected signals onto scoring keys "trigger": "request", "source": "detected", "framework": "langgraph", + "model_source": { + "provider": "anthropic", + "model_ids": ["claude-3-7-sonnet-latest"], + "sdk": "anthropic", + "api_surface": "messages", + "source_paths": ["src/agent.py"], + "detected_features": ["budget_tokens", "assistant_prefill"] + }, "coupling": { "interacts_with": ["summarizer"], "mode": "queue" @@ -132,9 +150,9 @@ Write `$RUN_DIR/context-signals.json` mapping detected signals onto scoring keys Only include keys you can detect with reasonable confidence. Everything else stays for Clarify. -`model_provider` (openai | anthropic | google-genai | bedrock | none) is not a scoring key — -it records which AI provider the code calls, and gates the migration-plan offer in Generate -Step 6. Include it whenever provider detection succeeded. +Keep the top-level `model_provider` compatibility field, but derive it from the primary unit's +`model_source.provider`. `model_provider` is not a scoring key. The structured per-unit +`model_source` object is authoritative for Model Recommend and the downstream handoff. ## Step 3 — Tell the user what was detected diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/generate/generate-report.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/generate/generate-report.md index 97a17175..4c2653e1 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/generate/generate-report.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/generate/generate-report.md @@ -84,7 +84,9 @@ exactly: - **§3 Workload recommendations** (one `.unit-sec` per unit: **scored form** = score bars titled "Runtime comparison for this agent" + eliminated note + "Why X" bullets + item table [model/services/runner-up/key change]; **rule-based form** = decision table with rule-cite - - considered-and-rejected) + - considered-and-rejected; each unit with a non-null `model_recommendation` additionally + gets the **"Why this model" card** — rationale visible, full model/migration detail in a + collapsed `
`, all values from `unit.model_recommendation`) - **§4 Target architecture** (figure w/ per-unit entry points from `trigger` + figcap; **4.1 component detail** table [Entry point from trigger / Compute / Model access / Supporting services]; **4.2 Security & networking** table [from the runtime cards' Serving & security @@ -138,6 +140,19 @@ that element entirely (use `display:none` or omit the HTML block). .elim-label { font-size: 11px; color: #ef4444; font-style: italic; margin-top: -6px; margin-bottom: 6px; } + /* "Why this model" card (per model-bearing unit; detail collapsed by default) */ + .model-why { border: 1px solid var(--rule); margin-top: 14px; } + .model-why-head { padding: 9px 14px; background: var(--soft); border-bottom: 1px solid var(--rule); + font-size: 12px; font-weight: 600; text-transform: uppercase; + letter-spacing: 0.4px; color: var(--muted); } + .model-why-body { padding: 11px 14px 12px; font-size: 13.5px; } + .model-why-body > p { margin: 0 0 8px; } + .model-why details > summary { cursor: pointer; font-size: 12.5px; font-weight: 600; color: #1a56db; } + .model-why details p.em { margin: 12px 0 4px; font-weight: 600; } + .model-why .mark { font-size: 11px; font-weight: 600; text-transform: uppercase; + letter-spacing: 0.4px; color: var(--muted); white-space: nowrap; } + .model-why .mark-block { color: #b42318; } + /* Alternatives grid */ .alt-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; margin-top: 12px; } @@ -296,7 +311,10 @@ that element entirely (use `display:none` or omit the HTML block). - + + + + {{ IF unit.agentcore_services }} {{ END IF }} @@ -317,6 +335,53 @@ that element entirely (use `display:none` or omit the HTML block).
ItemRecommendationNotes
Bedrock model{{ unit.model_recommendation.model }}{{ unit.model_recommendation.reasoning }}
Bedrock model{{ unit.model_recommendation.model_identity.display_name }}{{ unit.model_recommendation.reasoning }}
API path{{ unit.model_recommendation.api_path }}Path ID: {{ unit.model_recommendation.model }}
Account verification{{ unit.model_recommendation.live_verification.status OR "not_run" }}{{ IF unit.model_recommendation.live_verification.status === "passed" }}Invocation ID verified{{ ELSE }}Runnable access not yet established{{ END IF }}
Migration gates{{ COMMA_JOIN(CODES(unit.model_recommendation.blocks)) OR "None" }}{{ unit.model_recommendation.evaluation.mode }} evaluation; {{ unit.model_recommendation.rollout.strategy }} rollout
AgentCore services{{ COMMA_JOIN(unit.agentcore_services) }}{{ service notes }}

Key change: {{ unit.key_change }}

{{ END IF }} + {{ IF unit.model_recommendation }} + +
+
Why this model
+
+

{{ mr.reasoning }}

+
+ Full model & migration detail +

Identities: logical {{ mr.model_identity.model_key }} · path ID {{ mr.model_identity.path_model_id }} · invocation {{ mr.invocation_model_id OR "unresolved" }} · catalog verified {{ mr.verification.catalog_verified_at }} · limits: {{ mr.model_identity.context_window }} — probe in target account.
+ Compatibility: native {{ COMMA_JOIN(mr.compatibility.native, each wrapped in ) OR "—" }} · portable {{ COMMA_JOIN(mr.compatibility.portable, each wrapped in ) OR "—" }} · rearchitecture {{ COMMA_JOIN(mr.compatibility.rearchitecture, each wrapped in ) OR "—" }}

+ {{ IF NOT EMPTY(mr.migration_deltas) }} +

Migration changes

+ + {{ FOR EACH delta IN mr.migration_deltas }} + + {{ END FOR }} +
{{ delta.category }}{{ delta.description }}
+ {{ END IF }} + {{ IF NOT EMPTY(mr.blocks) OR NOT EMPTY(mr.tuning) }} +

Findings

+ + {{ FOR EACH b IN mr.blocks }} + + {{ END FOR }} + {{ FOR EACH t IN mr.tuning }} + + {{ END FOR }} +
blocks{{ b.message }} Fix: {{ b.remediation }}
tune{{ t.message }} {{ t.remediation }}
+ {{ END IF }} + {{ IF NOT EMPTY(mr.additional_targets) }} +

Separate-modality targets

+ + {{ FOR EACH t IN mr.additional_targets }} + + {{ END FOR }} +
{{ t.capability }}{{ t.status }}{{ t.service }}
+ {{ END IF }} +

Evaluation: {{ mr.evaluation.mode }} — {{ mr.evaluation.gates.length }} gates, e.g. “{{ ONE representative gate quoted VERBATIM from mr.evaluation.gates }}” · + Rollout: {{ mr.rollout.strategy }} ({{ mr.rollout.gate }}) · Verification: {{ mr.verification.probe_status }}/{{ mr.verification.availability_claim }} — probe the exact (model, path) before implementation.

+
+
+
+ {{ END IF }} {{ END FOR }} diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/generate/generate.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/generate/generate.md index 8829f1a7..d4b3f30c 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/generate/generate.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/generate/generate.md @@ -27,7 +27,9 @@ _preconditions: _postconditions: - _check_file_exists: [diagram.md, recommendation.md, mini-brief.md, recommendation-report.html] _on_failure: _halt_and_inform - - _assert: "recommendation.md fills all 12 sections (business summary first, technical detail after) with the freshness footer; mini-brief.md carries the recommendation, top-3 signals, eliminated, model, and any io_wait/fedramp/region/cris notes set in design.json (a non-agent unit in a mixed system may show 'no model' when its model_recommendation is null); recommendation-report.html was generated (Step 5 is not optional); when design.json has >1 unit, recommendation.md contains the System topology section and the report one unit card per unit" + - _assert: "recommendation.md fills all 12 sections (business summary first, technical detail after) with the freshness footer; mini-brief.md carries the recommendation, top-3 signals, eliminated, model, and any io_wait/fedramp/region/cris notes set in design.json (a non-agent unit in a mixed system may show 'no model' when its model_recommendation is null); recommendation-report.html was generated (Step 5 is not optional); when design.json has >1 unit, recommendation.md contains the System topology section and the report one unit card per unit; every unit card whose design.json unit has a non-null model_recommendation contains the 'Why this model' card (rationale paragraph + collapsed 'Full model & migration detail' details block) rendered from unit.model_recommendation values, not invented" + _on_failure: _halt_and_inform + - _assert: "recommendation-report.html follows the v3 section spec from generate-report.md Step R1 — its numbered top-level sections are, in order: Summary, Assessment inputs, Recommendation(s), Target architecture, then (Temporal migration only when temporal units exist), Cost summary, Next steps, Generated artifacts, with the dynamic numbering R1 defines; an html whose top-level section structure was invented instead of taken from the template (a known failure mode after a mid-phase context compaction) fails this phase — re-read generate-report.md and regenerate" _on_failure: _halt_and_inform --- @@ -84,10 +86,12 @@ decision-refs/temporal.md ("If the user asks") — do not add it to the plan. **Skip this step entirely for `migrate`** (execution artifacts belong to the downstream plugins). For Build paths: -- AgentCore + Harness → write a minimal `harness.json` skeleton with the model id from - model_recommendation and the selected services. +- AgentCore + Harness → write a minimal `harness.json` skeleton with the selected services. + Include the exact `invocation_model_id` only when `live_verification.status == "passed"`; + otherwise use `TODO: verify model id` and state that account access is unverified. - AgentCore + Framework / other runtimes → write a minimal framework starter note (entrypoint - contract: `/invocations` POST + `/ping` GET for AgentCore) + the model id. + contract: `/invocations` POST + `/ping` GET for AgentCore) + the same verified invocation ID + or explicit TODO placeholder. Write scaffolding under `$RUN_DIR/scaffold/`. Keep it minimal — heavy IaC hands off. ## Step 4.5 — Write the mini-brief to `$RUN_DIR/mini-brief.md` (delivered by the Step 5.5 sidebar) @@ -97,6 +101,9 @@ TO `$RUN_DIR/mini-brief.md` (a file, not just chat text; Step 5.5 re-reads it): - Recommendation (runtime + deployment model), Why (top 3 signals), Eliminated, Model, and a pointer to `$RUN_DIR/recommendation.md`. +- For each model-bearing unit, include API path, path-specific model ID, CRIS/invocation status, + `[BLOCKS]`, evaluation mode, and live verification status. Never describe model access as + runnable unless `live_verification.status == "passed"`. - Any `warnings` from the scoring result (e.g. 5 TPS). - If `design.json` has `io_wait_tco_note == true`: the I/O-wait TCO point (AgentCore bills $0 during model/human waits — a cost edge for spiky/HITL traffic; no dollar figures). @@ -214,5 +221,14 @@ Set `phases.generate` = completed (read-merge-write). Then branch: brief; close with a short completion message pointing at `recommendation.md` — the advisor flow is complete. +**Non-interactive runs (seeded).** When `$RUN_DIR/seed.json` exists (clarify.md Step 2.5), its +`gates` block answers these gates instead of AskUserQuestion: `gates.migration_plan` and +`gates.poc`, each `accept` or `decline`. **A gate the seed does not mention is DECLINED** — +never accept a gate on the user's behalf, because both gates spend real time and money. Persist +the resolved status exactly as the interactive path does (`phases.poc = "in_progress"` before +poc.md loads on accept, `"skipped"` on decline) so a resumed run does not re-ask. Record in +`$RUN_DIR/UNANSWERED.md` that the gate was resolved from the seed default rather than by a human +whenever the seed was silent. + Note: for `migrate` WITH a completed migration plan, Gate 2 is asked at the end of migration-plan.md (its Step 6), using the same wording as point 2 above. diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/intake/intake.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/intake/intake.md index aeb1f89d..1b8f8a38 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/intake/intake.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/intake/intake.md @@ -99,5 +99,5 @@ if a path is given; if the user declines a path, note it and set `discover = ski Write `$RUN_DIR/.phase-status.json` with `entry_point`, `audience` (from Q2), `intake` = completed. Set `discover` = pending if entry point is build_deploy/migrate AND the user -offered a code path, else `skipped`. Set all later phases (`clarify`, `confirm`, `design`, -`estimate`, `generate`) to pending. +offered a code path, else `skipped`. Set all later phases (`clarify`, `model-recommend`, +`confirm`, `design`, `estimate`, `generate`) to pending. diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/migration-plan/migration-plan.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/migration-plan/migration-plan.md index 595bbcda..5b96bfad 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/migration-plan/migration-plan.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/migration-plan/migration-plan.md @@ -23,7 +23,7 @@ _postconditions: _on_failure: _halt_and_inform - _assert: "the inline engine's Generate phase reached HANDOFF_OK (aws-design-ai.json exists with a non-empty ai_architecture in the recorded migration_dir), and migration_plan_ctx {repo, migration_dir} was recorded in .phase-status.json" _on_failure: _halt_and_inform - - _assert: "model reconciliation (Step 3.5) is settled — for every unit whose plan-chosen Bedrock model differs from its design.json model, design.json.units[].model_recommendation.model now equals the plan's model, the unit carries model_refined_by_plan: true, and recommendation.md AND recommendation-report.html name that unit's new model at every occurrence (table row, Mermaid node, ASCII overview, prose, and all-units roll-up/summary sentences) with no stale model left as that unit's chosen model; when no unit's model differs, this is a no-op" + - _assert: "model contract validation (Step 3.5) is settled — every model-bearing unit's plan output matches the advisor-confirmed model and api_path, or the phase halted with a mismatch that must be resolved by changing Model Recommend inputs and rerunning its deterministic engine; migration-plan never rewrote design.json model decisions" _on_failure: _halt_and_inform --- @@ -37,6 +37,38 @@ boundary. Everything runs inside the current agent-advisor session, so Steps 5 gcp-to-aws files are **read-only**: this phase never edits them. +## Step -1 — Engine presence check (ATX capability gate) + +This phase is the ONLY place in this skill that reads a **sibling** skill, so it is the only +place that can fail when the skill is deployed on its own. In the full plugin the engine is +always installed. In a standalone bundle — the ATX Custom transformation built by +`tools/atx-bundle/build.ts`, which ships this skill and nothing else — it is absent. + +Before resolving any path below, check whether `$GCP_BASE/references/phases/design/design.md` +exists (`$GCP_BASE` is defined in the next section). **If it does not:** + +1. Read-merge-write `$RUN_DIR/.phase-status.json`: set `phases.migration_plan = + "not_applicable"` AND `migration_plan_unavailable = "engine_absent"`, then advance + `current_phase` through `_advances_to` (`poc`) in the SAME state write + (`INTERPRETER.md` § Skill bindings — resolved statuses). The `migration_plan_unavailable` + marker is what distinguishes "this deployment cannot produce a plan" from "the user declined + one" — Gate 2 and poc.md both key on it. +2. Tell the user plainly, without blaming them: the full migration plan is produced by the + `gcp-to-aws` engine, which this deployment does not bundle. Everything through the + recommendation (runtime verdict, deployment model, service set, model + API path, cost + magnitude, and the generated documents) is unaffected — only the migration plan stage is + unavailable here. If they need the plan, point them at the full `migration-to-aws` plugin. +3. **Still offer Gate 2** — a missing plan does not remove the POC. Ask it exactly as Step 6 + below does (same wording, same `phases.poc = "in_progress"` persistence before poc.md loads), + but say the POC will be **design-backed** (built from `design.json`, labelled "not + plan-backed") rather than plan-backed. In a non-interactive run take the answer from + `$RUN_DIR/seed.json`'s `gates.poc` (clarify.md Step 2.5); a seed that omits it declines. +4. **Stop executing this phase.** Do not read any gcp-to-aws file, do not create + `migration-plan-injection.json`, and do not run any step below. The `_postconditions` above + are satisfied by the `not_applicable` resolution exactly as they are for an idea-only migrate. + +Only continue past this point when the engine is present. + ## Path definitions (resolve first, before any other step) ``` @@ -191,7 +223,9 @@ Write the injection context to `$RUN_DIR/migration-plan-injection.json`: "raw_verdict": "", "deployment_model": "", "endpoint_contract": "", - "model": "", + "model": "", + "api_path": "", + "source": "", "services": "", "evidence": "" } @@ -298,72 +332,27 @@ are not modified and gcp never sees this step: **Additive-only rule:** These annotations are purely ADDITIVE — never modify or remove any gcp-written fields. gcp's own validation checklists must keep passing. -### Step 3.5 — Reconcile the model choice (plan wins, but the recommendation must not lie) - -The advisor injected each unit's `model_recommendation` (Step 2) as the gcp engine's model -constraint, but the engine's AI-design phase may **refine** it — the plan sees the _source_ -model per unit (e.g. `gpt-4o-mini` vs `gpt-4o`) and the per-workload task, so it may pick a -different, better-fitting Bedrock model than the advisor's family-level baseline (e.g. a -`gpt-4o-mini` binary-moderation unit → Nova Lite, not the balanced-baseline Sonnet). **The -plan's per-unit model wins** — it is finer-grained and region-validated. But the advisor's -own `design.json` and `recommendation.md`/`recommendation-report.html` were written _before_ -the plan ran, so a stale model there would contradict the POC (which reads the plan-backed -model in `poc.md` Step 2). Reconcile so the deliverables agree: - -For each design_block matched to a unit (Step 3) **whose `model_recommendation` is non-null**, -compare the block's chosen target Bedrock model (`target_bedrock_model`, or the block's first -`bedrock_models[].aws_model_id`) against that unit's injected `model` (from -`design.json.units[].model_recommendation.model`). **Skip model-less units** — a non-agent -unit (batch/service/light_io) can have `model_recommendation: null`; it has no model to reconcile, -so leave it null and do NOT dereference `.model`: - -1. **If they name the same model** — nothing to do. -2. **If they differ** — the plan wins. You MUST do BOTH of the following: - - **(a) Update `design.json`** for that unit: - - Set `units[].model_recommendation.model` to the plan's model id. - - Append to `units[].model_recommendation.reasoning`: - `" — refined by the migration plan (was ): "`. - - Set `units[].model_refined_by_plan` to `true` (a top-level key ON THE UNIT, a - sibling of `model_recommendation` — NOT inside it) so the change is auditable. This key - is mandatory whenever you changed the model; a reconciled unit with no - `model_refined_by_plan: true` is a bug. - - **(b) Back-write the recommendation in BOTH `recommendation.md` AND - `recommendation-report.html`.** The stale model id can appear in MORE THAN ONE place per - file — you must replace EVERY occurrence for that unit, not just the first. Grep each file - for the old model id/key and check at least these locations before you finish: - - the per-unit verdict/summary table row (e.g. `| content-review | … | | …`); - - the **Mermaid architecture diagram** node labels (e.g. - `content_review_node["AWS Lambda
"]` in the `mermaid` block); - - any **ASCII/plain-text overview or unit list** (e.g. `[ content-review: … () ]`); - - any prose sentence naming that unit's model; - - **roll-up / summary statements that collapse ALL units into one model** — these are the - easily-missed ones. The executive summary, a section header, or a lede often say - something like "all three agents migrate from OpenAI to Claude Sonnet 4.6." Once one - unit is refined to a different model, that blanket claim is FALSE and must be corrected - (e.g. "support-chat and insights-writer → Claude Sonnet 4.6; content-review → Nova Lite"). - Check the exec summary, every section header, and any opening/closing sentence. - - in the HTML, ALL of the above surfaces (table cell, SVG/Mermaid label, inline text, - AND the lede/summary sentences). - Replace the stale model everywhere it names or implies THIS unit, then add a one-line note - in that unit's section: - `"Model refined by the migration plan: ()."` - After editing, re-grep both files for the OLD model id/name scoped to this unit — it must - return zero hits AS THIS UNIT'S MODEL. Pay special attention to blanket phrases like - "across all three", "all agents", "every unit" sitting next to the old model name: if such - a phrase still implies this unit uses the old model, it is a miss. The old id may still - legitimately appear elsewhere (another unit's model, or a "backup: Haiku 4.5" note), but - never as THIS unit's chosen model and never inside an all-units roll-up. - Keep every other section untouched; this is a targeted find-and-replace on this unit's - model only, NOT a re-render of the report. -3. Never invent a model here — only carry across the id the plan already chose and - region-validated. +### Step 3.5 — Validate the advisor model/path contract (advisor wins) + +For every matched, model-bearing unit, compare the plan's model and migration path with +`design.json.units[].model_recommendation.{model,api_path}`. + +1. If both match, annotate the design block with + `"advisor_model_contract": "validated"` and continue. +2. If either differs, do not rewrite `design.json`, the recommendation, or the report. + Record the proposed value and rationale in the design block as + `plan_model_mismatch`, then STOP with `_halt_and_inform`. Tell the user which requirement or + account/region probe caused the mismatch. Resolution MUST return to Model Recommend, update + `model-recommendation-input.json`, rerun `model_recommendation.py`, and reconfirm the new + result. A migration engine is a consumer of the advisor contract, not a second selector. +3. A live probe may invalidate availability, but it still does not authorize silent + substitution. Probe failure blocks runnable implementation claims. Resolve credentials, + IAM, profile ID, region, or model access first; return to Model Recommend only when the + accepted requirement, model, path, or profile must change. **Single-unit runs:** SKIP the unit-correlation overlay above (steps 1–5) — the collapse -invariant means zero annotation behavior change when there's only one unit. Step 3.5 model -reconciliation still applies: reconcile the one unit's model (the same stale-recommendation -risk exists), but there is no per-unit ambiguity. +invariant means zero annotation behavior change when there's only one unit. Step 3.5 contract +validation still applies to that one unit. On `HANDOFF_OK`: `aws-design-ai.json` (and/or other design artifacts) present, with unit annotations when multi-unit. diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/model-recommend/model-recommend-assemble.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/model-recommend/model-recommend-assemble.md new file mode 100644 index 00000000..cd2a1a8f --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/model-recommend/model-recommend-assemble.md @@ -0,0 +1,22 @@ +--- +_assemble: assemble-model-recommend +_of_phase: model-recommend +_reads: + - answers.json + - scoring-result.json + - context-signals.json when present +_produces: + - model-recommendation-input.json + - model-recommendation.json +_knowledge: + - { file: references/models/anthropic-bedrock-2026-07-21.json } +--- + +# Model Recommend — Assemble the recommendation contract + +The phase writes the normalized per-workload v2 input, then +`scripts/model_recommendation.py` creates `model-recommendation.json`. The script output is the +joint model/path contract; downstream phases consume it by workload id and do not rerank it. +`decision_required` entries must be resolved and rerun before completion. When the user approves +a live probe, `scripts/verify_model_path.py` separately writes `model-verification.json`; +recommendation acceptance never implies account invocability. diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/model-recommend/model-recommend.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/model-recommend/model-recommend.md new file mode 100644 index 00000000..f1cbfdef --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/model-recommend/model-recommend.md @@ -0,0 +1,258 @@ +--- +_phase: model-recommend +_title: "Model Recommend — Bedrock model and API path" +_requires_phase: clarify +_input: + - answers.json + - scoring-result.json + - context-signals.json +_knowledge: + - { file: references/models/anthropic-bedrock-2026-07-21.json } + - { file: references/models/openai-bedrock-2026-07-21.json } +_assemble: + _file: phases/model-recommend/model-recommend-assemble.md +_produces: + - model-recommendation-input.json + - model-recommendation.json +_advances_to: confirm +_preconditions: + - _check_phase_completed: clarify + _on_failure: _halt_and_inform + - _check_file_exists: [answers.json, scoring-result.json] + _on_failure: _unrecoverable + - _validate_json: [answers.json, scoring-result.json] + _on_failure: _unrecoverable +_postconditions: + - _check_file_exists: [model-recommendation-input.json, model-recommendation.json] + _on_failure: _halt_and_inform + - _validate_json: [model-recommendation-input.json, model-recommendation.json] + _on_failure: _halt_and_inform + - _assert: "model-recommendation.json was written by model_recommendation.py; it has one workloads[unit_id] entry for every agent_session unit and every other model-bearing unit; every entry has decision_status == recommended before this phase completes; each entry carries exact structured source hints, source_analysis, feature_assessment, model_identity, primary_model, api_path, invocation_model_id, compatibility, architecture_impacts, additional_targets when the workload has separate-modality capabilities (embeddings/images/audio, each unresolved with a named service and null candidate), blocks, tuning, migration_deltas, evaluation, rollout, and provisional verification with region plus catalog_verified_at; runtime-scoring output did not independently replace any selected model" + _on_failure: _halt_and_inform + - _assert: "every feature_status entry marked `detected` in model-recommendation-input.json has a named source location behind it (Step 3's evidence rule); a surface that was scanned and not found is `absent`, a surface whose source path could not be read is `unknown`, and no feature was marked `detected` because the migration implies it" + _on_failure: _halt_and_inform +--- + +# Phase: Model Recommend — Bedrock model and API path + +This phase is the model-selection authority. Runtime scoring remains in Clarify, but it MUST +NOT decide the final model or Bedrock API path. + +## Step 1 — Build the per-workload source inventory + +Read `$RUN_DIR/answers.json` and, when present, `$RUN_DIR/context-signals.json`. Create one +input row for every `agent_session` unit and every other unit with an LLM call. Do not create a +row for a model-less compute unit. + +For each row, preserve machine-readable source facts: + +- `provider`: `anthropic | openai | azure_openai | google_genai | bedrock | none | unknown` +- `model_ids[]`: exact strings found in code or stated by the user; do not normalize them +- `sdk`: exact SDK/package name when known +- `api_surface`: e.g. `messages`, `chat_completions`, `responses`, `converse` +- `source_paths[]`: the files containing the provider call sites — a file belongs here only if it + invokes the SDK/API. Tool definitions, capability manifests, and config that merely _evidences_ a + feature are not call sites; cite those where the feature is recorded instead. It must cover the + whole scan: if Step 3 marks a feature `detected` on the strength of a call in some file, that file + is a call site and belongs here — a feature cited from a path the list omits is an inconsistent + scan. Sort the list, so two scans of the same repository produce the same array. + +Discovery evidence is a hint, not an immutable fact. The downstream executor MUST re-scan code +and may override a stale hint with a logged mismatch; it MUST NOT silently replace the +advisor-confirmed target model/path. + +## Step 2 — Resolve model/path requirements + +Map existing answers into each row's `requirements`: + +- `model_priority` → `priority` +- `model_features` plus agent tool use → `critical_features[]` +- target region → top-level `region`; use `unknown` when no concrete region was given + +For Anthropic sources, ask one batched clarification only for requirements not established by +code or prior answers: + +- preserve the first-party Messages API? +- require newest Anthropic beta features? +- require Bedrock Guardrails, invocation logging, or CloudWatch integration? +- require a shared multi-model Converse surface? +- require a provider-native Bedrock request body? +- minimum context window and expected output-token ceiling? +- use thinking? +- allow Global CRIS, or require a geography-scoped CRIS profile? + +For OpenAI and Azure OpenAI sources, ask instead for the provider-neutral requirements (never +reuse the Anthropic `preserve_messages_api` switch): + +- `api_continuity`: `required | preferred | not_required | unknown` — must the OpenAI SDK and + API surface be preserved? +- require Bedrock Guardrails, invocation logging, or a shared multi-model Converse surface? +- does the code use `n` (multiple candidates), hosted state (Assistants/Threads, + `previous_response_id`), hosted web/file search, Files API, or vector stores? +- minimum context window and expected output-token ceiling? +- allow Global CRIS or require a geography-scoped profile (runtime Converse only)? + +OpenAI is handled by a dedicated provider module (`openai-bedrock-2026-07-21.json` catalog): +GPT-5.x on Mantle is Responses-only, so a Chat Completions source is reshaped, not routed to +`mantle_openai_chat`. Azure OpenAI remains an explicit `provider_module_pending` generic result. + +Do not ask users to choose an API path by name unless they already expressed a preference. +The deterministic engine ranks `(model, api_path)` candidates together after filtering hard +constraints. If an explicit preference exists, record `preferred_api_path`. Record +`min_context_tokens`, `expected_output_tokens`, `thinking_enabled`, `data_residency`, +`cris_geography`, and an explicit `inference_profile_id` when known. Do not manufacture a +Global or geography prefix when residency is unresolved. + +## Step 3 — Record migration-sensitive features + +For Anthropic source paths, scan for migration-sensitive features: + +- `budget_tokens`, `sampling_parameters`, `assistant_prefill` +- `refusal_handling`, `tokenizer_rebaseline`, `max_tokens_headroom` +- `structured_output`, `prompt_caching` +- `citations`, `streaming`, `tool_use`, `vision` +- `server_tools`, `files_api`, `url_sources`, `message_batches` +- `models_api`, `fallbacks`, `conversation_state`, `agent_infra` + +For OpenAI source paths, scan instead for OpenAI feature codes: + +- `tool_or_function_calling`, `structured_output_json`, `streaming`, `image_input_vision` +- `reasoning`, `sampling_params`, `max_tokens`, `multiple_candidates_n` +- `web_search`, `file_search_retrieval`, `files_api`, `vector_stores`, `assistants_threads` +- `audio_modality`, `embeddings`, `images`, `conversation_state` + +Put observed features in `detected_features[]`. Also write `feature_status` entries as +`detected | absent | unknown`. For a Claude version hop, inspect every source path and mark each +version-breaking surface explicitly; absence from `detected_features[]` alone means `unknown`, +not proved absent. + +**Each status must be earned by evidence, and the evidence is a source location.** + +- `detected` — you can name the file and line where the feature is used (a parameter, a call, a + request field, a declared capability). Cite that location in the phase's chat output for every + feature you mark `detected`. A feature you believe _ought_ to be there, or that the migration + will imply anyway, is not `detected`: `tokenizer_rebaseline`, for instance, is a consequence of + the version hop and belongs in `migration_deltas`, not in the scan, unless the source actually + counts tokens. +- `absent` — you read every source path and the surface is not used. This is the correct status + for a feature you cannot cite; do not round a plausible guess up to `detected`. +- `unknown` — a source path could not be read, so the surface was never scanned. Say which path. + +The scan feeds `blocks`, `tuning`, and `migration_deltas` counts, so a false `detected` inflates +the findings and two runs over the same repository then disagree. If the evidence is not citable, +the status is `absent`. An incomplete version scan produces `version_scan_incomplete` and the phase +must not present the migration as ready. For OpenAI, the module distinguishes the platform axis +(OpenAI hosting → Bedrock) from the model-generation axis (GPT-4.x/o-series → GPT-5.x reasoning); +opaque deployment names stay `unknown` and are never inferred to a family. + +## Step 4 — Write and validate model-recommendation-input.json + +Write `$RUN_DIR/model-recommendation-input.json` with this shape: + +```json +{ + "schema_version": 2, + "region": "us-east-1", + "primary_unit": "support-agent", + "workloads": [ + { + "workload_id": "support-agent", + "source": { + "provider": "anthropic", + "model_ids": ["claude-3-7-sonnet-latest"], + "sdk": "anthropic", + "api_surface": "messages", + "source_paths": ["src/agent.py"] + }, + "requirements": { + "priority": "balanced", + "critical_features": ["tool_use"], + "preserve_messages_api": true, + "governance": [], + "min_context_tokens": 200000, + "expected_output_tokens": 16000, + "thinking_enabled": true, + "data_residency": "geo_required", + "cris_geography": "us" + }, + "detected_features": ["budget_tokens", "assistant_prefill"], + "feature_status": { + "budget_tokens": "detected", + "assistant_prefill": "detected", + "sampling_parameters": "absent", + "refusal_handling": "unknown" + } + } + ] +} +``` + +Validate it against `scripts/schemas/model-recommendation-input.json`. On validation failure, +fix the input; do not bypass the schema. + +## Step 5 — Run the deterministic recommendation engine + +Resolve `$SCRIPTS` using the same plugin-root fallback as Clarify, then run: + +```bash +uv run python "$SCRIPTS/model_recommendation.py" \ + "$RUN_DIR/model-recommendation-input.json" \ + --output "$RUN_DIR/model-recommendation.json" +``` + +It prints `RESULT=ok WORKLOADS= SCHEMA_VALIDATED=`. `SCHEMA_VALIDATED=no` means the host +has no `jsonschema` (a bare `python3` invocation without `uv`): the recommendation is still +deterministic, but nothing machine-checked the input or the output, so read both against +`scripts/schemas/` yourself before continuing. + +The engine output is authoritative for the joint model/path ranking. Do not hand-edit its +recommendation. Keep these identities separate: + +- `model_identity.model_key`: logical catalog identity +- `model_identity.path_model_id` / `primary_model`: the ID form for the selected path +- `invocation_model_id`: the resolved account-callable ID; runtime may leave this null until a + CRIS profile is resolved + +Every capability claim remains `probe_status: not_run` and +`availability_claim: provisional` until the exact `(invocation_model_id, api_path)` pair is +invoked in the target account and region. + +OpenAI sources use the dedicated OpenAI provider module (`provider_module: openai`) and its +dated catalog. Azure OpenAI and Google sources remain the generic provisional branch and carry +`provider_module_pending` in `[BLOCKS]`; do not present those as provider-complete. All modules +share one artifact shape, so downstream phases consume every provider identically. + +## Step 6 — Resolve decision_required before advancing + +If any workload has `decision_status: decision_required`, present its `decision_options` and +tradeoffs. Ask the user to resolve the conflicting requirement, update +`model-recommendation-input.json`, and rerun the deterministic engine. Do not pick an option, +edit the output, or mark this phase completed while any workload remains unresolved. + +## Step 7 — Optionally probe the selected path + +Offer a live target-account probe after all workloads are `recommended`. This is optional and +separate from recommendation acceptance because it uses AWS credentials and invokes a billable +model. Run it only with explicit approval: + +```bash +uv run --with boto3 --with anthropic python "$SCRIPTS/verify_model_path.py" \ + "$RUN_DIR/model-recommendation.json" \ + --output "$RUN_DIR/model-verification.json" +``` + +The verifier must call only the recorded `invocation_model_id`; it never substitutes another +model. Runtime recommendations with an unresolved CRIS profile produce `needs_resolution`. +Keep `model-verification.json` when generated and show its per-workload status. A failed probe +does not change the accepted recommendation; it blocks runnable POC claims until resolved. + +## Step 8 — Present the recommendation and advance + +For each workload, show the primary model, API path, resolved invocation ID or CRIS TODO, +compatibility groups, architecture impacts, any `additional_targets` (separate-modality targets +such as embeddings/images/audio, shown as unresolved with their named service — never as a +runnable model ID), path rationale, `[BLOCKS]`, `[TUNE]`, evaluation mode, rollout gate, and +verification status. Do not resolve blockers in chat by silently changing the output. Confirm +handles user acceptance or requested requirement changes. + +Set `phases["model-recommend"]` = completed and leave `phases.confirm` = pending. diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/poc/poc.md b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/poc/poc.md index c0eedd15..0439c164 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/poc/poc.md +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/references/phases/poc/poc.md @@ -22,7 +22,7 @@ _preconditions: _postconditions: - _check_file_exists: plan.md _on_failure: _halt_and_inform - - _assert: "a deployment plan (plan.md) and a runtime-appropriate POC under poc/ were written (Step 3 dispatch on each unit's effective_runtime — the consolidated superset when platform.mode is consolidated, else the unit's resolved runtime — NOT the raw split verdict), with the resolved Bedrock model id (or a TODO: verify placeholder — never a fabricated id) and deploy.sh guardrails; Mode B additionally left a created-resources ledger + cleanup.sh" + - _assert: "a deployment plan (plan.md) and a runtime-appropriate POC under poc/ were written (Step 3 dispatch on each unit's effective_runtime — the consolidated superset when platform.mode is consolidated, else the unit's resolved runtime — NOT the raw split verdict), with the exact invocation_model_id only when that workload's model-verification status is passed (otherwise a TODO: verify placeholder — never a fabricated or merely catalog-derived id) and deploy.sh guardrails; Mode B additionally left a created-resources ledger + cleanup.sh" _on_failure: _halt_and_inform --- @@ -47,9 +47,14 @@ the generated deploy steps in the user's account under the Step 4 safety contrac including non-agent `batch`/`fargate`/`serverless_workers` units (their shapes are in `poc-shapes.md`, and a model-less non-agent unit omits Bedrock wiring); the gate keys on the primary agent unit, but the POC generation covers all units. -- For `migrate`: POC runs ONLY if `phases.migration_plan == "completed"` (the POC implements - the produced plan). Without a plan, migrate has nothing to implement — the flow completes - after Generate/handoff. +- For `migrate`: POC runs when `phases.migration_plan == "completed"` (the POC implements the + produced plan), OR when the plan stage resolved `not_applicable` with + `migration_plan_unavailable == "engine_absent"` in `.phase-status.json` — a standalone + deployment that does not bundle the migration engine (migration-plan.md Step -1). In that + second case the POC is **design-backed**: build from `design.json` alone and label it + "not plan-backed" exactly as Step 1 already prescribes for a skipped Stage 2. Otherwise — + the user declined the plan, or it is missing for any other reason — migrate has nothing to + implement and the flow completes after Generate/handoff. - This phase is only loaded after the user confirmed **Gate 2** (asked in generate.md Step 7 or migration-plan.md Step 6). @@ -74,12 +79,15 @@ Step 4 runs ONLY for Mode B; Steps 5–6 run for both. Read `$RUN_DIR/design.json` (verdict, deployment_model, agentcore_services, model_recommendation) and `$RUN_DIR/confirm.json` (confirmed deployment model + services). The recommendation doc + diagram already exist from Generate; POC builds on those decisions. +If `$RUN_DIR/model-verification.json` exists, read it as the only live account-invocability +evidence. **Plan-backed POC (Stage 2 ran):** if `.phase-status.json` has `migration_plan_ctx` and `phases.migration_plan == "completed"`, ALSO read `/aws-design-ai.json` — via the recorded path ONLY, never -by re-globbing `.migration/` (a glob can pick up a stale run). When present, it wins over -`design.json` where they overlap (Steps 2, 2.5 and 3 each state where). If Stage 2 did not +by re-globbing `.migration/` (a glob can pick up a stale run). When present, it supplies +implementation detail but MUST NOT replace the advisor-confirmed model/path in `design.json`. +If Stage 2 did not run (skipped / not applicable / failed), proceed from `design.json` alone — the pre-existing behavior — and label the POC "not plan-backed" in plan.md. @@ -112,27 +120,39 @@ trigger), never a Bedrock-calling agent. This applies to a model-less SECONDARY system (the primary is always an agent unit per Clarify's scope gate). Only dereference `model_recommendation.model` when it is non-null. -**`design.json` is the single source of truth for the model, and it is already reconciled.** -When a migration plan ran, its Phase C Step 3.5 (`migration-plan.md`) already reconciled every -unit's `model_recommendation` in `design.json` with the plan's per-unit choice (plan wins) and -back-wrote `recommendation.md`/`recommendation-report.html` to match. So by the time the POC -runs, `design.json.units[].model_recommendation.model` is authoritative for every unit — -plan-backed or not. Resolve each unit's model from ITS OWN entry; never take one unit's model -for another. - -For each unit, `design.json.units[].model_recommendation.model` carries an internal model -key (e.g. `claude_sonnet_4_6`, `nova_lite`). The POC needs the **real Bedrock -model/inference-profile id** for THAT unit. Verify it via the awsknowledge MCP per -`references/decision-refs/freshness.md` (same anti-fabrication rule). If the MCP is not called, -write the id as a clearly-marked `TODO: verify model id` placeholder in the generated files and -say so in the README — never fabricate an id as if verified. +**`design.json` is the single source of truth for model and API path.** Its per-unit +`model_recommendation` is the accepted output of Model Recommend. Migration Plan validates that +contract but never replaces it. Resolve each unit's model and `api_path` from ITS OWN entry; +never take one unit's decision from another or from a run-wide plan scalar. + +For each unit, `design.json.units[].model_recommendation.model` carries the selected +path-specific model ID (for example, `anthropic.claude-sonnet-4-6`). +`model_recommendation.invocation_model_id` is the exact account-callable ID only after CRIS +resolution. These identities must remain separate. + +Use an invocation ID in runnable code only when that unit's `model-verification.json` record +has `status: passed` and exactly matches the recommendation's `api_path` and +`invocation_model_id`. A catalog date, an awsknowledge lookup, user acceptance, or a non-null +CRIS-shaped string is not proof of account access. If the probe was not run, failed, or needs +resolution, write a clearly marked `TODO: verify model id` placeholder in generated files and +state that the POC has not established runnable model access. + +Separate-modality entries under `additional_targets` (embeddings, images, audio) with +`status: unresolved` have no chosen model id. Never invent one: emit a `TODO: select and verify + model/service` placeholder naming the recommendation's `service`, and keep the +capability out of the runnable text-model path. + +Mode A does not access AWS credentials, so it never runs a new probe. In Mode B, if live +verification is missing, ask before the billable model call and run +`scripts/verify_model_path.py` against the recommendation. Never substitute another model when +the probe fails. To change a model/path/profile, update the Model Recommend input, rerun the +engine, and reconfirm. **Plan-backed cross-check (optional):** when `aws-design-ai.json` is loaded you may confirm a unit's id against its OWN matching design block — match the block whose `source_paths[]` overlap that unit's `evidence` (NOT `design_blocks[0]`, which is only the first unit's block). -The block's `target_bedrock_model` should already equal the reconciled `model_recommendation` -(Step 3.5 made them agree); if they somehow differ, `design.json` wins because reconciliation -is authoritative. The MCP check then only confirms the id is still current. +The block's `target_bedrock_model` and path should equal `model_recommendation` because Step 3.5 +validated them. If they differ, stop and return to Model Recommend; do not choose one silently. **Strip environment annotations from the model id.** The running assistant's model name may carry a context-window annotation like `[1m]` (e.g. `us.anthropic.claude-sonnet-4-6[1m]`). @@ -438,16 +458,20 @@ calls the recommended Bedrock model and answers the AgentCore entrypoint contrac - Uses `boto3` bedrock-runtime. Region from an env var (`AWS_REGION`), no hardcoded creds. - Include a one-line system prompt and echo the model's reply — enough to prove the loop works. -**Code-path branch (plan-backed only):** if `aws-design-ai.json` has -`ai_architecture.code_migration.migration_path == "mantle"`, do NOT use the boto3 template — -generate the agent with an OpenAI-compatible client pointed at the Bedrock Mantle endpoint -instead (same `/invocations` + `/ping` contract; base URL and model id from the plan; mark -the endpoint URL `TODO: verify` if not MCP-checked this run). For `"converse"`, absent, or -not plan-backed: use the boto3 template below. - -Use this template verbatim, substituting `` with the Step 2 value **for this unit** -(or the `TODO: verify model id` placeholder if the MCP was not called). In a multi-unit POC, -each unit's agent.py uses THAT unit's model id from `design.json.units[].model_recommendation`: +**Code-path branch (per unit):** read +`design.json.units[].model_recommendation.api_path`. For `mantle_messages`, generate an +`AnthropicBedrockMantle` Messages client. For `runtime_converse`, use the boto3 Converse +template. For `runtime_invoke`, use boto3 InvokeModel with the provider-native body. Future +`mantle_openai_chat` and `mantle_openai_responses` paths use their matching OpenAI-compatible +clients. Never branch on one run-wide `migration_path` string. + +The template below applies only to `runtime_converse`. Use it verbatim for that path, +substituting `` with the Step 2 invocation ID **for this unit** (or the +`TODO: verify model id` placeholder if the MCP was not called). For the other API paths, keep +the same `/invocations` and `/ping` HTTP wrapper but replace the imports, client construction, +and `run_prompt` implementation with the selected path's client. Do not paste a Converse call +into a Mantle or InvokeModel POC. In a multi-unit POC, each unit's agent.py uses THAT unit's +model decision from `design.json.units[].model_recommendation`: ```python """Minimal AgentCore POC agent — proves the Bedrock loop works end to end. diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/anthropic_model_recommendation.py b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/anthropic_model_recommendation.py new file mode 100644 index 00000000..862bb85d --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/anthropic_model_recommendation.py @@ -0,0 +1,772 @@ +"""Deterministic Anthropic-to-Bedrock model/path recommendation for agent-advisor. + +Provider module called by model_recommendation.py (the orchestrator): joint +(model, api_path) filtering/ranking over the dated Anthropic catalog, Claude +version-hop analysis, CRIS resolution without guessing geography, and the shared +downstream recommendation contract. Offline: no SDK imports at module load. +""" + +import re + + +SUPPORTED_PATHS = { + "mantle_messages", + "mantle_openai_chat", + "mantle_openai_responses", + "runtime_converse", + "runtime_invoke", +} +ANTHROPIC_PATHS = ( + "mantle_messages", + "runtime_converse", + "runtime_invoke", +) + +_PRIORITY_ORDER = { + "quality": ["claude_opus_4_8", "claude_sonnet_5", "claude_haiku_4_5"], + "balanced": ["claude_sonnet_5", "claude_opus_4_8", "claude_haiku_4_5"], + "speed": ["claude_haiku_4_5", "claude_sonnet_5", "claude_opus_4_8"], + "cost": ["claude_haiku_4_5", "claude_sonnet_5", "claude_opus_4_8"], + "unknown": ["claude_sonnet_5", "claude_opus_4_8", "claude_haiku_4_5"], +} + +_FEATURE_ORDER = { + "agentic": [ + "claude_sonnet_5", + "claude_opus_4_8", + "claude_haiku_4_5", + ], +} + +_CAPABILITY_ALIASES = {"multimodal": "vision"} +_MODEL_CAPABILITIES = { + "extended_thinking", + "long_context", + "tool_use", + "vision", +} +_VERSION_SCAN_FEATURES = { + "assistant_prefill", + "budget_tokens", + "max_tokens_headroom", + "refusal_handling", + "sampling_parameters", + "tokenizer_rebaseline", +} +_REARCHITECTURE_FEATURES = { + "agent_infra", + "conversation_state", + "fallbacks", + "files_api", + "message_batches", + "server_tools", + "url_sources", +} + +_BLOCK_FINDINGS = { + "budget_tokens": ( + "budget_tokens_removed", + "`budget_tokens` is rejected by current Claude targets.", + "Replace it with adaptive thinking and an explicit effort setting.", + ), + "sampling_parameters": ( + "sampling_parameters_removed", + "Legacy temperature/top_p/top_k controls are rejected by current Claude targets.", + "Remove them and calibrate behavior with the golden set.", + ), + "assistant_prefill": ( + "assistant_prefill_removed", + "Assistant-prefill structured output is rejected by current Claude targets.", + "Use a forced tool without strict and validate the resulting schema.", + ), + "server_tools": ( + "server_tools_not_portable", + "First-party server tools are not provided by the selected Bedrock path.", + "Implement an application-owned tool loop or select an external service.", + ), + "files_api": ( + "files_api_not_portable", + "The first-party Files API is not available on Bedrock.", + "Inline supported content within the 20 MB request cap or add object retrieval.", + ), + "url_sources": ( + "url_sources_not_portable", + "First-party URL image and document sources are not available on Bedrock.", + "Fetch, validate, and inline the content from the application.", + ), + "message_batches": ( + "message_batches_not_portable", + "Anthropic Message Batches do not port directly to Bedrock.", + "Use CreateModelInvocationJob and redesign job submission and result handling.", + ), + "models_api": ( + "models_api_not_portable", + "The first-party Models API is not available on Bedrock.", + "Use Bedrock model discovery and keep runtime invocability as a separate probe.", + ), + "fallbacks": ( + "fallbacks_not_portable", + "Server-side model fallbacks are not available on the selected Bedrock path.", + "Implement explicit client-side routing, retry limits, and fallback observability.", + ), + "conversation_state": ( + "conversation_state_not_portable", + "Server-side conversation state is not available on the selected Bedrock path.", + "Persist and resend conversation history from an application-owned store.", + ), + "agent_infra": ( + "agent_infra_rearchitecture", + "First-party Anthropic agent infrastructure is not a portable inference-call feature.", + "Redesign Skills, MCP connectors, and managed-agent dependencies on AWS.", + ), +} + +_TUNE_FINDINGS = { + "refusal_handling": ( + "refusal_handling", + "Current Claude targets can return stop_reason=refusal.", + "Add explicit refusal handling and include refusal cases in evaluation.", + ), + "tokenizer_rebaseline": ( + "tokenizer_rebaseline", + "Token counts change across Claude versions and Bedrock paths.", + "Re-baseline context sizing, usage accounting, and truncation alerts.", + ), + "max_tokens_headroom": ( + "max_tokens_headroom", + "Thinking tokens share max_tokens and framework defaults can truncate output.", + "Preserve source intent and tune from output distributions and thinking headroom.", + ), + "prompt_caching": ( + "prompt_cache_validation", + "Prompt caching is supported but cache-hit behavior must not be assumed.", + "Verify writes, reads, TTL, minimum-token thresholds, and hit telemetry.", + ), +} + + +def _finding(code, tag, message, remediation): + return { + "code": code, + "tag": tag, + "message": message, + "remediation": remediation, + } + + +def _delta(code, category, description): + return {"code": code, "category": category, "description": description} + + +def _version_tuple(value): + if not value: + return None + match = re.search(r"(\d+)\.(\d+)", value) + return tuple(map(int, match.groups())) if match else None + + +def _source_version(source): + patterns = ( + r"claude-(?:opus-|sonnet-|haiku-)?(\d+)[.-](\d+)", + r"claude-(\d+)[.-](\d+)-(?:opus|sonnet|haiku)", + ) + for model_id in source.get("model_ids", []): + normalized = model_id.lower().replace("_", "-") + for pattern in patterns: + match = re.search(pattern, normalized) + if match: + return f"{int(match.group(1))}.{int(match.group(2))}" + return None + + +def _candidate_order(requirements): + features = requirements.get("critical_features", []) + for feature in ("agentic",): + if feature in features: + return _FEATURE_ORDER[feature], feature + priority = requirements.get("priority", "unknown") + return _PRIORITY_ORDER.get(priority, _PRIORITY_ORDER["unknown"]), priority + + +def _required_capabilities(requirements): + capabilities = { + _CAPABILITY_ALIASES.get(feature, feature) + for feature in requirements.get("critical_features", []) + if _CAPABILITY_ALIASES.get(feature, feature) in _MODEL_CAPABILITIES + } + if requirements.get("thinking_enabled"): + capabilities.add("extended_thinking") + return capabilities + + +def _path_constraints(workload): + source = workload["source"] + requirements = workload["requirements"] + preferred = requirements.get("preferred_api_path") + if preferred and preferred not in SUPPORTED_PATHS: + raise ValueError(f"unsupported preferred_api_path: {preferred}") + + runtime_required = bool(requirements.get("governance")) or requirements.get( + "multi_model_converse", False + ) + native_required = requirements.get("requires_native_payload", False) + messages_required = requirements.get("preserve_messages_api", False) or requirements.get( + "newest_anthropic_betas", False + ) + source_messages = ( + source["provider"] == "anthropic" + and source.get("api_surface") == "messages" + and requirements.get("preserve_messages_api") is not False + ) + + runtime_path = "runtime_invoke" if native_required else "runtime_converse" + conflicts = [] + if messages_required and (runtime_required or native_required): + conflicts.append("messages_vs_runtime") + if preferred: + if preferred.startswith("mantle_") and (runtime_required or native_required): + conflicts.append("preferred_path_vs_runtime") + if preferred.startswith("runtime_") and messages_required: + conflicts.append("preferred_path_vs_messages") + if native_required and preferred != "runtime_invoke": + conflicts.append("preferred_path_vs_native_payload") + + if conflicts: + return { + "paths": [], + "conflicts": sorted(set(conflicts)), + "option_paths": ["mantle_messages", runtime_path], + "rationale": [ + "Messages continuity or beta requirements conflict with required Bedrock runtime capabilities." + ], + } + if preferred: + return { + "paths": [preferred], + "conflicts": [], + "option_paths": [], + "rationale": [f"User selected the {preferred} API path."], + } + if native_required: + return { + "paths": ["runtime_invoke"], + "conflicts": [], + "option_paths": [], + "rationale": ["InvokeModel is required for the provider-native Bedrock body."], + } + if runtime_required: + return { + "paths": ["runtime_converse"], + "conflicts": [], + "option_paths": [], + "rationale": [ + "Converse is required by governance, logging, or multi-model requirements." + ], + } + if messages_required: + return { + "paths": ["mantle_messages"], + "conflicts": [], + "option_paths": [], + "rationale": [ + "Mantle is required to preserve Messages semantics or newest beta features." + ], + } + if source_messages: + return { + "paths": list(ANTHROPIC_PATHS), + "conflicts": [], + "option_paths": [], + "rationale": [ + "Mantle is preferred for an existing first-party Messages API call." + ], + } + return { + "paths": ["runtime_converse", "runtime_invoke", "mantle_messages"], + "conflicts": [], + "option_paths": [], + "rationale": ["Converse is the default for a new governance-ready Bedrock workload."], + } + + +def _build_candidates(catalog, paths, requirements): + model_order, driver = _candidate_order(requirements) + required_capabilities = _required_capabilities(requirements) + min_context = requirements.get("min_context_tokens", 0) + expected_output = requirements.get("expected_output_tokens", 0) + candidates = [] + for path_rank, path in enumerate(paths): + for model_rank, model_key in enumerate(model_order): + model = catalog["models"][model_key] + path_config = model["paths"].get(path, {}) + if path_config.get("available") is not True: + continue + if not required_capabilities.issubset(set(model["capabilities"])): + continue + if model["context_window"] < min_context: + continue + if model["output_token_ceiling"] < expected_output: + continue + candidates.append( + { + "model_key": model_key, + "model": model, + "path": path, + "path_config": path_config, + "rank": (path_rank, model_rank), + "driver": driver, + } + ) + return sorted(candidates, key=lambda item: item["rank"]) + + +def _candidate_summary(candidate, requirements, reason): + model = candidate["model"] + path_config = candidate["path_config"] + invocation_model_id = _resolve_invocation_model_id( + path_config["model_id"], path_config["requires_cris"], requirements + ) + return { + "model_key": candidate["model_key"], + "model": path_config["model_id"], + "api_path": candidate["path"], + "invocation_model_id": invocation_model_id, + "requires_cris": path_config["requires_cris"], + "reason": reason, + } + + +def _resolve_invocation_model_id(model_id, requires_cris, requirements): + if not requires_cris: + return model_id + explicit = requirements.get("inference_profile_id") + if explicit: + return explicit + residency = requirements.get("data_residency", "unknown") + if residency == "global_allowed": + return f"global.{model_id}" + if residency == "geo_required" and requirements.get("cris_geography"): + return f"{requirements['cris_geography']}.{model_id}" + return None + + +def _decision_options(catalog, workload, option_paths): + options = [] + for path in dict.fromkeys(option_paths): + candidates = _build_candidates(catalog, [path], workload["requirements"]) + if not candidates: + continue + tradeoff = ( + "Preserves Messages code and beta access but gives up runtime-only governance." + if path == "mantle_messages" + else "Provides runtime governance but requires rewriting the Messages integration." + ) + options.append( + _candidate_summary(candidates[0], workload["requirements"], tradeoff) + ) + return options + + +def _feature_assessment(workload, source_version, target_version): + detected = set(workload.get("detected_features", [])) + statuses = dict(workload.get("feature_status", {})) + for feature in detected: + statuses[feature] = "detected" + source_tuple = _version_tuple(source_version) + target_tuple = _version_tuple(target_version) + if source_tuple and target_tuple and source_tuple < target_tuple and target_tuple >= (4, 7): + for feature in _VERSION_SCAN_FEATURES: + statuses.setdefault(feature, "unknown") + return dict(sorted(statuses.items())) + + +def _source_analysis(source, target_version): + source_version = _source_version(source) + source_tuple = _version_tuple(source_version) + target_tuple = _version_tuple(target_version) + changed = ( + source_tuple != target_tuple + if source_tuple is not None and target_tuple is not None + else None + ) + return { + "detected_version": source_version, + "target_version": target_version, + "version_changed": changed, + } + + +def _migration_deltas(source, source_analysis, path, feature_status, requirements): + if source["provider"] != "anthropic": + return [] + deltas = [ + _delta( + "sdk_and_auth", + "path", + "Authentication and client construction change for the selected Bedrock path.", + ), + _delta( + "model_id_shape", + "path", + "Mantle uses a clean path ID; runtime requires a verified CRIS profile.", + ), + _delta( + "iam_action", + "platform", + "Mantle and runtime use different IAM actions and resource conditions.", + ), + _delta( + "version_header", + "path", + "Mantle uses the HTTP version header; runtime uses the Bedrock body field.", + ), + _delta( + "error_and_quota_surface", + "platform", + "Messages 400s, runtime ValidationException, and quota pools differ by path.", + ), + ] + if source_analysis["version_changed"]: + deltas.append( + _delta( + "claude_version_hop", + "version", + f"Claude {source_analysis['detected_version']} to " + f"{source_analysis['target_version']} requires version migration checks.", + ) + ) + detected = { + feature for feature, status in feature_status.items() if status == "detected" + } + if "structured_output" in detected: + deltas.append( + _delta( + "structured_output", + "feature", + "Use a forced tool without strict as the portable default; native fields " + "are model, path, and region dependent.", + ) + ) + if path == "runtime_converse" and ( + "extended_thinking" in requirements.get("critical_features", []) + or "budget_tokens" in detected + ): + deltas.append( + _delta( + "additional_model_request_fields", + "path", + "Converse carries Anthropic thinking and effort fields through " + "additionalModelRequestFields.", + ) + ) + if "prompt_caching" in detected: + deltas.append( + _delta( + "prompt_cache_shape", + "path", + "Mantle cache_control and Converse cachePoint use different request shapes.", + ) + ) + return deltas + + +def _compatibility(feature_status, requirements, path): + detected = { + feature for feature, status in feature_status.items() if status == "detected" + } + native = { + feature + for feature in detected + if feature in {"citations", "streaming", "tool_use", "vision"} + } + if "prompt_caching" in detected and path == "mantle_messages": + native.add("prompt_caching") + portable = detected.intersection(_VERSION_SCAN_FEATURES) + portable.update(detected.intersection({"models_api", "structured_output"})) + if "prompt_caching" in detected and path != "mantle_messages": + portable.add("prompt_caching") + rearchitecture = detected.intersection(_REARCHITECTURE_FEATURES) + critical = set(requirements.get("critical_features", [])) + native.update(critical.intersection({"tool_use", "vision", "multimodal"})) + return { + "native": sorted(native), + "portable": sorted(portable - rearchitecture), + "rearchitecture": sorted(rearchitecture), + "unsupported": [], + } + + +def _architecture_impacts(feature_status): + detected = { + feature for feature, status in feature_status.items() if status == "detected" + } + impacts = [] + for feature in sorted(detected.intersection(_REARCHITECTURE_FEATURES)): + _, message, remediation = _BLOCK_FINDINGS[feature] + impacts.append( + { + "feature": feature, + "impact": message, + "recommendation": remediation, + } + ) + return impacts + + +def _evaluation_requirements(workload, feature_status): + detected = { + feature for feature, status in feature_status.items() if status == "detected" + } + critical = set(workload["requirements"].get("critical_features", [])) + trajectory = bool( + critical.intersection({"agentic", "tool_use"}) + or detected.intersection({"agent_infra", "server_tools"}) + ) + gates = [ + "Compare representative source and target outputs against a versioned golden set.", + "Fail on refusal mishandling, truncation, or invalid structured output.", + ] + if trajectory: + gates.extend( + [ + "Verify the correct tool is called with valid arguments.", + "Verify the agent loop terminates and guardrails remain effective.", + ] + ) + return {"mode": "trajectory" if trajectory else "prompt", "gates": gates} + + +def _base_findings(feature_status, source_analysis): + detected = { + feature for feature, status in feature_status.items() if status == "detected" + } + blocks = [] + tuning = [] + for feature in sorted(detected): + if feature in _BLOCK_FINDINGS: + code, message, remediation = _BLOCK_FINDINGS[feature] + blocks.append(_finding(code, "[BLOCKS]", message, remediation)) + if feature in _TUNE_FINDINGS: + code, message, remediation = _TUNE_FINDINGS[feature] + tuning.append(_finding(code, "[TUNE]", message, remediation)) + unknown_version_features = sorted( + feature + for feature in _VERSION_SCAN_FEATURES + if feature_status.get(feature) == "unknown" + ) + if source_analysis["version_changed"] and unknown_version_features: + blocks.append( + _finding( + "version_scan_incomplete", + "[BLOCKS]", + "The source-to-target Claude version hop has unverified breaking-change surfaces: " + + ", ".join(unknown_version_features), + "Scan the recorded source paths and mark every feature detected or absent.", + ) + ) + if "structured_output" in detected: + blocks.append( + _finding( + "structured_output_portable_pattern", + "[BLOCKS]", + "Native structured-output controls vary by model, path, and region.", + "Use a forced tool without strict; validate the schema subset and do not " + "combine structured output with citations.", + ) + ) + if "structured_output" in detected and "citations" in detected: + blocks.append( + _finding( + "structured_output_citations_conflict", + "[BLOCKS]", + "Structured output and citations are incompatible in the reference snapshot.", + "Choose one output contract per call site and verify it live.", + ) + ) + return blocks, tuning + + +def _verification(candidate, region, catalog, invocation_model_id): + path = candidate["path"] + checks = [ + "Probe the selected model through the selected API path in the target account and region.", + "Verify path-specific IAM before code rewrite or POC generation.", + ] + if candidate["path_config"]["requires_cris"]: + checks.insert( + 1, + "Resolve and probe a Global or geography-scoped CRIS inference profile.", + ) + return { + "region": region, + "catalog_verified_at": catalog["verified_at"], + "verified_at": None, + "probe_status": "not_run", + "availability_claim": "provisional", + "invocation_model_id": invocation_model_id, + "required_checks": checks, + } + + +def _decision_required(workload, region, catalog, constraints): + feature_status = _feature_assessment(workload, _source_version(workload["source"]), None) + decision_options = _decision_options( + catalog, workload, constraints["option_paths"] + ) + if len(decision_options) != len(set(constraints["option_paths"])): + raise ValueError( + "catalog has no candidate for every conflicting model/path option " + f"for workload {workload['workload_id']}" + ) + blocks = [ + _finding( + "model_path_decision_required", + "[BLOCKS]", + "Messages continuity conflicts with required Bedrock runtime capabilities.", + "Choose the Mantle continuity option or the runtime governance option, " + "update requirements, and rerun Model Recommend.", + ) + ] + return { + "workload_id": workload["workload_id"], + "provider_module": "anthropic", + "decision_status": "decision_required", + "source": workload["source"], + "source_analysis": { + "detected_version": _source_version(workload["source"]), + "target_version": None, + "version_changed": None, + }, + "feature_assessment": feature_status, + "primary_model": None, + "model_identity": None, + "api_path": None, + "invocation_model_id": None, + "decision_options": decision_options, + "alternatives": [], + "rationale": constraints["rationale"], + "blocks": blocks, + "tuning": [], + "compatibility": _compatibility(feature_status, workload["requirements"], ""), + "architecture_impacts": _architecture_impacts(feature_status), + "migration_deltas": [], + "evaluation": _evaluation_requirements(workload, feature_status), + "rollout": { + "strategy": "decision_required", + "gate": "Resolve the model/path conflict before implementation.", + }, + "verification": { + "region": region, + "catalog_verified_at": catalog["verified_at"], + "verified_at": None, + "probe_status": "not_applicable", + "availability_claim": "not_selected", + "invocation_model_id": None, + "required_checks": [ + "Resolve the model/path decision before running an availability probe." + ], + }, + } + + +def recommend_anthropic_workload(workload, region, catalog): + constraints = _path_constraints(workload) + if constraints["conflicts"]: + return _decision_required(workload, region, catalog, constraints) + + candidates = _build_candidates( + catalog, constraints["paths"], workload["requirements"] + ) + if not candidates: + raise ValueError( + f"catalog has no model/path candidate satisfying workload " + f"{workload['workload_id']} requirements" + ) + chosen = candidates[0] + model = chosen["model"] + path = chosen["path"] + path_config = chosen["path_config"] + source_analysis = _source_analysis(workload["source"], model["version"]) + feature_status = _feature_assessment( + workload, + source_analysis["detected_version"], + source_analysis["target_version"], + ) + blocks, tuning = _base_findings(feature_status, source_analysis) + provider = workload["source"]["provider"] + provider_module = ( + "anthropic" if provider in {"anthropic", "none", "unknown"} else "generic" + ) + if provider_module == "generic": + blocks.append( + _finding( + "provider_module_pending", + "[BLOCKS]", + f"The {provider} to Bedrock compatibility module is not implemented yet.", + "Keep this recommendation provisional until its provider module runs.", + ) + ) + invocation_model_id = _resolve_invocation_model_id( + path_config["model_id"], + path_config["requires_cris"], + workload["requirements"], + ) + rationale = list(constraints["rationale"]) + rationale.append( + f"{model['display_name']} is the highest-ranked {chosen['driver']} model " + f"that satisfies the {path} constraints." + ) + alternatives = [ + _candidate_summary( + candidate, + workload["requirements"], + "Next compatible model/path candidate after hard-constraint filtering.", + ) + for candidate in candidates[1:4] + ] + return { + "workload_id": workload["workload_id"], + "provider_module": provider_module, + "decision_status": "recommended", + "source": workload["source"], + "source_analysis": source_analysis, + "feature_assessment": feature_status, + "primary_model": path_config["model_id"], + "model_identity": { + "model_key": chosen["model_key"], + "display_name": model["display_name"], + "family": model["family"], + "version": model["version"], + "context_window": model["context_window"], + "output_token_ceiling": model["output_token_ceiling"], + "path_model_id": path_config["model_id"], + "requires_cris": path_config["requires_cris"], + }, + "api_path": path, + "invocation_model_id": invocation_model_id, + "decision_options": [], + "alternatives": alternatives, + "rationale": rationale, + "blocks": blocks, + "tuning": tuning, + "compatibility": _compatibility( + feature_status, workload["requirements"], path + ), + "architecture_impacts": _architecture_impacts(feature_status), + "migration_deltas": _migration_deltas( + workload["source"], + source_analysis, + path, + feature_status, + workload["requirements"], + ), + "evaluation": _evaluation_requirements(workload, feature_status), + "rollout": { + "strategy": "canary", + "gate": "Compare source and target on the golden set before percentage rollout.", + }, + "verification": _verification( + chosen, region, catalog, invocation_model_id + ), + } + diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/model_recommendation.py b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/model_recommendation.py new file mode 100644 index 00000000..ba217232 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/model_recommendation.py @@ -0,0 +1,205 @@ +"""Bedrock model/path recommendation orchestrator for agent-advisor. + +Validates the shared input contract, dispatches each workload to its provider +module (anthropic_model_recommendation / openai_model_recommendation; anything +else falls through to the provisional generic path), aggregates per-workload +results with per-workload catalog provenance, and validates the shared output +contract. Provider-specific rules live in the provider modules, not here. +""" + +import argparse +import json +import pathlib + +import anthropic_model_recommendation +import openai_model_recommendation + + +SKILL_DIR = pathlib.Path(__file__).parent.parent +MODELS_DIR = SKILL_DIR / "references" / "models" +DEFAULT_CATALOG = MODELS_DIR / "anthropic-bedrock-2026-07-21.json" +OPENAI_CATALOG = MODELS_DIR / "openai-bedrock-2026-07-21.json" + +# Source providers handled by a real, dedicated recommendation module. +# Everything else falls through to the provisional generic Anthropic path. +PROVIDER_MODULES = { + "anthropic": "anthropic", + "none": "anthropic", + "unknown": "anthropic", + "openai": "openai", +} + + + +def load_catalog(path=DEFAULT_CATALOG): + catalog_path = pathlib.Path(path) + try: + catalog = json.loads(catalog_path.read_text()) + except json.JSONDecodeError as exc: + raise ValueError(f"{catalog_path}: invalid JSON ({exc})") from exc + required = { + "schema_version", + "provider", + "verified_at", + "verified_region", + "paths", + "models", + } + missing = sorted(required - set(catalog)) + if missing: + raise ValueError(f"{catalog_path}: missing required keys {missing}") + for model_key, model in catalog["models"].items(): + for field in ( + "display_name", + "family", + "version", + "context_window", + "output_token_ceiling", + "capabilities", + "paths", + ): + if field not in model: + raise ValueError(f"{catalog_path}: {model_key} missing {field}") + return catalog + + +def load_openai_catalog(path=OPENAI_CATALOG): + """Load and validate the dated OpenAI/Bedrock path catalog. + + Unlike the Anthropic catalog, numeric limits may be the string "unknown" + (the reference proves paths, not context/output ceilings), and each model + carries a `generation` and per-path `evidence` citation. + """ + catalog_path = pathlib.Path(path) + try: + catalog = json.loads(catalog_path.read_text()) + except json.JSONDecodeError as exc: + raise ValueError(f"{catalog_path}: invalid JSON ({exc})") from exc + required = { + "schema_version", + "provider", + "verified_at", + "verified_region", + "paths", + "models", + } + missing = sorted(required - set(catalog)) + if missing: + raise ValueError(f"{catalog_path}: missing required keys {missing}") + for model_key, model in catalog["models"].items(): + for field in ( + "display_name", + "family", + "generation", + "version", + "context_window", + "output_token_ceiling", + "capabilities", + "paths", + ): + if field not in model: + raise ValueError(f"{catalog_path}: {model_key} missing {field}") + for limit in ("context_window", "output_token_ceiling"): + value = model[limit] + if value != "unknown" and not (isinstance(value, int) and value > 0): + raise ValueError( + f"{catalog_path}: {model_key} {limit} must be a positive int or " + f'"unknown", got {value!r}' + ) + return catalog + + + + +def _catalog_provenance(catalog): + return { + "provider": catalog["provider"], + "verified_at": catalog["verified_at"], + "verified_region": catalog["verified_region"], + "source": catalog.get("source"), + } + + +def recommend(input_data, catalog=None, openai_catalog=None): + """Dispatch each workload to its provider module. + + Anthropic (and none/unknown/generic) use `catalog`; OpenAI uses + `openai_catalog`. Every workload records the provenance of the catalog that + produced it so a mixed-provider run never mislabels a source. + """ + if catalog is None: + catalog = load_catalog() + workloads = {} + provenance = {} + catalogs_used = {} + for workload in input_data["workloads"]: + workload_id = workload["workload_id"] + if workload_id in workloads: + raise ValueError(f"duplicate workload_id: {workload_id}") + provider = workload["source"]["provider"] + module = PROVIDER_MODULES.get(provider, "generic") + if module == "openai": + if openai_catalog is None: + openai_catalog = load_openai_catalog() + workloads[workload_id] = openai_model_recommendation.recommend_openai_workload( + workload, input_data["region"], openai_catalog + ) + provenance[workload_id] = _catalog_provenance(openai_catalog) + catalogs_used["openai"] = openai_catalog + else: + workloads[workload_id] = anthropic_model_recommendation.recommend_anthropic_workload( + workload, input_data["region"], catalog + ) + provenance[workload_id] = _catalog_provenance(catalog) + catalogs_used["anthropic"] = catalog + primary_unit = input_data["primary_unit"] + if primary_unit not in workloads: + raise ValueError(f"primary_unit not found in workloads: {primary_unit}") + # Backward-compatible top-level `catalog`: the primary unit's catalog, plus + # explicit per-workload provenance so a mixed run keeps correct attribution. + return { + "schema_version": 2, + "catalog": provenance[primary_unit], + "catalog_provenance": provenance, + "primary_unit": primary_unit, + "workloads": workloads, + } + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="agent-advisor Bedrock model recommendation" + ) + parser.add_argument("input", type=pathlib.Path) + parser.add_argument( + "--output", + type=pathlib.Path, + help="defaults to model-recommendation.json beside the input", + ) + parser.add_argument("--catalog", type=pathlib.Path, default=DEFAULT_CATALOG) + args = parser.parse_args(argv) + + try: + import jsonschema + except ImportError: + jsonschema = None + + def validate(instance, schema_name): + if jsonschema is None: + return + schemas = pathlib.Path(__file__).parent / "schemas" + jsonschema.validate(instance, json.loads((schemas / schema_name).read_text())) + + input_data = json.loads(args.input.read_text()) + validate(input_data, "model-recommendation-input.json") + result = recommend(input_data, load_catalog(args.catalog)) + validate(result, "model-recommendation.json") + output = args.output or args.input.parent / "model-recommendation.json" + output.write_text(json.dumps(result, indent=2) + "\n") + validated = "no" if jsonschema is None else "yes" + print(f"RESULT=ok WORKLOADS={len(result['workloads'])} SCHEMA_VALIDATED={validated}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/openai_model_recommendation.py b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/openai_model_recommendation.py new file mode 100644 index 00000000..1349d1f2 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/openai_model_recommendation.py @@ -0,0 +1,940 @@ +"""Deterministic OpenAI-to-Bedrock model/path recommendation for agent-advisor. + +Factual baseline: an internal OpenAI-to-Bedrock migration reference (2026-06 live +probes and its recorded feature-status notes). The engine is offline: +no openai/boto3/token-generator imports at module load. Rankings are only produced +where the reference provides evidence; otherwise the engine returns an explicit +decision or a provisional limitation rather than inventing a comparative order. +""" + +import re + + +# --- Source model-family detection (ported from the reference compatibility matrix) --- +# Reasoning generation: gpt-5 / gpt5 / gpt-5.4 / openai.gpt-5.4, and o1/o3/o4. +_REASONING_PATTERNS = ( + re.compile(r"(?i)(^|[^a-z0-9])gpt[-_ ]?5(\.\d+)?([^0-9]|$)"), + re.compile(r"(?i)(^|[^a-z0-9])o[134](-mini|-pro|-preview)?([^a-z0-9]|$)"), +) +# Legacy generation: gpt-4*, gpt-3.5 / gpt-35. +_LEGACY_PATTERNS = ( + re.compile(r"(?i)gpt[-_ ]?4o"), + re.compile(r"(?i)gpt[-_ ]?4(?!\d)"), + re.compile(r"(?i)gpt[-_ ]?4[-_ ]?32k"), + re.compile(r"(?i)gpt[-_ ]?3\.?5"), + re.compile(r"(?i)gpt[-_ ]?35"), +) +# GPT-5.2+ re-accepts sampling on Responses; earlier reasoning models differ. +_GPT5_MINOR = re.compile(r"(?i)gpt[-_ ]?5\.(\d+)") + + +def detect_family(model_id): + """Return 'reasoning', 'legacy', or 'unknown'. + + Fails to 'unknown' for opaque deployment names (e.g. 'prod-default') so the + engine never infers a model family from a deployment alias. + """ + name = (model_id or "").strip() + if not name: + return "unknown" + for pat in _REASONING_PATTERNS: + if pat.search(name): + return "reasoning" + for pat in _LEGACY_PATTERNS: + if pat.search(name): + return "legacy" + return "unknown" + + +def _gpt5_minor(model_id): + match = _GPT5_MINOR.search(model_id or "") + return int(match.group(1)) if match else None + + +def _primary_source_id(source): + ids = source.get("model_ids") or [] + return ids[0] if ids else "" + + +def _source_family(source): + """Family for the workload: reasoning if ANY id is reasoning, else legacy if + any is legacy, else unknown (opaque deployment names stay unknown).""" + families = [detect_family(mid) for mid in source.get("model_ids") or [""]] + if "reasoning" in families: + return "reasoning" + if "legacy" in families: + return "legacy" + return "unknown" + + +def _finding(code, tag, message, remediation): + return {"code": code, "tag": tag, "message": message, "remediation": remediation} + + +def _delta(code, category, description): + return {"code": code, "category": category, "description": description} + + +# --- Feature vocabulary (provider-scoped; codes match scanner feature-catalog) --- +_HOSTED_TOOL_IMPACTS = { + "web_search": ( + "Hosted web search does not execute live on Bedrock Mantle (verified passthrough: " + "no query issued, no citations).", + "Implement a client-side tool loop, or expose a server-side tool (MCP/Lambda) through " + "Bedrock AgentCore Gateway; use Knowledge Bases for document retrieval.", + ), + "file_search_retrieval": ( + "Hosted file search / retrieval is not a drop-in on Bedrock.", + "Re-platform retrieval onto Bedrock Knowledge Bases or application-owned retrieval.", + ), + "files_api": ( + "The OpenAI Files API and vector stores do not port directly.", + "Redesign data ingestion and retrieval; use Knowledge Bases or application storage.", + ), + "vector_stores": ( + "OpenAI vector stores are not a Bedrock primitive.", + "Move vectors to a Bedrock-supported store and retrieve from the application.", + ), + "assistants_threads": ( + "Assistants/Threads hosted state is not available on Bedrock.", + "Redesign agent state, tools, and memory (e.g. Bedrock AgentCore).", + ), + "audio_modality": ( + "Audio is not a single-call text-model capability on Bedrock.", + "Split into a verified STT/TTS service path (Amazon Transcribe / Polly).", + ), + "embeddings": ( + "Embeddings require a separate Bedrock embedding model, not the text model.", + "Select a sourced Bedrock embedding model (e.g. Titan/Cohere) as a separate workload.", + ), + "images": ( + "Image generation/editing is a separate capability, not a text-model swap.", + "Select a sourced Bedrock image model/service as a separate workload.", + ), +} + + +# Converse tier map: a governance workload keeps its capability tier when moving +# from the (Mantle-only) GPT-5.x family to Claude on runtime Converse. +# GPT-5.6 Sol (frontier) -> Claude Opus 4.8 +# GPT-5.6 Terra / 5.5 / 5.4 -> Claude Sonnet 5 (also the default tier) +# GPT-5.6 Luna (fast/low-cost)-> Claude Haiku 4.5 +_CONVERSE_TIER_DEFAULT = "anthropic_claude_sonnet_5" +_CONVERSE_TIER_ORDER = ( + "anthropic_claude_sonnet_5", + "anthropic_claude_opus_4_8", + "anthropic_claude_haiku_4_5", +) + + +def _converse_tier_for_source(source): + sid = _primary_source_id(source).lower() + if "5.6-sol" in sid: + return "anthropic_claude_opus_4_8" + if "5.6-luna" in sid: + return "anthropic_claude_haiku_4_5" + # 5.6-terra, 5.5, 5.4, legacy, and unknown sources all map to the balanced tier. + return _CONVERSE_TIER_DEFAULT + + +def _converse_candidate_order(source): + tier = _converse_tier_for_source(source) + return [tier] + [k for k in _CONVERSE_TIER_ORDER if k != tier] + + +def _catalog_model_for_path(catalog, path, detected_features=None, requirements=None, + candidate_order=None): + """Pick the first available catalog model for `path` whose capability evidence + covers the required/detected text features (evidence-driven fallback: a newer + model without feature-level evidence is skipped in favor of an older probed + one). When no candidate covers them, return the first available model together + with its unmet-capability list so the caller can fail closed. Deterministic: + iterates `candidate_order` when given (e.g. the Converse tier map), else + catalog key order.""" + first = None + first_unmet = None + keys = candidate_order if candidate_order is not None else list(catalog["models"]) + for model_key in keys: + model = catalog["models"].get(model_key) + if model is None: + continue + path_config = model["paths"].get(path, {}) + if path_config.get("available") is not True: + continue + unmet = _unsupported_required_capabilities( + model, detected_features, requirements or {} + ) + if first is None: + first = (model_key, model, path_config) + first_unmet = unmet + if not unmet: + return (model_key, model, path_config), [] + return first, first_unmet + + +def _resolve_invocation_model_id(model_id, requires_cris, requirements): + if not requires_cris: + return model_id + explicit = requirements.get("inference_profile_id") + if explicit: + return explicit + residency = requirements.get("data_residency", "unknown") + if residency == "global_allowed": + return f"global.{model_id}" + if residency == "geo_required" and requirements.get("cris_geography"): + return f"{requirements['cris_geography']}.{model_id}" + return None + + +def _api_continuity(requirements): + """Provider-neutral continuity: required | preferred | not_required | unknown. + + Accepts the explicit `api_continuity` field; falls back to the OpenAI-specific + `preserve_openai_api` boolean. Never reuses Anthropic's preserve_messages_api. + """ + explicit = requirements.get("api_continuity") + if explicit: + return explicit + if requirements.get("preserve_openai_api") is True: + return "required" + if requirements.get("preserve_openai_api") is False: + return "not_required" + return "unknown" + + +def _runtime_required(requirements): + return bool(requirements.get("governance")) or requirements.get( + "multi_model_converse", False + ) + + +def _model_identity(model_key, model, path_config): + return { + "model_key": model_key, + "display_name": model["display_name"], + "family": model["family"], + "version": model["version"], + "context_window": model["context_window"], + "output_token_ceiling": model["output_token_ceiling"], + "path_model_id": path_config["model_id"], + "requires_cris": path_config["requires_cris"], + } + + +def _numeric_requirement_conflict(model, requirements): + """Fail closed: an unknown catalog limit cannot satisfy a hard numeric need.""" + conflicts = [] + for req_key, cat_key, label in ( + ("min_context_tokens", "context_window", "context window"), + ("expected_output_tokens", "output_token_ceiling", "output ceiling"), + ): + need = requirements.get(req_key) + if not need: + continue + have = model.get(cat_key) + if have == "unknown" or not isinstance(have, int): + conflicts.append( + f"{label} for {model['display_name']} is unknown in the catalog; " + f"a required {need}-token need cannot be confirmed" + ) + elif have < need: + conflicts.append( + f"{label} for {model['display_name']} ({have}) is below the required {need}" + ) + return conflicts + + +# Extracts a numeric OpenAI version (major or major.minor) from an id/version. +_OPENAI_VERSION = re.compile(r"(?i)gpt[-_ ]?(\d+(?:\.\d+)?)") + + +def _openai_version(text): + match = _OPENAI_VERSION.search(text or "") + return match.group(1) if match else None + + +def _version_tuple(value): + if not value: + return None + parts = value.split(".") + try: + return (int(parts[0]), int(parts[1]) if len(parts) > 1 else 0) + except (ValueError, IndexError): + return None + + +def _target_accepts_sampling(target_model): + """GPT-5.2+ re-accepts sampling params on Responses (verified for GPT-5.4). + + Returns True/False for an OpenAI target of known version, None when the + target is not an OpenAI model or its version can't be parsed. + """ + if target_model is None or target_model.get("generation") == "bedrock_native": + return None + version = _version_tuple(target_model.get("version")) + if version is None: + return None + return version >= (5, 2) + + +def _source_analysis(source, target_model=None): + """Source-side facts plus, when a target has been selected, target-derived + version/generation change flags. Target fields stay null before selection.""" + source_id = _primary_source_id(source) + family = _source_family(source) + surface = source.get("api_surface") + if target_model is None: + target_version = None + version_changed = None + generation_changes = None + else: + target_version = target_model.get("version") + target_generation = target_model.get("generation") + if target_generation == "bedrock_native": + # OpenAI source -> a Bedrock-native model is always a model change. + version_changed = True + generation_changes = True + else: + source_version = _openai_version(source_id) + if source_version and target_version: + version_changed = source_version != target_version + else: + # o-series / opaque source vs a specific GPT-5.x target: a move, + # but we cannot pin the exact source version. + version_changed = family in {"legacy", "reasoning"} or None + generation_changes = family == "legacy" and target_generation == "reasoning" + return { + "detected_version": source_id or None, + "target_version": target_version, + "version_changed": version_changed, + "source_family": family, + "source_api_surface": surface, + "model_generation_changes": generation_changes, + } + + +def _reasoning_findings(source, requirements, target_model, path): + """Version- and surface-specific parameter findings, derived from the SELECTED + target model and path — never from the source id, and never Anthropic's blanket + sampling-removal rule.""" + blocks, tuning, deltas = [], [], [] + src_family = _source_family(source) + src_id = _primary_source_id(source) + target_generation = target_model.get("generation") if target_model else None + target_name = target_model.get("display_name") if target_model else "the target model" + + if target_generation == "reasoning": + tuning.append( + _finding( + "reasoning_token_headroom", + "[TUNE]", + "Reasoning models spend hidden thinking tokens against the output budget; " + "too-small caps return status:incomplete.", + "Size the output cap for reasoning output. The reference uses ~2.5x the legacy " + "budget plus a 4096 floor as a STARTING heuristic, not a guaranteed value; " + "tune from measured output distributions.", + ) + ) + if src_family == "legacy" and target_generation == "reasoning": + deltas.append( + _delta( + "model_generation_hop", + "version", + f"Source '{src_id}' is a GPT-4.x/legacy model migrating to a reasoning target " + f"({target_name}): reasoning controls, token budget, and prompt behavior change.", + ) + ) + + # Sampling acceptance is a property of the TARGET model and the selected path. + if path == "runtime_converse": + tuning.append( + _finding( + "sampling_via_converse", + "[TUNE]", + f"On runtime Converse, sampling is set through inferenceConfig " + f"(temperature/topP) on {target_name}, not OpenAI SDK kwargs.", + "Move temperature/top_p into Converse inferenceConfig; penalties/logprobs/stop " + "are not universally supported and must be verified.", + ) + ) + else: + accepts = _target_accepts_sampling(target_model) + if accepts is True: + tuning.append( + _finding( + "sampling_params_accepted", + "[TUNE]", + f"{target_name} (GPT-5.2+) accepts temperature and top_p on the Responses " + "surface; do not strip them.", + "Keep temperature/top_p and calibrate against a golden set.", + ) + ) + tuning.append( + _finding( + "sampling_penalties_rejected", + "[TUNE]", + "frequency_penalty and presence_penalty were rejected as SDK kwargs on the " + "verified GPT-5.4 Responses probe.", + "Remove frequency_penalty/presence_penalty. logprobs, logit_bias, and stop " + "were NOT part of that probe and are version/endpoint-specific — verify each " + "for the exact target model rather than assuming acceptance or rejection.", + ) + ) + elif accepts is False: + tuning.append( + _finding( + "sampling_params_version_specific", + "[TUNE]", + f"{target_name} predates GPT-5.2 and may reject sampling parameters on " + "Responses; behavior is version-specific.", + "Verify sampling acceptance for the exact target model and surface.", + ) + ) + return blocks, tuning, deltas + + +def _chat_to_responses_deltas(): + return [ + _delta( + "chat_to_responses_request", + "path", + "Chat Completions -> Responses: messages->input, system message->instructions, " + "max_tokens->max_output_tokens.", + ), + _delta( + "chat_to_responses_response", + "path", + "Read model output from output_text instead of the chat message content.", + ), + _delta( + "chat_to_responses_tools", + "feature", + "Tool results move to function_call_output on the Responses surface.", + ), + _delta( + "chat_to_responses_state", + "path", + "Replayed multi-turn context uses previous_response_id where appropriate, or " + "application-owned state.", + ), + ] + + +def _feature_findings(detected_features, requirements, path=None): + """Structured output, n, tools, state, and hosted-tool/modality impacts. + + Findings are path-aware: Mantle Responses keeps verified capabilities + (responses.parse typed output, hosted store=True continuation) that a + Bedrock-native Converse target does not.""" + detected = set(detected_features or []) + detected.update(requirements.get("critical_features") or []) + is_mantle = path == "mantle_openai_responses" + blocks, tuning, deltas, impacts = [], [], [], [] + + if "structured_output_json" in detected: + if is_mantle: + # Verified on Mantle: responses.parse(text_format=Model) returns a typed object. + deltas.append( + _delta( + "structured_output_text_format", + "feature", + "On Mantle Responses, keep typed structured output: " + "client.responses.parse(..., text_format=Model) is verified, or use raw " + "text.format json_schema.", + ) + ) + else: + deltas.append( + _delta( + "structured_output_text_format", + "feature", + "For a Bedrock-native Converse target, enforce the schema via tool-use or " + "prompt + validation; the OpenAI parse() helper is not the selected path.", + ) + ) + if requirements.get("uses_n") or "multiple_candidates_n" in detected: + deltas.append( + _delta( + "responses_no_n", + "feature", + "The Responses API does not support n; request multiple candidates with " + "repeated calls.", + ) + ) + if "tool_or_function_calling" in detected: + deltas.append( + _delta( + "tool_result_shape", + "feature", + "Tool calls continue via function_call_output on Responses; the application " + "executes the tool.", + ) + ) + if "conversation_state" in detected or requirements.get("uses_hosted_state"): + if is_mantle: + deltas.append( + _delta( + "conversation_state_ownership", + "path", + "Two verified modes on Mantle Responses: server-hosted continuation " + "(store=True + previous_response_id) or manual replay (store=False with " + "application-owned history). Retention/compliance/availability are " + "verification questions, not a lost capability.", + ) + ) + else: + deltas.append( + _delta( + "conversation_state_ownership", + "path", + "A Bedrock-native Converse target has no hosted Responses state; carry " + "conversation history in an application-owned store.", + ) + ) + + impacted = sorted(detected.intersection(_HOSTED_TOOL_IMPACTS)) + for feature in impacted: + impact, remediation = _HOSTED_TOOL_IMPACTS[feature] + impacts.append( + {"feature": feature, "impact": impact, "recommendation": remediation} + ) + return blocks, tuning, deltas, impacts + + +# Text-model features that map to portable request-shape changes on any path. +_PORTABLE_FEATURES = {"max_tokens", "sampling_params", "sampling_params_accepted"} + + +# Separate-capability modalities that need their OWN target, not the text model. +# service = the Bedrock service family the reference names; candidate stays null +# (unresolved) because the reference does not pin a specific verified model ID. +_SEPARATE_MODALITY_TARGETS = { + "embeddings": ( + "Amazon Bedrock embedding model (e.g. Titan/Cohere families)", + "05_migrating_for_real.ipynb maps embeddings to a separate Bedrock embedding model", + ), + "images": ( + "Amazon Bedrock image model/service (e.g. Titan Image / Nova / Stability)", + "03_reasoning_api_migration.ipynb maps images to a separate Bedrock image model", + ), + "audio_modality": ( + "Amazon Transcribe / Polly (STT/TTS)", + "feature-catalog + notebooks map audio to Transcribe/Polly, not a text model", + ), +} + + +def _additional_targets(detected_features, requirements): + """Emit an explicit per-modality target contract; candidate stays null + (unresolved) since the reference names a service family, not a verified ID (G06).""" + detected = set(detected_features or []) + detected.update(requirements.get("critical_features") or []) + targets = [] + for capability in sorted(detected.intersection(_SEPARATE_MODALITY_TARGETS)): + service, evidence = _SEPARATE_MODALITY_TARGETS[capability] + targets.append( + { + "capability": capability, + "status": "unresolved", + "candidate": None, + "service": service, + "evidence": evidence + + "; select and verify a specific model/service in the target account", + } + ) + return targets + + +def _feature_assessment(workload): + """Merge detected_features and feature_status; feature_status is authoritative + for detected/absent/unknown, so an explicit 'unknown' is preserved (G08).""" + assessment = {} + for feature in workload.get("detected_features") or []: + assessment[feature] = "detected" + for feature, status in (workload.get("feature_status") or {}).items(): + assessment[feature] = status + return dict(sorted(assessment.items())) + + +def _unknown_required_features(feature_assessment, requirements): + """Required features whose status is explicitly unknown (G08).""" + required = set(requirements.get("critical_features") or []) + return sorted(f for f in required if feature_assessment.get(f) == "unknown") + + +def _unsupported_required_capabilities(target_model, detected_features, requirements): + """Required/detected text-model features NOT evidenced by the target's catalog + capabilities. Hosted tools/modalities are handled as architecture impacts, not here.""" + if target_model is None: + return [] + catalog_caps = set(target_model.get("capabilities") or []) + needed = set(requirements.get("critical_features") or []) + needed.update(detected_features or []) + checkable = needed.intersection( + { + "tool_or_function_calling", + "structured_output_json", + "streaming", + "image_input_vision", + "reasoning", + } + ) + return sorted(checkable - catalog_caps) + + +def _compatibility(detected_features, requirements, path, target_model=None): + """native = only features the SELECTED target's catalog evidences; features + needed but not evidenced go to unsupported (never derive native from the name).""" + detected = set(detected_features or []) + detected.update(requirements.get("critical_features") or []) + text_features = detected.intersection( + {"tool_or_function_calling", "structured_output_json", "streaming", "image_input_vision", "reasoning"} + ) + catalog_caps = set((target_model or {}).get("capabilities") or []) + native = sorted(text_features.intersection(catalog_caps)) if target_model else [] + unsupported = sorted(text_features - catalog_caps) if target_model else [] + rearchitecture = sorted(detected.intersection(_HOSTED_TOOL_IMPACTS)) + portable = sorted(detected.intersection(_PORTABLE_FEATURES)) + return { + "native": native, + "portable": portable, + "rearchitecture": rearchitecture, + "unsupported": unsupported, + } + + +def _evaluation(detected_features, requirements): + detected = set(detected_features or []) + detected.update(requirements.get("critical_features") or []) + trajectory = bool( + detected.intersection( + {"tool_or_function_calling", "assistants_threads", "web_search", "file_search_retrieval"} + ) + or "agentic" in (requirements.get("critical_features") or []) + ) + gates = [ + "Build a deterministic discovery inventory of clients, APIs, model IDs, and features.", + "Compare viable path/model candidates on a representative golden set.", + "Size quota for peak traffic and reasoning-output tokens.", + "Fail on refusal mishandling, truncation, or invalid structured output.", + ] + if trajectory: + gates.extend( + [ + "Verify correct tool selection, valid tool arguments, and tool-result continuation.", + "Verify loop termination and structured-output validity.", + ] + ) + return {"mode": "trajectory" if trajectory else "prompt", "gates": gates} + + +def _verification(region, catalog, path, requires_cris, invocation_model_id, selected): + if not selected: + return { + "region": region, + "catalog_verified_at": catalog["verified_at"], + "verified_at": None, + "probe_status": "not_applicable", + "availability_claim": "not_selected", + "invocation_model_id": None, + "required_checks": [ + "Resolve the model/path decision before running an availability probe." + ], + } + checks = [ + "Probe the selected model through the selected API path in the target account and region.", + "Verify path-specific IAM, model access, and quota before code rewrite or POC generation.", + ] + if requires_cris: + checks.insert( + 1, "Resolve and probe a Global or geography-scoped CRIS inference profile." + ) + return { + "region": region, + "catalog_verified_at": catalog["verified_at"], + "verified_at": None, + "probe_status": "not_run", + "availability_claim": "provisional", + "invocation_model_id": invocation_model_id, + "required_checks": checks, + } + + +def _decision_options(catalog, workload, region): + """Two-sided option set: Mantle Responses continuity vs runtime Converse governance.""" + options = [] + detected = workload.get("detected_features") or [] + mantle, _ = _catalog_model_for_path( + catalog, "mantle_openai_responses", detected, workload["requirements"] + ) + if mantle: + model_key, model, path_config = mantle + options.append( + { + "model_key": model_key, + "model": path_config["model_id"], + "api_path": "mantle_openai_responses", + "invocation_model_id": _resolve_invocation_model_id( + path_config["model_id"], path_config["requires_cris"], workload["requirements"] + ), + "requires_cris": path_config["requires_cris"], + "reason": "Preserves the OpenAI SDK and Responses surface; gives up runtime-only " + "Bedrock governance.", + } + ) + runtime, _ = _catalog_model_for_path( + catalog, "runtime_converse", detected, workload["requirements"], + candidate_order=_converse_candidate_order(workload["source"]), + ) + if runtime: + model_key, model, path_config = runtime + options.append( + { + "model_key": model_key, + "model": path_config["model_id"], + "api_path": "runtime_converse", + "invocation_model_id": _resolve_invocation_model_id( + path_config["model_id"], path_config["requires_cris"], workload["requirements"] + ), + "requires_cris": path_config["requires_cris"], + "reason": "Uses Bedrock-native Converse request/response shapes and a Bedrock-native " + "model; requires rewriting the OpenAI integration.", + } + ) + return options + + +def _base(workload, decision_status): + return { + "workload_id": workload["workload_id"], + "provider_module": "openai", + "decision_status": decision_status, + "source": workload["source"], + } + + +def recommend_openai_workload(workload, region, catalog): + """Return a recommendation dict in the shared downstream contract shape.""" + source = workload["source"] + requirements = workload["requirements"] + surface = source.get("api_surface") + continuity = _api_continuity(requirements) + runtime_required = _runtime_required(requirements) + continuity_required = continuity == "required" + + feature_assessment = _feature_assessment(workload) + # feature_status is authoritative: a feature is "effectively detected" only if + # the merged assessment marks it detected. Explicit absent/unknown are excluded, + # so downstream compatibility/deltas/targets never contradict the assessment. + detected = [f for f, status in feature_assessment.items() if status == "detected"] + + unknown_required = _unknown_required_features(feature_assessment, requirements) + + def _unresolved(reason_head, reasons, block, path_for_compat): + # Shared decision_required assembly (no target selected). + rec = _base(workload, "decision_required") + rec.update( + { + "source_analysis": _source_analysis(source, None), + "feature_assessment": feature_assessment, + "primary_model": None, + "model_identity": None, + "api_path": None, + "invocation_model_id": None, + "decision_options": _decision_options(catalog, workload, region), + "alternatives": [], + "rationale": [reason_head] + reasons, + "blocks": [block], + "tuning": [], + "compatibility": _compatibility(detected, requirements, path_for_compat), + "architecture_impacts": _feature_findings(detected, requirements, path_for_compat)[3], + "additional_targets": _additional_targets(detected, requirements), + "migration_deltas": [], + "evaluation": _evaluation(detected, requirements), + "rollout": { + "strategy": "decision_required", + "gate": "Resolve the open decision before implementation.", + }, + "verification": _verification(region, catalog, None, False, None, selected=False), + } + ) + return rec + + # --- Fail closed: a required feature with unknown status is not ready (G08) --- + if unknown_required: + return _unresolved( + "Required features are unresolved (status unknown): " + + ", ".join(unknown_required), + [ + "The scan did not confirm these required features as detected or absent, so " + "readiness cannot be asserted." + ], + _finding( + "feature_scan_incomplete", + "[BLOCKS]", + "Required features are unresolved (status unknown): " + + ", ".join(unknown_required), + "Scan the recorded source paths and mark each required feature detected or " + "absent, then rerun Model Recommend.", + ), + "", + ) + + # --- Hard conflict: OpenAI continuity required AND runtime-only governance --- + if continuity_required and runtime_required: + options = _decision_options(catalog, workload, region) + if len(options) < 2: + raise ValueError( + f"catalog cannot supply both decision options for workload " + f"{workload['workload_id']}" + ) + return _unresolved( + "Required OpenAI API continuity conflicts with a runtime-only Bedrock " + "governance requirement.", + [ + "Mantle preserves the OpenAI SDK/Responses surface; runtime Converse provides " + "Bedrock-native governance with a Bedrock-native model." + ], + _finding( + "model_path_decision_required", + "[BLOCKS]", + "OpenAI API continuity and runtime-only governance cannot both be satisfied " + "by one path.", + "Choose the Mantle continuity option or the runtime governance option, " + "update requirements, and rerun Model Recommend.", + ), + "", + ) + + # --- Select a path --- + candidate_order = None + if runtime_required: + path = "runtime_converse" + candidate_order = _converse_candidate_order(source) + tier = _converse_tier_for_source(source) + rationale_head = ( + "Bedrock governance or multi-model requirements select runtime Converse; " + f"the source tier maps to {catalog['models'][tier]['display_name']} " + "(capability-evidence fallback across Claude tiers)." + ) + else: + path = "mantle_openai_responses" + rationale_head = ( + "OpenAI source with API continuity lands on Mantle Responses (GPT-5.x is " + "Responses-only on Mantle)." + ) + + # Evidence-driven selection: pick the first candidate on the path whose catalog + # capabilities cover the required/detected text features (a newer model without + # feature-level evidence falls back to an older probed one). G02: fail closed + # only when NO candidate on the path has the evidence. + catalog_hit, unsupported = _catalog_model_for_path( + catalog, path, detected, requirements, candidate_order=candidate_order + ) + if not catalog_hit: + raise ValueError(f"catalog has no available model for path {path}") + model_key, model, path_config = catalog_hit + if unsupported: + return _unresolved( + rationale_head, + [ + f"No {path} candidate has catalog capability evidence for: " + + ", ".join(unsupported) + ], + _finding( + "unverified_capability", + "[BLOCKS]", + f"No {path} candidate is evidenced to support required features: " + + ", ".join(unsupported), + "Add dated capability evidence for a candidate, choose a different path/model, " + "or drop the requirement, then rerun Model Recommend.", + ), + path, + ) + + numeric_conflicts = _numeric_requirement_conflict(model, requirements) + if numeric_conflicts: + return _unresolved( + rationale_head, + numeric_conflicts, + _finding( + "unverified_capacity", + "[BLOCKS]", + "; ".join(numeric_conflicts), + "Source the missing capability data from a dated reference, or reduce the " + "hard numeric requirement, then rerun Model Recommend.", + ), + path, + ) + + invocation_model_id = _resolve_invocation_model_id( + path_config["model_id"], path_config["requires_cris"], requirements + ) + source_analysis = _source_analysis(source, model) + + # --- Findings (target- and path-derived) --- + r_blocks, r_tuning, r_deltas = _reasoning_findings(source, requirements, model, path) + f_blocks, f_tuning, f_deltas, impacts = _feature_findings(detected, requirements, path) + blocks = r_blocks + f_blocks + tuning = r_tuning + f_tuning + deltas = list(r_deltas) + + if path == "mantle_openai_responses" and surface == "chat_completions": + deltas.extend(_chat_to_responses_deltas()) + blocks.append( + _finding( + "chat_completions_to_responses_required", + "[BLOCKS]", + "GPT-5.x on Mantle rejects Chat Completions; the source must reshape to the " + "Responses API.", + "Apply the request/response/tool/state reshape deltas before cutover; do not " + "target mantle_openai_chat for GPT-5.x.", + ) + ) + deltas.extend(f_deltas) + + if path == "runtime_converse": + deltas.append( + _delta( + "openai_sdk_to_converse", + "path", + "The OpenAI SDK integration is rewritten to boto3 bedrock-runtime Converse with a " + "Bedrock-native model; streaming moves to ConverseStream event shapes.", + ) + ) + + rationale = [rationale_head] + if model["context_window"] == "unknown" or model["output_token_ceiling"] == "unknown": + rationale.append( + f"{model['display_name']} path is evidenced by the reference, but its context/output " + "limits are unknown in the catalog and must be probed." + ) + rec = _base(workload, "recommended") + rec.update( + { + "source_analysis": source_analysis, + "feature_assessment": feature_assessment, + "primary_model": path_config["model_id"], + "model_identity": _model_identity(model_key, model, path_config), + "api_path": path, + "invocation_model_id": invocation_model_id, + "decision_options": [], + "alternatives": [], + "rationale": rationale, + "blocks": blocks, + "tuning": tuning, + "compatibility": _compatibility(detected, requirements, path, model), + "architecture_impacts": impacts, + "additional_targets": _additional_targets(detected, requirements), + "migration_deltas": deltas, + "evaluation": _evaluation(detected, requirements), + "rollout": { + "strategy": "canary", + "gate": "Compare source and target on the golden set before percentage rollout.", + }, + "verification": _verification( + region, catalog, path, path_config["requires_cris"], invocation_model_id, selected=True + ), + } + ) + return rec diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/model-recommendation-input.json b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/model-recommendation-input.json new file mode 100644 index 00000000..61477901 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/model-recommendation-input.json @@ -0,0 +1,207 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "featureName": { + "enum": [ + "agent_infra", + "assistant_prefill", + "assistants_threads", + "audio_modality", + "budget_tokens", + "citations", + "conversation_state", + "embeddings", + "fallbacks", + "file_search_retrieval", + "files_api", + "image_input_vision", + "images", + "max_tokens", + "max_tokens_headroom", + "message_batches", + "models_api", + "multiple_candidates_n", + "prompt_caching", + "reasoning", + "refusal_handling", + "sampling_parameters", + "sampling_params", + "server_tools", + "streaming", + "structured_output", + "structured_output_json", + "tokenizer_rebaseline", + "tool_or_function_calling", + "tool_use", + "url_sources", + "vector_stores", + "vision", + "web_search" + ] + }, + "source": { + "type": "object", + "required": ["provider", "model_ids", "sdk", "api_surface", "source_paths"], + "properties": { + "provider": { + "enum": [ + "anthropic", + "openai", + "azure_openai", + "google_genai", + "bedrock", + "none", + "unknown" + ] + }, + "model_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "sdk": { + "type": "string" + }, + "api_surface": { + "type": "string" + }, + "source_paths": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "type": "object", + "required": ["schema_version", "region", "primary_unit", "workloads"], + "properties": { + "schema_version": { + "const": 2 + }, + "region": { + "type": "string", + "minLength": 1 + }, + "primary_unit": { + "type": "string", + "minLength": 1 + }, + "workloads": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["workload_id", "source", "requirements"], + "properties": { + "workload_id": { + "type": "string", + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" + }, + "source": { + "$ref": "#/$defs/source" + }, + "requirements": { + "type": "object", + "required": ["priority", "critical_features"], + "properties": { + "priority": { + "enum": ["quality", "balanced", "speed", "cost", "unknown"] + }, + "critical_features": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "preserve_messages_api": { + "type": "boolean" + }, + "newest_anthropic_betas": { + "type": "boolean" + }, + "api_continuity": { + "enum": ["required", "preferred", "not_required", "unknown"] + }, + "preserve_openai_api": { + "type": "boolean" + }, + "uses_n": { + "type": "boolean" + }, + "uses_hosted_state": { + "type": "boolean" + }, + "governance": { + "type": "array", + "uniqueItems": true, + "items": { + "enum": ["guardrails", "invocation_logging", "cloudwatch"] + } + }, + "multi_model_converse": { + "type": "boolean" + }, + "requires_native_payload": { + "type": "boolean" + }, + "preferred_api_path": { + "enum": [ + "mantle_messages", + "mantle_openai_chat", + "mantle_openai_responses", + "runtime_converse", + "runtime_invoke" + ] + }, + "min_context_tokens": { + "type": "integer", + "minimum": 1 + }, + "expected_output_tokens": { + "type": "integer", + "minimum": 1 + }, + "thinking_enabled": { + "type": "boolean" + }, + "data_residency": { + "enum": ["global_allowed", "geo_required", "unknown"] + }, + "cris_geography": { + "enum": ["us", "eu", "jp", "au"] + }, + "inference_profile_id": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "detected_features": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/featureName" + } + }, + "feature_status": { + "type": "object", + "propertyNames": { + "$ref": "#/$defs/featureName" + }, + "additionalProperties": { + "enum": ["detected", "absent", "unknown"] + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/model-recommendation.json b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/model-recommendation.json new file mode 100644 index 00000000..ddf14006 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/model-recommendation.json @@ -0,0 +1,554 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "catalogProvenance": { + "type": "object", + "required": ["provider", "verified_at", "verified_region", "source"], + "properties": { + "provider": { + "type": "string" + }, + "verified_at": { + "type": "string" + }, + "verified_region": { + "type": "string" + }, + "source": { + "type": ["string", "null"] + } + }, + "additionalProperties": false + }, + "apiPath": { + "enum": [ + "mantle_messages", + "mantle_openai_chat", + "mantle_openai_responses", + "runtime_converse", + "runtime_invoke" + ] + }, + "source": { + "type": "object", + "required": ["provider", "model_ids", "sdk", "api_surface", "source_paths"], + "properties": { + "provider": { + "enum": [ + "anthropic", + "openai", + "azure_openai", + "google_genai", + "bedrock", + "none", + "unknown" + ] + }, + "model_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "sdk": { + "type": "string" + }, + "api_surface": { + "type": "string" + }, + "source_paths": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "finding": { + "type": "object", + "required": ["code", "tag", "message", "remediation"], + "properties": { + "code": { + "type": "string" + }, + "tag": { + "enum": ["[BLOCKS]", "[TUNE]"] + }, + "message": { + "type": "string" + }, + "remediation": { + "type": "string" + } + }, + "additionalProperties": false + }, + "delta": { + "type": "object", + "required": ["code", "category", "description"], + "properties": { + "code": { + "type": "string" + }, + "category": { + "enum": ["path", "version", "platform", "feature"] + }, + "description": { + "type": "string" + } + }, + "additionalProperties": false + }, + "candidate": { + "type": "object", + "required": [ + "model_key", + "model", + "api_path", + "invocation_model_id", + "requires_cris", + "reason" + ], + "properties": { + "model_key": { + "type": "string" + }, + "model": { + "type": "string" + }, + "api_path": { + "$ref": "#/$defs/apiPath" + }, + "invocation_model_id": { + "type": ["string", "null"] + }, + "requires_cris": { + "type": "boolean" + }, + "reason": { + "type": "string" + } + }, + "additionalProperties": false + }, + "modelIdentity": { + "type": "object", + "required": [ + "model_key", + "display_name", + "family", + "version", + "context_window", + "output_token_ceiling", + "path_model_id", + "requires_cris" + ], + "properties": { + "model_key": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "family": { + "type": "string" + }, + "version": { + "type": "string" + }, + "context_window": { + "oneOf": [ + { "type": "integer", "minimum": 1 }, + { "const": "unknown" } + ] + }, + "output_token_ceiling": { + "oneOf": [ + { "type": "integer", "minimum": 1 }, + { "const": "unknown" } + ] + }, + "path_model_id": { + "type": "string" + }, + "requires_cris": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "stringList": { + "type": "array", + "items": { + "type": "string" + } + }, + "additionalTarget": { + "type": "object", + "required": ["capability", "status", "candidate", "evidence"], + "properties": { + "capability": { + "type": "string" + }, + "status": { + "enum": ["recommended", "unresolved"] + }, + "candidate": { + "type": ["string", "null"] + }, + "service": { + "type": ["string", "null"] + }, + "evidence": { + "type": "string" + } + }, + "additionalProperties": false + }, + "verification": { + "type": "object", + "required": [ + "region", + "catalog_verified_at", + "verified_at", + "probe_status", + "availability_claim", + "invocation_model_id", + "required_checks" + ], + "properties": { + "region": { + "type": "string" + }, + "catalog_verified_at": { + "type": "string" + }, + "verified_at": { + "type": ["string", "null"] + }, + "probe_status": { + "enum": [ + "not_run", + "passed", + "failed", + "needs_resolution", + "not_applicable" + ] + }, + "availability_claim": { + "enum": [ + "provisional", + "verified", + "unavailable", + "unresolved", + "not_selected" + ] + }, + "invocation_model_id": { + "type": ["string", "null"] + }, + "required_checks": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "recommendation": { + "type": "object", + "required": [ + "workload_id", + "provider_module", + "decision_status", + "source", + "source_analysis", + "feature_assessment", + "primary_model", + "model_identity", + "api_path", + "invocation_model_id", + "decision_options", + "alternatives", + "rationale", + "blocks", + "tuning", + "compatibility", + "architecture_impacts", + "migration_deltas", + "evaluation", + "rollout", + "verification" + ], + "properties": { + "workload_id": { + "type": "string" + }, + "provider_module": { + "enum": ["anthropic", "openai", "generic"] + }, + "decision_status": { + "enum": ["recommended", "decision_required"] + }, + "source": { + "$ref": "#/$defs/source" + }, + "source_analysis": { + "type": "object", + "required": ["detected_version", "target_version", "version_changed"], + "properties": { + "detected_version": { + "type": ["string", "null"] + }, + "target_version": { + "type": ["string", "null"] + }, + "version_changed": { + "type": ["boolean", "null"] + }, + "source_family": { + "type": "string" + }, + "source_api_surface": { + "type": ["string", "null"] + }, + "model_generation_changes": { + "type": ["boolean", "null"] + } + }, + "additionalProperties": false + }, + "feature_assessment": { + "type": "object", + "additionalProperties": { + "enum": ["detected", "absent", "unknown"] + } + }, + "primary_model": { + "type": ["string", "null"] + }, + "model_identity": { + "oneOf": [ + { + "$ref": "#/$defs/modelIdentity" + }, + { + "type": "null" + } + ] + }, + "api_path": { + "oneOf": [ + { + "$ref": "#/$defs/apiPath" + }, + { + "type": "null" + } + ] + }, + "invocation_model_id": { + "type": ["string", "null"] + }, + "decision_options": { + "type": "array", + "items": { + "$ref": "#/$defs/candidate" + } + }, + "alternatives": { + "type": "array", + "items": { + "$ref": "#/$defs/candidate" + } + }, + "rationale": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "blocks": { + "type": "array", + "items": { + "$ref": "#/$defs/finding" + } + }, + "tuning": { + "type": "array", + "items": { + "$ref": "#/$defs/finding" + } + }, + "compatibility": { + "type": "object", + "required": ["native", "portable", "rearchitecture", "unsupported"], + "properties": { + "native": { + "$ref": "#/$defs/stringList" + }, + "portable": { + "$ref": "#/$defs/stringList" + }, + "rearchitecture": { + "$ref": "#/$defs/stringList" + }, + "unsupported": { + "$ref": "#/$defs/stringList" + } + }, + "additionalProperties": false + }, + "architecture_impacts": { + "type": "array", + "items": { + "type": "object", + "required": ["feature", "impact", "recommendation"], + "properties": { + "feature": { + "type": "string" + }, + "impact": { + "type": "string" + }, + "recommendation": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additional_targets": { + "type": "array", + "items": { + "$ref": "#/$defs/additionalTarget" + } + }, + "migration_deltas": { + "type": "array", + "items": { + "$ref": "#/$defs/delta" + } + }, + "evaluation": { + "type": "object", + "required": ["mode", "gates"], + "properties": { + "mode": { + "enum": ["prompt", "trajectory"] + }, + "gates": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "rollout": { + "type": "object", + "required": ["strategy", "gate"], + "properties": { + "strategy": { + "enum": ["canary", "decision_required"] + }, + "gate": { + "type": "string" + } + }, + "additionalProperties": false + }, + "verification": { + "$ref": "#/$defs/verification" + } + }, + "allOf": [ + { + "if": { + "properties": { + "decision_status": { + "const": "recommended" + } + } + }, + "then": { + "properties": { + "primary_model": { + "type": "string" + }, + "model_identity": { + "$ref": "#/$defs/modelIdentity" + }, + "api_path": { + "$ref": "#/$defs/apiPath" + }, + "decision_options": { + "maxItems": 0 + } + } + }, + "else": { + "properties": { + "primary_model": { + "type": "null" + }, + "model_identity": { + "type": "null" + }, + "api_path": { + "type": "null" + }, + "invocation_model_id": { + "type": "null" + }, + "decision_options": { + "minItems": 2 + } + } + } + } + ], + "additionalProperties": false + } + }, + "type": "object", + "required": [ + "schema_version", + "catalog", + "catalog_provenance", + "primary_unit", + "workloads" + ], + "properties": { + "schema_version": { + "const": 2 + }, + "catalog": { + "$ref": "#/$defs/catalogProvenance" + }, + "catalog_provenance": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/catalogProvenance" + } + }, + "primary_unit": { + "type": "string" + }, + "workloads": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/recommendation" + } + } + }, + "additionalProperties": false +} diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/model-verification.json b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/model-verification.json new file mode 100644 index 00000000..1afd3cbb --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/model-verification.json @@ -0,0 +1,120 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "apiPath": { + "enum": [ + "mantle_messages", + "mantle_openai_chat", + "mantle_openai_responses", + "runtime_converse", + "runtime_invoke" + ] + }, + "verification": { + "type": "object", + "required": [ + "workload_id", + "decision_status", + "api_path", + "path_model_id", + "invocation_model_id", + "region", + "status", + "checked_at", + "response_model_id", + "error" + ], + "properties": { + "workload_id": { + "type": "string" + }, + "decision_status": { + "enum": ["recommended", "decision_required"] + }, + "api_path": { + "oneOf": [ + { + "$ref": "#/$defs/apiPath" + }, + { + "type": "null" + } + ] + }, + "path_model_id": { + "type": ["string", "null"] + }, + "invocation_model_id": { + "type": ["string", "null"] + }, + "region": { + "type": "string" + }, + "status": { + "enum": [ + "not_run", + "passed", + "failed", + "needs_resolution", + "not_applicable" + ] + }, + "checked_at": { + "type": ["string", "null"], + "format": "date-time" + }, + "response_model_id": { + "type": ["string", "null"] + }, + "error": { + "oneOf": [ + { + "type": "object", + "required": ["type", "message"], + "properties": { + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "type": "object", + "required": [ + "schema_version", + "recommendation_schema_version", + "generated_at", + "workloads" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "recommendation_schema_version": { + "const": 2 + }, + "generated_at": { + "type": "string", + "format": "date-time" + }, + "workloads": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/verification" + } + } + }, + "additionalProperties": false +} diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/scoring-result.json b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/scoring-result.json index def56aba..23a5b1c0 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/scoring-result.json +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/scoring-result.json @@ -4,7 +4,7 @@ "$defs": { "scoredUnit": { "type": "object", - "required": ["verdict", "scores", "eliminated", "model_recommendation"], + "required": ["verdict", "scores", "eliminated"], "properties": { "verdict": { "enum": ["agentcore", "lambda_microvms", "ecs", "eks", "lambda", "co_recommend", "no_viable_runtime"] @@ -13,16 +13,6 @@ "eliminated": { "type": "object", "additionalProperties": { "type": "string" } }, "deployment_model": { "type": ["string", "null"], "enum": ["harness", "framework_on_runtime", null] }, "agentcore_services": { "type": "array", "items": { "type": "string" } }, - "model_recommendation": { - "type": "object", - "required": ["model", "reasoning"], - "properties": { - "model": { "type": "string" }, - "reasoning": { "type": "string" }, - "migration_from": { "type": "string" }, - "pricing_note": { "type": "string" } - } - }, "assumptions_used": { "type": "array", "items": { "type": "string" } }, "warnings": { "type": "array", "items": { "type": "string" } }, "co_recommend": { "type": "array", "items": { "type": "string" } }, @@ -37,7 +27,6 @@ "eliminated", "deployment_model", "agentcore_services", - "model_recommendation", "assumptions_used", "warnings", "units" @@ -50,16 +39,6 @@ "eliminated": { "type": "object", "additionalProperties": { "type": "string" } }, "deployment_model": { "type": ["string", "null"], "enum": ["harness", "framework_on_runtime", null] }, "agentcore_services": { "type": "array", "items": { "type": "string" } }, - "model_recommendation": { - "type": "object", - "required": ["model", "reasoning"], - "properties": { - "model": { "type": "string" }, - "reasoning": { "type": "string" }, - "migration_from": { "type": "string" }, - "pricing_note": { "type": "string" } - } - }, "assumptions_used": { "type": "array", "items": { "type": "string" } }, "warnings": { "type": "array", "items": { "type": "string" } }, "co_recommend": { "type": "array", "items": { "type": "string" } }, diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/seed.json b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/seed.json new file mode 100644 index 00000000..5eccaab2 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/schemas/seed.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "agent-advisor run seed", + "description": "Machine-readable answers for a non-interactive run. Supplies the dimensions Clarify would otherwise ask a human for, so the deterministic scoring engine receives identical input on every run. Prose (CLAUDE.md) describes context; this file supplies enum values. Every key is optional — a dimension absent here falls through to prose, then to code detection, then to an `assumed` value that MUST be recorded in UNANSWERED.md.", + "type": "object", + "additionalProperties": false, + "properties": { + "entry_point": { "enum": ["build_scratch", "build_deploy", "migrate", "add_capabilities"] }, + "audience": { "enum": ["technical", "business"] }, + "system": { "$ref": "#/definitions/systemDims" }, + "units": { + "type": "object", + "description": "Per-unit dimensions keyed by unit id. Unit ids must match the ids Discover/Clarify assign.", + "additionalProperties": { "$ref": "#/definitions/unitDims" } + }, + "gates": { + "type": "object", + "additionalProperties": false, + "description": "Answers for the optional gated stages. A gate absent here is DECLINED — never accepted by default.", + "properties": { + "migration_plan": { "enum": ["accept", "decline"] }, + "poc": { "enum": ["accept", "decline"] } + } + }, + "poc_mode": { + "enum": ["deliverables", "assisted_build"], + "description": "Gate 2b. Absent means `deliverables` — an assisted build in a real account requires an explicit opt-in here." + }, + "probe": { + "enum": ["accept", "decline"], + "description": "Live Bedrock model probe in the target account. Absent means decline; verification.probe_status stays not_run." + }, + "chosen_runtime": { + "enum": ["agentcore", "ecs", "eks", "lambda", "lambda_microvms"], + "description": "Tie-break for a co_recommend verdict, normally asked in Confirm. Ignored when the verdict is not a tie." + } + }, + "definitions": { + "systemDims": { + "type": "object", + "additionalProperties": false, + "properties": { + "multi_cloud": { "enum": ["yes", "no", "unknown"] }, + "compliance": { + "type": "array", + "items": { "enum": ["none", "soc2", "hipaa", "pci", "fedramp", "gdpr", "ccpa"] } + }, + "region": { + "type": "object", + "additionalProperties": false, + "properties": { + "scope": { "enum": ["single", "multi", "global", "unknown"] }, + "regions": { "type": "array", "items": { "type": "string" } } + } + }, + "existing_cluster": { "enum": ["eks", "ecs", "none", "unknown"] }, + "platform_fit": { "enum": ["ecs", "eks", "lambda", "none", "unknown"] }, + "ops_preference": { "enum": ["minimal", "moderate", "full_control", "unknown"] } + } + }, + "unitDims": { + "type": "object", + "additionalProperties": false, + "properties": { + "workload_class": { "type": "string" }, + "session_duration": { "enum": ["under_15min", "15min_to_8hr", "over_8hr", "unknown"] }, + "traffic_pattern": { "enum": ["bursty", "steady", "idle", "unknown"] }, + "session_state": { "enum": ["stateless", "stateful", "hitl", "unknown"] }, + "isolation": { "enum": ["required", "nice_to_have", "not_needed", "unknown"] }, + "memory_needs": { "enum": ["cross_session", "session_only", "none", "unknown"] }, + "ops_preference": { "enum": ["minimal", "moderate", "full_control", "unknown"] }, + "compute_tier": { "enum": ["light", "heavy_non_gpu", "gpu", "unknown"] }, + "idle_resume": { "enum": ["process_level", "filesystem", "none", "unknown"] }, + "launch_concurrency": { "enum": ["high", "moderate", "low", "unknown"] }, + "multi_agent": { "enum": ["yes", "no", "unknown"] }, + "deployment_preference": { "enum": ["harness", "framework", "either", "unknown"] }, + "framework": { "enum": ["strands", "langgraph", "crewai", "custom", "none", "unknown"] }, + "existing_cluster": { "enum": ["eks", "ecs", "none", "unknown"] }, + "multi_cloud": { "enum": ["yes", "no", "unknown"] }, + "platform_fit": { "enum": ["ecs", "eks", "lambda", "none", "unknown"] } + } + } + } +} diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/scoring.py b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/scoring.py index cab4161c..27ccfb9d 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/scoring.py +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/scoring.py @@ -40,9 +40,6 @@ DEFAULTS = { **{dim: "unknown" for dim in DIMENSIONS}, "compliance": ["none"], - "model_priority": "unknown", - "model_features": "unknown", - "current_model": "unknown", "region": "unknown", } @@ -149,95 +146,6 @@ def add(name): return services -# Q16 priority baseline. Model choice is independent of runtime scoring. -_MODEL_PRIORITY = { - "quality": ("claude_sonnet_4_6", "Best quality for agentic workloads"), - "balanced": ("claude_sonnet_4_6", "Balanced quality, speed, and cost"), - "speed": ("claude_haiku_4_5", "Fastest response time"), - "cost": ("claude_haiku_4_5", "Lowest cost per token"), - "unknown": ("claude_sonnet_4_6", "Default for agentic workloads"), -} - -# Q17 specialized-feature HARD override (beats priority). Coarse family mapping -# only — no pricing. Active models only, per migration-to-aws ai-model-lifecycle.md -# (the drift test in test_scoring.py locks this against the source lifecycle file). -# Each value: (model, reasoning, alternates). -_FEATURE_OVERRIDE = { - "tool_use": ("claude_sonnet_4_6", "Best-in-class tool use on Bedrock", []), - "long_context": ( - "llama_4_scout", - "Ultra-long context (10M native window); Claude Sonnet 4.6 for shorter long-context", - ["claude_sonnet_4_6"]), - "extended_thinking": ( - "claude_sonnet_4_6_thinking", "Extended thinking for deep reasoning", []), - "rag": ( - "claude_sonnet_4_6", - "Strong retrieval + reasoning; pair with Bedrock Knowledge Bases + Titan Embeddings", - ["titan_embed_v2"]), - "multimodal": ( - "claude_sonnet_4_6", - "Vision understanding; add a Stability AI model if you also generate images", - ["stability_image_core"]), - "image_generation": ( - "stability_image_core", - "Image generation (Stable Image Core for cost, Ultra for quality) — a separate " - "capability, not a text-model swap; see the llm-to-bedrock skill for integration", - ["stability_image_ultra"]), - "speech": ( - "nova_2_sonic", - "Speech-to-speech (Transcribe/Polly for one-directional STT/TTS) — a separate " - "capability, not a text-model swap; see the llm-to-bedrock skill for integration", - []), - "embedding": ( - "titan_embed_v2", - "Text embeddings (Titan Embeddings v2)", []), -} - -# Coarse source->family mapping for migrate (baseline only; feature override wins). -_MIGRATE_FAMILY = { - "gpt4": "claude_sonnet_4_6", "gpt4o": "claude_sonnet_4_6", - "gemini_flash": "nova_lite", "gemini_pro": "claude_sonnet_4_6", - "claude": "claude_sonnet_4_6", "other": "claude_sonnet_4_6", -} - -_PRICING_NOTE = ("Coarse family mapping only — see migration-to-aws for detailed " - "model pricing and TCO comparison.") - - -def _select_model(answers): - priority = answers.get("model_priority", "unknown") - feature = answers.get("model_features", "unknown") - - # Priority baseline. - model, reasoning = _MODEL_PRIORITY.get(priority, _MODEL_PRIORITY["unknown"]) - rec = {"model": model, "reasoning": reasoning, "alternates": []} - - # Q17 feature HARD override (beats priority and, later, migrate family). - override = _FEATURE_OVERRIDE.get(feature) if feature not in ("none", "unknown") else None - if override: - rec["model"], rec["reasoning"], alternates = override - rec["alternates"] = list(alternates) - # Cost/speed conflict advisory: the specialized model may not be cheapest/fastest. - if priority in ("cost", "speed"): - rec["reasoning"] += ( - f" (Feature override applied over your '{priority}' priority — this " - "specialized model may not be the lowest-cost/fastest option; see pricing " - "downstream.)") - - # Migrate: record source, and only fall to family mapping when no feature override. - if answers.get("_entry_point") == "migrate": - current = answers.get("current_model", "unknown") - if current in _MIGRATE_FAMILY: - rec["migration_from"] = current - if not override: - rec["model"] = _MIGRATE_FAMILY[current] - rec["pricing_note"] = _PRICING_NOTE - - if not rec["alternates"]: - del rec["alternates"] - return rec - - def _collect_assumptions(raw_answers): out = [] for dim in DIMENSIONS: @@ -291,7 +199,6 @@ def score(input_data, profiles=None): "eliminated": eliminated, "deployment_model": deployment_model, "agentcore_services": _select_agentcore_services(answers), - "model_recommendation": _select_model(answers), "assumptions_used": _collect_assumptions(raw_answers), "warnings": _collect_warnings(answers, verdict, co_recommend), } diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_model_recommendation.py b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_model_recommendation.py new file mode 100644 index 00000000..2e3a87df --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_model_recommendation.py @@ -0,0 +1,369 @@ +import json +import pathlib + +import jsonschema +import pytest + +import model_recommendation + + +VERSION_SCAN_FEATURES = { + "assistant_prefill", + "budget_tokens", + "max_tokens_headroom", + "refusal_handling", + "sampling_parameters", + "tokenizer_rebaseline", +} + + +def _workload(**overrides): + workload = { + "workload_id": "support-agent", + "source": { + "provider": "anthropic", + "model_ids": ["claude-3-7-sonnet-latest"], + "sdk": "anthropic", + "api_surface": "messages", + "source_paths": ["src/agent.py"], + }, + "requirements": { + "priority": "balanced", + "critical_features": ["tool_use"], + }, + "detected_features": [], + } + for key, value in overrides.items(): + if key in {"source", "requirements"}: + workload[key].update(value) + else: + workload[key] = value + return workload + + +def _input(workload=None): + return { + "schema_version": 2, + "region": "us-east-1", + "primary_unit": "support-agent", + "workloads": [workload or _workload()], + } + + +def _recommend(workload=None): + return model_recommendation.recommend(_input(workload))["workloads"][ + "support-agent" + ] + + +def _codes(items): + return {item["code"] for item in items} + + +def test_default_anthropic_messages_prefers_mantle_and_sonnet5(): + # Sonnet 5 (launched 2026-06-30) supports Mantle Messages, so the balanced + # priority no longer has to escalate to Opus for Messages continuity. + rec = _recommend() + + assert rec["decision_status"] == "recommended" + assert rec["primary_model"] == "anthropic.claude-sonnet-5" + assert rec["api_path"] == "mantle_messages" + assert rec["invocation_model_id"] == "anthropic.claude-sonnet-5" + assert rec["model_identity"]["path_model_id"] == rec["primary_model"] + assert rec["verification"]["probe_status"] == "not_run" + + +def test_cost_priority_on_messages_uses_clean_mantle_haiku_id(): + rec = _recommend(_workload(requirements={"priority": "cost"})) + + assert rec["api_path"] == "mantle_messages" + assert rec["primary_model"] == "anthropic.claude-haiku-4-5" + assert rec["model_identity"]["requires_cris"] is False + + +def test_governance_selects_runtime_converse_when_messages_is_a_preference(): + rec = _recommend( + _workload( + requirements={"governance": ["guardrails", "invocation_logging"]} + ) + ) + + assert rec["decision_status"] == "recommended" + assert rec["api_path"] == "runtime_converse" + assert rec["primary_model"] == "anthropic.claude-sonnet-5" + + +def test_required_messages_and_governance_require_user_decision(): + rec = _recommend( + _workload( + requirements={ + "preserve_messages_api": True, + "governance": ["guardrails"], + } + ) + ) + + assert rec["decision_status"] == "decision_required" + assert rec["primary_model"] is None + assert rec["api_path"] is None + assert {option["api_path"] for option in rec["decision_options"]} == { + "mantle_messages", + "runtime_converse", + } + assert "model_path_decision_required" in _codes(rec["blocks"]) + + +def test_conflicting_paths_without_catalog_candidates_are_rejected(): + workload = _workload( + requirements={ + "preserve_messages_api": True, + "governance": ["guardrails"], + "min_context_tokens": 2000000, + } + ) + + with pytest.raises(ValueError, match="no candidate for every conflicting"): + _recommend(workload) + + +def test_native_payload_selects_runtime_invoke(): + rec = _recommend( + _workload(requirements={"requires_native_payload": True}) + ) + + assert rec["api_path"] == "runtime_invoke" + + +def test_context_and_output_limits_filter_candidates(): + rec = _recommend( + _workload( + requirements={ + "priority": "cost", + "min_context_tokens": 1000000, + "expected_output_tokens": 100000, + } + ) + ) + + assert rec["model_identity"]["model_key"] == "claude_sonnet_5" + assert all( + option["model_key"] != "claude_haiku_4_5" + for option in rec["alternatives"] + ) + + +def test_thinking_requirement_filters_haiku(): + rec = _recommend( + _workload( + requirements={ + "priority": "cost", + "thinking_enabled": True, + } + ) + ) + + assert rec["model_identity"]["model_key"] == "claude_sonnet_5" + + +def test_source_version_hop_requires_explicit_feature_scan(): + rec = _recommend() + + assert rec["source_analysis"] == { + "detected_version": "3.7", + "target_version": "5.0", + "version_changed": True, + } + assert "claude_version_hop" in _codes(rec["migration_deltas"]) + assert "version_scan_incomplete" in _codes(rec["blocks"]) + assert { + feature + for feature, status in rec["feature_assessment"].items() + if status == "unknown" + } == VERSION_SCAN_FEATURES + + +def test_explicit_absent_status_clears_version_scan_block(): + workload = _workload( + feature_status={feature: "absent" for feature in VERSION_SCAN_FEATURES} + ) + rec = _recommend(workload) + + assert "version_scan_incomplete" not in _codes(rec["blocks"]) + + +def test_detected_version_features_emit_blocks_and_tuning(): + rec = _recommend( + _workload( + detected_features=[ + "budget_tokens", + "sampling_parameters", + "assistant_prefill", + "refusal_handling", + "tokenizer_rebaseline", + "max_tokens_headroom", + ] + ) + ) + + assert { + "budget_tokens_removed", + "sampling_parameters_removed", + "assistant_prefill_removed", + }.issubset(_codes(rec["blocks"])) + assert { + "refusal_handling", + "tokenizer_rebaseline", + "max_tokens_headroom", + }.issubset(_codes(rec["tuning"])) + + +@pytest.mark.parametrize( + "requirements,expected_path", + [ + ({"preserve_messages_api": True}, "mantle_messages"), + ({"governance": ["guardrails"]}, "runtime_converse"), + ], +) +def test_structured_output_uses_portable_forced_tool_guidance( + requirements, expected_path +): + rec = _recommend( + _workload( + requirements=requirements, + detected_features=["structured_output"], + ) + ) + + assert rec["api_path"] == expected_path + assert "structured_output_portable_pattern" in _codes(rec["blocks"]) + assert "structured_output" in _codes(rec["migration_deltas"]) + finding = next( + item + for item in rec["blocks"] + if item["code"] == "structured_output_portable_pattern" + ) + assert "forced tool without strict" in finding["remediation"] + + +def test_structured_output_and_citations_emit_conflict(): + rec = _recommend( + _workload(detected_features=["structured_output", "citations"]) + ) + + assert "structured_output_citations_conflict" in _codes(rec["blocks"]) + + +def test_agent_features_produce_architecture_and_trajectory_requirements(): + rec = _recommend( + _workload( + requirements={"critical_features": ["agentic", "tool_use"]}, + detected_features=["agent_infra", "server_tools"], + ) + ) + + assert rec["evaluation"]["mode"] == "trajectory" + assert {item["feature"] for item in rec["architecture_impacts"]} == { + "agent_infra", + "server_tools", + } + assert rec["compatibility"]["rearchitecture"] == [ + "agent_infra", + "server_tools", + ] + + +@pytest.mark.parametrize( + "requirements,expected", + [ + ({"data_residency": "unknown"}, None), + ( + {"data_residency": "global_allowed"}, + "global.anthropic.claude-sonnet-5", + ), + ( + {"data_residency": "geo_required", "cris_geography": "eu"}, + "eu.anthropic.claude-sonnet-5", + ), + ( + { + "inference_profile_id": "arn:aws:bedrock:us-east-1:123:inference-profile/custom" + }, + "arn:aws:bedrock:us-east-1:123:inference-profile/custom", + ), + ], +) +def test_runtime_cris_resolution(requirements, expected): + requirements["governance"] = ["guardrails"] + rec = _recommend(_workload(requirements=requirements)) + + assert rec["api_path"] == "runtime_converse" + assert rec["invocation_model_id"] == expected + assert rec["verification"]["invocation_model_id"] == expected + + +def test_future_provider_is_explicitly_provisional(): + # OpenAI now has a real provider module; Azure OpenAI remains the pending case. + rec = _recommend( + _workload( + source={ + "provider": "azure_openai", + "model_ids": ["gpt-5.4"], + "sdk": "azure_openai", + "api_surface": "responses", + } + ) + ) + + assert rec["provider_module"] == "generic" + assert "provider_module_pending" in _codes(rec["blocks"]) + + +def test_duplicate_workload_ids_are_rejected(): + data = _input() + data["workloads"].append(_workload()) + + with pytest.raises(ValueError, match="duplicate workload_id"): + model_recommendation.recommend(data) + + +def test_input_and_output_match_schemas(): + scripts = pathlib.Path(model_recommendation.__file__).parent + input_data = _input() + result = model_recommendation.recommend(input_data) + + input_schema = json.loads( + (scripts / "schemas" / "model-recommendation-input.json").read_text() + ) + output_schema = json.loads( + (scripts / "schemas" / "model-recommendation.json").read_text() + ) + jsonschema.validate(input_data, input_schema) + jsonschema.validate(result, output_schema) + + +def test_catalog_records_path_specific_ids_and_limits(): + catalog = model_recommendation.load_catalog() + + assert catalog["verified_at"] == "2026-07-21" + assert catalog["verified_region"] == "us-east-1" + assert ( + catalog["models"]["claude_sonnet_5"]["paths"]["mantle_messages"][ + "available" + ] + is True + ) + assert ( + catalog["models"]["claude_haiku_4_5"]["paths"]["mantle_messages"][ + "model_id" + ] + == "anthropic.claude-haiku-4-5" + ) + assert ( + catalog["models"]["claude_haiku_4_5"]["paths"]["runtime_converse"][ + "model_id" + ] + == "anthropic.claude-haiku-4-5-20251001-v1:0" + ) + assert ( + catalog["models"]["claude_opus_4_8"]["output_token_ceiling"] == 128000 + ) diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_openai_model_recommendation.py b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_openai_model_recommendation.py new file mode 100644 index 00000000..3ad4863a --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_openai_model_recommendation.py @@ -0,0 +1,595 @@ +"""Tests for the OpenAI-to-Bedrock provider module (handoff Section 11 matrix).""" + +import json +import pathlib + +import jsonschema +import pytest + +import model_recommendation +import openai_model_recommendation as oai + + +SCRIPTS = pathlib.Path(model_recommendation.__file__).parent +OPENAI_CATALOG = model_recommendation.load_openai_catalog() + + +def _workload(**overrides): + workload = { + "workload_id": "chat-svc", + "source": { + "provider": "openai", + "model_ids": ["gpt-5.4"], + "sdk": "openai", + "api_surface": "responses", + "source_paths": ["src/app.py"], + }, + "requirements": { + "priority": "balanced", + "critical_features": [], + }, + "detected_features": [], + } + for key, value in overrides.items(): + if key in {"source", "requirements"}: + workload[key].update(value) + else: + workload[key] = value + return workload + + +def _recommend(workload=None): + return oai.recommend_openai_workload( + workload or _workload(), "us-east-2", OPENAI_CATALOG + ) + + +def _codes(items): + return {item["code"] for item in items} + + +def _delta_codes(rec): + return {d["code"] for d in rec["migration_deltas"]} + + +# --- 11.1 Provider and path selection ------------------------------------- + +def test_responses_source_with_continuity_recommends_mantle_responses(): + rec = _recommend( + _workload(requirements={"api_continuity": "required"}) + ) + assert rec["provider_module"] == "openai" + assert rec["decision_status"] == "recommended" + assert rec["api_path"] == "mantle_openai_responses" + assert rec["primary_model"] == "openai.gpt-5.6-sol" + + +def test_chat_completions_gpt5_target_selects_responses_not_chat(): + rec = _recommend( + _workload( + source={"api_surface": "chat_completions"}, + requirements={"api_continuity": "required"}, + ) + ) + assert rec["api_path"] == "mantle_openai_responses" + assert rec["api_path"] != "mantle_openai_chat" + assert "chat_completions_to_responses_required" in _codes(rec["blocks"]) + + +def test_chat_completions_case_includes_full_reshape_deltas(): + rec = _recommend( + _workload( + source={"api_surface": "chat_completions"}, + requirements={"api_continuity": "required"}, + detected_features=["tool_or_function_calling", "conversation_state"], + ) + ) + codes = _delta_codes(rec) + assert "chat_to_responses_request" in codes + assert "chat_to_responses_response" in codes + assert "chat_to_responses_tools" in codes + assert "chat_to_responses_state" in codes + + +def test_governance_selects_runtime_converse(): + rec = _recommend( + _workload(requirements={"governance": ["guardrails", "invocation_logging"]}) + ) + assert rec["decision_status"] == "recommended" + assert rec["api_path"] == "runtime_converse" + assert rec["primary_model"] == "anthropic.claude-sonnet-5" + + +def test_continuity_plus_runtime_only_returns_decision_required(): + rec = _recommend( + _workload( + requirements={ + "api_continuity": "required", + "governance": ["guardrails"], + } + ) + ) + assert rec["decision_status"] == "decision_required" + assert rec["primary_model"] is None + assert rec["api_path"] is None + assert {o["api_path"] for o in rec["decision_options"]} == { + "mantle_openai_responses", + "runtime_converse", + } + assert "model_path_decision_required" in _codes(rec["blocks"]) + + +def test_decision_options_explain_tradeoffs(): + rec = _recommend( + _workload( + requirements={"api_continuity": "required", "governance": ["guardrails"]} + ) + ) + reasons = {o["api_path"]: o["reason"] for o in rec["decision_options"]} + assert "OpenAI SDK" in reasons["mantle_openai_responses"] + assert "Bedrock-native" in reasons["runtime_converse"] + + +# --- 11.2 Model-generation analysis --------------------------------------- + +def test_gpt4_to_gpt5_emits_model_generation_finding(): + rec = _recommend( + _workload( + source={"model_ids": ["gpt-4o"], "api_surface": "chat_completions"}, + requirements={"api_continuity": "required"}, + ) + ) + assert rec["source_analysis"]["source_family"] == "legacy" + assert rec["source_analysis"]["model_generation_changes"] is True + assert "model_generation_hop" in _delta_codes(rec) + + +def test_oseries_source_is_reasoning_not_legacy(): + rec = _recommend(_workload(source={"model_ids": ["o3-mini"]})) + assert rec["source_analysis"]["source_family"] == "reasoning" + assert rec["source_analysis"]["model_generation_changes"] is False + + +def test_opaque_deployment_name_stays_unknown(): + assert oai.detect_family("prod-default") == "unknown" + rec = _recommend(_workload(source={"model_ids": ["prod-default"]})) + assert rec["source_analysis"]["source_family"] == "unknown" + + +def test_reasoning_workload_requires_output_headroom(): + rec = _recommend() # gpt-5.4 reasoning + assert "reasoning_token_headroom" in _codes(rec["tuning"]) + + +def test_reasoning_headroom_is_a_starting_heuristic_not_a_guarantee(): + rec = _recommend() + msg = next( + f["remediation"] for f in rec["tuning"] if f["code"] == "reasoning_token_headroom" + ) + assert "2.5x" in msg + assert "STARTING heuristic" in msg or "not a guaranteed" in msg + + +# --- 11.3 Parameter and API behavior -------------------------------------- + +def test_sampling_is_target_derived_not_source_derived(): + # G01: the Mantle target is openai.gpt-5.4 (accepts sampling), so BOTH an + # early-GPT-5 source and a GPT-5.4 source get the same target-derived finding. + early = _recommend(_workload(source={"model_ids": ["gpt-5.1"]})) + later = _recommend(_workload(source={"model_ids": ["gpt-5.4"]})) + assert "sampling_params_accepted" in _codes(early["tuning"]) + assert "sampling_params_accepted" in _codes(later["tuning"]) + # G10: penalties/logprobs/stop are called out as rejected separately. + assert "sampling_penalties_rejected" in _codes(early["tuning"]) + + +def test_gpt54_does_not_get_anthropic_sampling_removal(): + rec = _recommend(_workload(source={"model_ids": ["gpt-5.4"]})) + assert "sampling_parameters_removed" not in _codes(rec["blocks"]) + assert "sampling_params_accepted" in _codes(rec["tuning"]) + + +def test_n_usage_emits_repeated_call_requirement(): + rec = _recommend(_workload(requirements={"uses_n": True})) + assert "responses_no_n" in _delta_codes(rec) + + +def test_tool_results_emit_function_call_output_delta(): + rec = _recommend(_workload(detected_features=["tool_or_function_calling"])) + assert "tool_result_shape" in _delta_codes(rec) + + +def test_structured_output_emits_text_format_mapping(): + rec = _recommend(_workload(detected_features=["structured_output_json"])) + assert "structured_output_text_format" in _delta_codes(rec) + + +def test_multiturn_state_emits_previous_response_or_app_state(): + rec = _recommend(_workload(requirements={"uses_hosted_state": True})) + assert "conversation_state_ownership" in _delta_codes(rec) + + +# --- 11.4 Architecture impacts -------------------------------------------- + +def _impact_features(rec): + return {a["feature"] for a in rec["architecture_impacts"]} + + +def test_hosted_web_search_not_reported_as_live_native_search(): + rec = _recommend(_workload(detected_features=["web_search"])) + impacts = {a["feature"]: a["impact"] for a in rec["architecture_impacts"]} + assert "web_search" in impacts + assert "passthrough" in impacts["web_search"].lower() + + +def test_file_search_and_vector_stores_produce_retrieval_impacts(): + rec = _recommend( + _workload(detected_features=["file_search_retrieval", "files_api", "vector_stores"]) + ) + assert {"file_search_retrieval", "files_api", "vector_stores"} <= _impact_features(rec) + + +def test_assistants_threads_produce_state_impacts(): + rec = _recommend(_workload(detected_features=["assistants_threads"])) + assert "assistants_threads" in _impact_features(rec) + + +def test_modalities_are_separate_capability_paths(): + rec = _recommend( + _workload(detected_features=["audio_modality", "embeddings", "images"]) + ) + assert {"audio_modality", "embeddings", "images"} <= _impact_features(rec) + + +def test_agentic_workload_uses_trajectory_evaluation(): + rec = _recommend(_workload(detected_features=["tool_or_function_calling"])) + assert rec["evaluation"]["mode"] == "trajectory" + + +# --- 11.5 Catalog and verification ---------------------------------------- + +def test_unknown_limits_do_not_pass_hard_numeric_requirement(): + rec = _recommend(_workload(requirements={"min_context_tokens": 400000})) + assert rec["decision_status"] == "decision_required" + assert "unverified_capacity" in _codes(rec["blocks"]) + + +def test_no_aws_account_leaves_probe_not_run_and_provisional(): + rec = _recommend() + assert rec["verification"]["probe_status"] == "not_run" + assert rec["verification"]["availability_claim"] == "provisional" + + +def test_decision_required_verification_is_not_applicable(): + rec = _recommend( + _workload(requirements={"api_continuity": "required", "governance": ["guardrails"]}) + ) + assert rec["verification"]["probe_status"] == "not_applicable" + assert rec["verification"]["availability_claim"] == "not_selected" + + +def test_catalog_limits_are_sourced_or_unknown(): + # Limits must be positive sourced integers or the explicit string "unknown" — + # never a fabricated placeholder. Every model must cite its capability source. + for model in OPENAI_CATALOG["models"].values(): + for limit in (model["context_window"], model["output_token_ceiling"]): + assert limit == "unknown" or (isinstance(limit, int) and limit > 0) + assert model.get("capability_source") + + +# --- Mixed provider provenance (via orchestrator) ------------------------- + +def test_mixed_provider_run_keeps_catalog_provenance(): + data = { + "schema_version": 2, + "region": "us-east-2", + "primary_unit": "claude-agent", + "workloads": [ + { + "workload_id": "claude-agent", + "source": { + "provider": "anthropic", + "model_ids": ["claude-3-7-sonnet-latest"], + "sdk": "anthropic", + "api_surface": "messages", + "source_paths": ["a.py"], + }, + "requirements": {"priority": "balanced", "critical_features": ["tool_use"]}, + "detected_features": [], + }, + { + "workload_id": "openai-svc", + "source": { + "provider": "openai", + "model_ids": ["gpt-5.4"], + "sdk": "openai", + "api_surface": "responses", + "source_paths": ["b.py"], + }, + "requirements": {"priority": "balanced", "critical_features": [], "api_continuity": "required"}, + "detected_features": [], + }, + ], + } + out = model_recommendation.recommend(data) + assert out["workloads"]["claude-agent"]["provider_module"] == "anthropic" + assert out["workloads"]["openai-svc"]["provider_module"] == "openai" + assert out["catalog_provenance"]["claude-agent"]["provider"] == "anthropic" + assert out["catalog_provenance"]["openai-svc"]["provider"] == "openai" + # Output validates against the schema. + schema = json.loads((SCRIPTS / "schemas" / "model-recommendation.json").read_text()) + jsonschema.validate(out, schema) + + +def test_openai_output_validates_against_schema(): + data = { + "schema_version": 2, + "region": "us-east-2", + "primary_unit": "chat-svc", + "workloads": [_workload(requirements={"api_continuity": "required"})], + } + schemas = SCRIPTS / "schemas" + jsonschema.validate( + data, json.loads((schemas / "model-recommendation-input.json").read_text()) + ) + out = model_recommendation.recommend(data) + jsonschema.validate( + out, json.loads((schemas / "model-recommendation.json").read_text()) + ) + + +# --- Regression tests for the Codex gap review (G01-G10) ------------------- + +def _delta_by_code(rec, code): + return next((d for d in rec["migration_deltas"] if d["code"] == code), None) + + +def test_g01_runtime_target_is_not_reported_as_gpt5(): + # A Bedrock-native (Nova) target must not carry target_version gpt-5.x. + rec = _recommend(_workload(requirements={"governance": ["guardrails"]})) + assert rec["api_path"] == "runtime_converse" + assert rec["source_analysis"]["target_version"] == "5" # Claude Sonnet 5, not gpt-5.x + assert rec["source_analysis"]["target_version"] != "gpt-5.x" + + +def test_g01_same_version_source_and_target_not_marked_changed(): + # gpt-5.6 source -> openai.gpt-5.6-sol target: version did not change. + rec = _recommend(_workload(source={"model_ids": ["gpt-5.6"]})) + assert rec["source_analysis"]["target_version"] == "5.6" + assert rec["source_analysis"]["version_changed"] is False + + +def test_g01_legacy_source_to_reasoning_target_marks_generation_change(): + rec = _recommend( + _workload(source={"model_ids": ["gpt-4o"], "api_surface": "chat_completions"}, + requirements={"api_continuity": "required"}) + ) + assert rec["source_analysis"]["model_generation_changes"] is True + assert rec["source_analysis"]["version_changed"] is True + + +def test_g02_runtime_selection_fails_closed_on_unproven_capability(): + # No runtime_converse candidate has streaming evidence in the catalog, so a + # workload requiring it must NOT get a silent native claim — it fails closed. + rec = _recommend( + _workload( + requirements={"governance": ["guardrails"], "critical_features": ["streaming"]} + ) + ) + assert rec["decision_status"] == "decision_required" + assert "unverified_capability" in _codes(rec["blocks"]) + + +def test_g02_native_only_lists_catalog_evidenced_features(): + # Detected structured output is only feature-probed on gpt-5.4, so the engine + # falls back past gpt-5.6-sol (no structured-output evidence) to gpt-5.4 and + # native lists exactly what that catalog entry supports. + rec = _recommend( + _workload(detected_features=["tool_or_function_calling", "structured_output_json"]) + ) + assert rec["decision_status"] == "recommended" + assert rec["primary_model"] == "openai.gpt-5.4" # evidence-driven fallback + assert set(rec["compatibility"]["native"]) == { + "tool_or_function_calling", + "structured_output_json", + } + + +def test_g02_vision_requirement_selects_the_evidenced_candidate(): + # image_input_vision is evidenced on gpt-5.6-sol (model card) but not on + # gpt-5.5/terra/luna/5.4 — the engine must select the evidenced candidate, + # never claim native on an unevidenced one. + rec = _recommend( + _workload( + requirements={"critical_features": ["image_input_vision"]}, + ) + ) + assert rec["decision_status"] == "recommended" + assert rec["primary_model"] == "openai.gpt-5.6-sol" + assert "image_input_vision" in rec["compatibility"]["native"] + + +def test_g03_mantle_keeps_typed_responses_parse(): + rec = _recommend(_workload(detected_features=["structured_output_json"])) + delta = _delta_by_code(rec, "structured_output_text_format") + assert delta is not None + assert "responses.parse" in delta["description"] + assert "no direct" not in delta["description"].lower() + + +def test_g04_mantle_state_offers_hosted_and_manual_modes(): + rec = _recommend(_workload(requirements={"uses_hosted_state": True})) + delta = _delta_by_code(rec, "conversation_state_ownership") + assert delta is not None + assert "store=True" in delta["description"] + assert "store=False" in delta["description"] + + +def test_g08_feature_status_unknown_blocks_readiness(): + rec = _recommend( + _workload( + requirements={"critical_features": ["structured_output_json"]}, + feature_status={"structured_output_json": "unknown"}, + ) + ) + assert "feature_scan_incomplete" in _codes(rec["blocks"]) + + +def test_g08_feature_assessment_is_populated(): + rec = _recommend( + _workload( + detected_features=["tool_or_function_calling"], + feature_status={"web_search": "absent"}, + ) + ) + assert rec["feature_assessment"]["tool_or_function_calling"] == "detected" + assert rec["feature_assessment"]["web_search"] == "absent" + + +def test_g06_separate_modalities_emit_additional_targets(): + rec = _recommend( + _workload(detected_features=["embeddings", "audio_modality", "images"]) + ) + targets = {t["capability"]: t for t in rec["additional_targets"]} + assert set(targets) == {"embeddings", "audio_modality", "images"} + # Unknown target model stays unresolved with a named service, never a fake ID. + for cap, t in targets.items(): + assert t["status"] == "unresolved" + assert t["candidate"] is None + assert t["service"] + + +def test_g06_no_modalities_means_empty_additional_targets(): + rec = _recommend(_workload(detected_features=["tool_or_function_calling"])) + assert rec["additional_targets"] == [] + + +# --- Regression tests for the consistency re-review (N01-N04, G08 enforcement) --- + +import json as _json # noqa: E402 +import pathlib as _pathlib # noqa: E402 + +_OUT_SCHEMA = _json.loads( + (SCRIPTS / "schemas" / "model-recommendation.json").read_text() +) + + +def _full_recommend(workload): + data = { + "schema_version": 2, + "region": "us-east-2", + "primary_unit": workload["workload_id"], + "workloads": [workload], + } + return model_recommendation.recommend(data) + + +def test_n01_decision_required_openai_output_is_schema_valid(): + # A continuity/governance conflict is decision_required; the full artifact must + # validate (model_generation_changes null before selection is allowed). + out = _full_recommend( + _workload(requirements={"api_continuity": "required", "governance": ["guardrails"]}) + ) + jsonschema.validate(out, _OUT_SCHEMA) + rec = out["workloads"]["chat-svc"] + assert rec["decision_status"] == "decision_required" + assert rec["source_analysis"]["model_generation_changes"] is None + + +def test_n02_absent_feature_is_not_reported_native_or_delta(): + rec = _recommend( + _workload(feature_status={"structured_output_json": "absent"}) + ) + assert "structured_output_json" not in rec["compatibility"]["native"] + assert "structured_output_text_format" not in _delta_codes(rec) + + +def test_n02_status_only_detected_feature_flows_to_behavior(): + # Detected via feature_status (not the array) must still drive native/delta. + rec = _recommend( + _workload(detected_features=[], feature_status={"tool_or_function_calling": "detected"}) + ) + assert "tool_or_function_calling" in rec["compatibility"]["native"] + assert "tool_result_shape" in _delta_codes(rec) + + +def test_g08_unknown_required_feature_forces_decision_required(): + rec = _recommend( + _workload( + requirements={"critical_features": ["structured_output_json"]}, + feature_status={"structured_output_json": "unknown"}, + ) + ) + assert rec["decision_status"] == "decision_required" + assert "feature_scan_incomplete" in _codes(rec["blocks"]) + assert rec["primary_model"] is None + + +def test_n03_tool_requirement_never_lands_on_unevidenced_nova(): + # Nova has no asserted capabilities; a governance workload requiring tools must + # land on the evidenced Converse candidate (Claude), never claim Nova native. + rec = _recommend( + _workload( + requirements={ + "governance": ["guardrails"], + "critical_features": ["tool_or_function_calling"], + } + ) + ) + assert rec["decision_status"] == "recommended" + assert rec["primary_model"] == "anthropic.claude-sonnet-5" + assert "tool_or_function_calling" in rec["compatibility"]["native"] + + +def test_n04_logprobs_and_stop_not_declared_rejected(): + rec = _recommend(_workload(source={"model_ids": ["gpt-5.4"]})) + finding = next( + f for f in rec["tuning"] if f["code"] == "sampling_penalties_rejected" + ) + # Only the two probed penalties are declared rejected in the message. + assert "frequency_penalty" in finding["message"] + assert "presence_penalty" in finding["message"] + assert "logprobs" not in finding["message"] + assert "stop" not in finding["message"] + # logprobs/stop are routed to verification, not rejection. + assert "verify" in finding["remediation"].lower() + + +# --- Converse tier mapping (source OpenAI tier -> matching Claude tier) ---- + +def test_tier_map_sol_source_maps_to_opus_on_converse(): + rec = _recommend( + _workload(source={"model_ids": ["gpt-5.6-sol"]}, + requirements={"governance": ["guardrails"]}) + ) + assert rec["api_path"] == "runtime_converse" + assert rec["primary_model"] == "anthropic.claude-opus-4-8" + + +def test_tier_map_luna_source_maps_to_haiku_on_converse(): + rec = _recommend( + _workload(source={"model_ids": ["gpt-5.6-luna"]}, + requirements={"governance": ["guardrails"]}) + ) + assert rec["primary_model"] == "anthropic.claude-haiku-4-5-20251001-v1:0" + + +def test_tier_map_terra_and_55_sources_map_to_sonnet_on_converse(): + for src in ("gpt-5.6-terra", "gpt-5.5", "gpt-5.4"): + rec = _recommend( + _workload(source={"model_ids": [src]}, + requirements={"governance": ["guardrails"]}) + ) + assert rec["primary_model"] == "anthropic.claude-sonnet-5", src + + +def test_tier_map_falls_back_across_tiers_on_capability_evidence(): + # Luna maps to Haiku, but Haiku has no reasoning evidence — the engine falls + # back to the next Claude tier that covers the requirement (Sonnet). + rec = _recommend( + _workload(source={"model_ids": ["gpt-5.6-luna"]}, + requirements={"governance": ["guardrails"], + "critical_features": ["reasoning"]}) + ) + assert rec["decision_status"] == "recommended" + assert rec["primary_model"] == "anthropic.claude-sonnet-5" diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_scoring.py b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_scoring.py index 38fb34c0..1d97f93c 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_scoring.py +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_scoring.py @@ -195,90 +195,6 @@ def test_services_no_duplicate_memory(): assert services.count("memory") == 1 -def test_model_default_balanced(): - rec = scoring._select_model({"model_priority": "balanced"}) - assert rec["model"] == "claude_sonnet_4_6" - assert "pricing_note" not in rec - - -def test_model_speed_picks_haiku(): - rec = scoring._select_model({"model_priority": "speed"}) - assert rec["model"] == "claude_haiku_4_5" - - -def test_model_extended_thinking_override(): - rec = scoring._select_model( - {"model_priority": "quality", "model_features": "extended_thinking"}) - assert rec["model"] == "claude_sonnet_4_6_thinking" - - -def test_model_migrate_adds_family_note_without_pricing(): - rec = scoring._select_model( - {"_entry_point": "migrate", "current_model": "gpt4o", - "model_priority": "unknown"}) - assert rec["migration_from"] == "gpt4o" - assert "migration-to-aws" in rec["pricing_note"] - assert "$" not in rec["pricing_note"] - - -def test_model_migrate_with_extended_thinking_keeps_override(): - rec = scoring._select_model({ - "_entry_point": "migrate", "current_model": "gpt4o", - "model_features": "extended_thinking"}) - assert rec["model"] == "claude_sonnet_4_6_thinking" # extended-thinking override wins - assert rec["migration_from"] == "gpt4o" - - -# --- Model-selection refactor (feature override + widened pool) --- - -def test_model_feature_speech_picks_nova_2_sonic(): - rec = scoring._select_model({"model_priority": "balanced", "model_features": "speech"}) - assert rec["model"] == "nova_2_sonic" # NOT sonnet, NOT nova_sonic v1 - - -def test_model_feature_image_generation_picks_stability(): - rec = scoring._select_model( - {"model_priority": "balanced", "model_features": "image_generation"}) - assert rec["model"].startswith("stability_image") # NOT nova_canvas - - -def test_model_feature_long_context_uses_llama_scout(): - rec = scoring._select_model( - {"model_priority": "balanced", "model_features": "long_context"}) - # Llama 4 Scout is the ultra-long-context primary (Nova 2 Pro gated on GA) - assert rec["model"] == "llama_4_scout" or "llama_4_scout" in rec.get("alternates", []) - - -def test_model_feature_embedding_picks_titan(): - rec = scoring._select_model( - {"model_priority": "cost", "model_features": "embedding"}) - assert rec["model"] == "titan_embed_v2" - - -def test_model_feature_override_beats_cost_priority_with_advisory(): - # priority=cost would pick haiku, but speech feature hard-overrides to nova_2_sonic, - # and the reasoning must carry a cost-conflict advisory. - rec = scoring._select_model({"model_priority": "cost", "model_features": "speech"}) - assert rec["model"] == "nova_2_sonic" - assert "cost" in rec["reasoning"].lower() # advisory present - - -def test_model_multimodal_primary_vision_stability_alternate(): - rec = scoring._select_model( - {"model_priority": "balanced", "model_features": "multimodal"}) - assert rec["model"] == "claude_sonnet_4_6" # vision understanding primary - assert any("stability" in a for a in rec.get("alternates", [])) - - -def test_model_migrate_feature_override_beats_family(): - # migrate source gpt4o would map to sonnet family, but long_context feature wins. - rec = scoring._select_model({ - "_entry_point": "migrate", "current_model": "gpt4o", - "model_features": "long_context"}) - assert rec["model"] == "llama_4_scout" - assert rec["migration_from"] == "gpt4o" # still recorded - - def test_model_selection_never_changes_verdict(): # Independence invariant: model_* answers must not affect the runtime verdict/scores. base = {"session_duration": "15min_to_8hr", "traffic_pattern": "bursty", @@ -480,26 +396,29 @@ def test_profile_is_well_formed(profile): # Map each internal model id in our selection pool to a substring that identifies it # in the lifecycle file's Legacy/EOL table (by model name or model-id fragment). +# The pool is the Model Recommend engine's selectable set: the priority ordering +# plus every model in the dated per-provider catalogs. _POOL_LIFECYCLE_KEYS = { - "claude_sonnet_4_6": "claude-sonnet-4-6", - "claude_sonnet_4_6_thinking": "claude-sonnet-4-6", - "claude_opus_4_7": "claude-opus-4-7", + "claude_opus_4_8": "claude-opus-4-8", + "claude_sonnet_5": "claude-sonnet-5", "claude_haiku_4_5": "claude-haiku-4-5", - "nova_micro": "nova-micro", - "nova_lite": "nova-lite", - "nova_2_sonic": "nova-2-sonic", - "stability_image_core": "stable-image-core", - "stability_image_ultra": "stable-image-ultra", - "llama_4_scout": "llama4-scout", - "titan_embed_v2": "titan-embed", + "openai_gpt_5_6_sol": "gpt-5.6-sol", + "openai_gpt_5_6_terra": "gpt-5.6-terra", + "openai_gpt_5_6_luna": "gpt-5.6-luna", + "openai_gpt_5_5": "gpt-5.5", + "openai_gpt_5_4": "gpt-5.4", + "anthropic_claude_sonnet_5": "claude-sonnet-5", + "anthropic_claude_opus_4_8": "claude-opus-4-8", + "anthropic_claude_haiku_4_5": "claude-haiku-4-5", } def _pool_models(): - pool = {m for m, _ in scoring._MODEL_PRIORITY.values()} - for model, _reason, alts in scoring._FEATURE_OVERRIDE.values(): - pool.add(model) - pool.update(alts) + import anthropic_model_recommendation as amr + import model_recommendation as mr + pool = {m for order in amr._PRIORITY_ORDER.values() for m in order} + pool.update(mr.load_catalog()["models"]) + pool.update(mr.load_openai_catalog()["models"]) return pool diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_unit_grouping.py b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_unit_grouping.py index 28de2f41..9b503f9b 100644 --- a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_unit_grouping.py +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_unit_grouping.py @@ -121,65 +121,24 @@ def test_migration_plan_unit_correlation_overlay(): "collapse invariant: single-unit runs skip the overlay" -def test_migration_plan_reconciles_model_and_backwrites_recommendation(): - # Live-test 0715 finding: the plan's per-unit model (e.g. gpt-4o-mini -> Nova Lite) - # can differ from the advisor's design.json model, and the POC (plan-backed) used the - # plan's model while recommendation.md still said the old one -> report/POC contradiction. - # Fix: migration-plan Step 3.5 reconciles (plan wins) AND back-writes the recommendation. +def test_migration_plan_validates_advisor_model_contract_without_reselection(): plan = _norm(pathlib.Path(__file__).parent.parent / "references" / "phases" / "migration-plan" / "migration-plan.md") - # A dedicated reconciliation step exists. - assert re.search(r"Reconcile the model", plan) or "model_refined_by_plan" in plan, \ - "migration-plan must have a model reconciliation step" - # Plan wins over the advisor's baseline model. - assert re.search(r"plan(?:'s)?.{0,40}(wins|model wins)", plan, re.IGNORECASE), \ - "reconciliation must state the plan's per-unit model wins" - # design.json is updated with the plan's model. - assert re.search(r"[Uu]pdate `?design\.json`?", plan) and "model_recommendation" in plan, \ - "reconciliation must update design.json.units[].model_recommendation" - assert "model_refined_by_plan" in plan, \ - "reconciliation must mark the refined unit auditable" - # The recommendation report/doc is back-written so it does not contradict the POC. - assert re.search(r"[Bb]ack-write", plan) and \ - "recommendation.md" in plan and "recommendation-report.html" in plan, \ - "reconciliation must back-write recommendation.md AND recommendation-report.html" - # It is a targeted edit, not a full re-render, and other sections stay untouched. - assert re.search(r"NOT a re-render|not a re-render|targeted", plan), \ - "back-write must be a targeted edit, not a full report re-render" - # Re-run 0715 finding: the model can appear in MORE THAN ONE place per file - # (table row, Mermaid diagram node, ASCII overview). The first fix only replaced the - # table row, leaving the diagram + ASCII list stale. Enforce every-occurrence replacement. - assert re.search(r"EVERY occurrence|every occurrence|more than one place|MORE THAN ONE", - plan), \ - "back-write must replace every occurrence, not just the first" - # The load-bearing surfaces must be named so the model doesn't leave a stale copy behind. - assert "Mermaid" in plan or "mermaid" in plan, \ - "back-write must call out the Mermaid diagram node labels as a surface to update" - assert re.search(r"ASCII|plain-text|unit list", plan), \ - "back-write must call out the ASCII/plain-text overview as a surface to update" - # A grep-after-editing check catches leftover stale ids. - assert re.search(r"re-grep|grep .{0,40}(old|OLD)|zero hits", plan), \ - "back-write must re-grep for the old model id and expect zero hits for the unit" - # 3rd-pass 0715 finding: roll-up/summary sentences ("all three agents -> Sonnet") were - # missed because they collapse every unit into one model. Enforce covering them. - assert re.search(r"roll-up|summary statement|collapse ALL units|blanket", plan, - re.IGNORECASE), \ - "back-write must call out roll-up/summary statements that collapse all units into one model" - assert re.search(r"executive summary|exec summary", plan, re.IGNORECASE) and \ - re.search(r"across all three|all agents|every unit|all units", plan, re.IGNORECASE), \ - "back-write must name the exec-summary + blanket phrases as easily-missed surfaces" - # model_refined_by_plan must be a unit-level key (sibling of model_recommendation), - # not nested inside it (the first fix left it unset because placement was ambiguous). - assert re.search(r"model_refined_by_plan.{0,120}(sibling|top-level|ON THE UNIT|NOT inside)", - plan, re.IGNORECASE | re.DOTALL), \ - "model_refined_by_plan must be specified as a unit-level sibling key, not nested" - # The reconciliation is a cross-phase write (migration-plan edits design.json + - # recommendation, which it declares as _input, not _produces). Make it visible in the - # frontmatter contract as a _postcondition so the DSL records the side effect. + + assert re.search(r"Validate the advisor model/path contract", plan), \ + "migration-plan must validate the advisor's per-workload model/path contract" + assert "advisor wins" in plan.lower(), \ + "the advisor must remain the model-selection authority" + assert "plan_model_mismatch" in plan and "_halt_and_inform" in plan, \ + "a plan mismatch must be visible and blocking" + assert re.search(r"do not rewrite `design\.json`", plan, re.IGNORECASE), \ + "migration-plan must not silently replace the confirmed advisor decision" + assert "model-recommendation-input.json" in plan and \ + "model_recommendation.py" in plan, \ + "mismatch resolution must return to the deterministic Model Recommend phase" frontmatter = plan.split("---", 2)[1] if plan.count("---") >= 2 else "" - assert "model_refined_by_plan" in frontmatter and \ - re.search(r"reconciliation|reconcil", frontmatter, re.IGNORECASE), \ - "migration-plan _postconditions must assert the Step 3.5 model reconciliation is settled" + assert re.search(r"model contract validation", frontmatter, re.IGNORECASE), \ + "migration-plan postconditions must enforce contract validation" def test_unit_trigger_vocabulary(): diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_verify_model_path.py b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_verify_model_path.py new file mode 100644 index 00000000..32b2acaa --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/test_verify_model_path.py @@ -0,0 +1,274 @@ +import json +import pathlib + +import jsonschema + +import model_recommendation +import verify_model_path + + +NOW = "2026-07-20T12:00:00+00:00" + + +def _input(requirements=None): + return { + "schema_version": 2, + "region": "us-east-1", + "primary_unit": "support-agent", + "workloads": [ + { + "workload_id": "support-agent", + "source": { + "provider": "anthropic", + "model_ids": ["claude-3-7-sonnet-latest"], + "sdk": "anthropic", + "api_surface": "messages", + "source_paths": ["src/agent.py"], + }, + "requirements": { + "priority": "balanced", + "critical_features": ["tool_use"], + **(requirements or {}), + }, + "detected_features": [], + } + ], + } + + +class FakeMessages: + def __init__(self, response=None, error=None): + self.calls = [] + self.response = response or type( + "Response", (), {"model": "anthropic.claude-sonnet-5"} + )() + self.error = error + + def create(self, **kwargs): + self.calls.append(kwargs) + if self.error: + raise self.error + return self.response + + +class FakeMantleClient: + def __init__(self, messages=None): + self.messages = messages or FakeMessages() + + +class FakeRuntimeClient: + def __init__(self): + self.converse_calls = [] + self.invoke_calls = [] + + def converse(self, **kwargs): + self.converse_calls.append(kwargs) + return {"modelId": kwargs["modelId"]} + + def invoke_model(self, **kwargs): + self.invoke_calls.append(kwargs) + return {} + + +def _verify(requirements=None, **kwargs): + recommendation = model_recommendation.recommend(_input(requirements)) + return verify_model_path.verify_recommendation( + recommendation, now=NOW, **kwargs + ) + + +def test_mantle_probe_uses_recommended_clean_id(): + client = FakeMantleClient() + result = _verify(mantle_client_factory=lambda region: client) + verification = result["workloads"]["support-agent"] + + assert verification["status"] == "passed" + assert verification["response_model_id"] == "anthropic.claude-sonnet-5" + assert client.messages.calls[0]["model"] == "anthropic.claude-sonnet-5" + assert client.messages.calls[0]["max_tokens"] == 8 + + +def test_converse_probe_uses_resolved_cris_without_substitution(): + client = FakeRuntimeClient() + result = _verify( + { + "governance": ["guardrails"], + "data_residency": "global_allowed", + }, + runtime_client_factory=lambda region: client, + ) + verification = result["workloads"]["support-agent"] + + assert verification["status"] == "passed" + assert ( + client.converse_calls[0]["modelId"] + == "global.anthropic.claude-sonnet-5" + ) + + +def test_invoke_probe_uses_native_bedrock_body(): + client = FakeRuntimeClient() + result = _verify( + { + "requires_native_payload": True, + "data_residency": "global_allowed", + }, + runtime_client_factory=lambda region: client, + ) + verification = result["workloads"]["support-agent"] + body = json.loads(client.invoke_calls[0]["body"]) + + assert verification["status"] == "passed" + assert body["anthropic_version"] == "bedrock-2023-05-31" + assert body["messages"][0]["content"] == verify_model_path.PROMPT + + +def test_unresolved_runtime_cris_does_not_create_client(): + calls = [] + result = _verify( + {"governance": ["guardrails"]}, + runtime_client_factory=lambda region: calls.append(region), + ) + verification = result["workloads"]["support-agent"] + + assert verification["status"] == "needs_resolution" + assert verification["invocation_model_id"] is None + assert calls == [] + + +def test_probe_failure_is_structured(): + client = FakeMantleClient(FakeMessages(error=PermissionError("denied"))) + result = _verify(mantle_client_factory=lambda region: client) + verification = result["workloads"]["support-agent"] + + assert verification["status"] == "failed" + assert verification["error"] == { + "type": "PermissionError", + "message": "denied", + } + + +def test_decision_required_is_not_probed(): + result = _verify( + { + "preserve_messages_api": True, + "governance": ["guardrails"], + } + ) + + assert result["workloads"]["support-agent"]["status"] == "not_applicable" + + +def test_verification_output_matches_schema(): + result = _verify(mantle_client_factory=lambda region: FakeMantleClient()) + schema = json.loads( + ( + pathlib.Path(verify_model_path.__file__).parent + / "schemas" + / "model-verification.json" + ).read_text() + ) + + jsonschema.validate( + result, + schema, + format_checker=jsonschema.FormatChecker(), + ) + + +# --- OpenAI provider verification (mocked, no network) --------------------- + + +class FakeResponses: + def __init__(self, response=None, error=None): + self.calls = [] + self.response = response or type("Resp", (), {"model": "openai.gpt-5.6-sol"})() + self.error = error + + def create(self, **kwargs): + self.calls.append(kwargs) + if self.error: + raise self.error + return self.response + + +class FakeOpenAIClient: + def __init__(self, responses=None): + self.responses = responses or FakeResponses() + + +def _openai_input(requirements=None): + return { + "schema_version": 2, + "region": "us-east-2", + "primary_unit": "openai-svc", + "workloads": [ + { + "workload_id": "openai-svc", + "source": { + "provider": "openai", + "model_ids": ["gpt-5.4"], + "sdk": "openai", + "api_surface": "responses", + "source_paths": ["src/app.py"], + }, + "requirements": { + "priority": "balanced", + "critical_features": [], + **(requirements or {}), + }, + "detected_features": [], + } + ], + } + + +def _verify_openai(requirements=None, **kwargs): + recommendation = model_recommendation.recommend(_openai_input(requirements)) + return verify_model_path.verify_recommendation(recommendation, now=NOW, **kwargs) + + +def test_mantle_responses_probe_calls_responses_create_with_exact_id(): + client = FakeOpenAIClient() + result = _verify_openai( + {"api_continuity": "required"}, + openai_responses_client_factory=lambda region: client, + ) + verification = result["workloads"]["openai-svc"] + + assert verification["status"] == "passed" + assert verification["api_path"] == "mantle_openai_responses" + assert client.responses.calls[0]["model"] == "openai.gpt-5.6-sol" + assert client.responses.calls[0]["input"] + assert verification["response_model_id"] == "openai.gpt-5.6-sol" + + +def test_mantle_responses_failure_does_not_substitute_model(): + client = FakeOpenAIClient(FakeResponses(error=RuntimeError("access denied"))) + result = _verify_openai( + {"api_continuity": "required"}, + openai_responses_client_factory=lambda region: client, + ) + verification = result["workloads"]["openai-svc"] + + assert verification["status"] == "failed" + assert verification["invocation_model_id"] == "openai.gpt-5.6-sol" + # Only the exact selected model was ever probed. + assert [c["model"] for c in client.responses.calls] == ["openai.gpt-5.6-sol"] + + +def test_openai_decision_required_is_not_probed(): + called = {"n": 0} + + def factory(region): + called["n"] += 1 + return FakeOpenAIClient() + + result = _verify_openai( + {"api_continuity": "required", "governance": ["guardrails"]}, + openai_responses_client_factory=factory, + ) + verification = result["workloads"]["openai-svc"] + + assert verification["status"] == "not_applicable" + assert called["n"] == 0 # no client constructed for a non-selected recommendation diff --git a/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/verify_model_path.py b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/verify_model_path.py new file mode 100644 index 00000000..d1078c7a --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/verify_model_path.py @@ -0,0 +1,249 @@ +"""Probe agent-advisor model/path recommendations in the target AWS account.""" + +import argparse +import datetime +import json +import pathlib + + +SCHEMAS = pathlib.Path(__file__).parent / "schemas" +PROMPT = "Reply with exactly: ok" + + +def _utc_now(): + return datetime.datetime.now(datetime.timezone.utc).isoformat() + + +def _default_mantle_client(region): + try: + from anthropic import AnthropicBedrockMantle + except ImportError as exc: + raise RuntimeError( + "Mantle verification requires the anthropic package with " + "AnthropicBedrockMantle support" + ) from exc + return AnthropicBedrockMantle(aws_region=region) + + +def _default_runtime_client(region): + try: + import boto3 + except ImportError as exc: + raise RuntimeError("Runtime verification requires boto3") from exc + return boto3.client("bedrock-runtime", region_name=region) + + +def _default_openai_responses_client(region): + """Lazily build an OpenAI SDK client pointed at the Bedrock Mantle endpoint. + + Imports are deferred so the offline recommendation/verify code path never + requires the openai or token-generator packages. + """ + try: + from openai import OpenAI + except ImportError as exc: + raise RuntimeError( + "Mantle Responses verification requires the openai package" + ) from exc + try: + from aws_bedrock_token_generator import provide_token + except ImportError as exc: + raise RuntimeError( + "Mantle Responses verification requires aws-bedrock-token-generator" + ) from exc + base_url = f"https://bedrock-mantle.{region}.api.aws/openai/v1" + return OpenAI(base_url=base_url, api_key=provide_token(region=region)) + + +def _probe_openai_responses(client, model_id): + return client.responses.create(model=model_id, input=PROMPT) + + +def _response_model_id(response): + if isinstance(response, dict): + return response.get("model") or response.get("modelId") + return getattr(response, "model", None) + + +def _probe_mantle(client, model_id): + return client.messages.create( + model=model_id, + max_tokens=8, + messages=[{"role": "user", "content": PROMPT}], + ) + + +def _probe_converse(client, model_id): + return client.converse( + modelId=model_id, + messages=[{"role": "user", "content": [{"text": PROMPT}]}], + inferenceConfig={"maxTokens": 8}, + ) + + +def _probe_invoke(client, model_id): + body = { + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": 8, + "messages": [{"role": "user", "content": PROMPT}], + } + return client.invoke_model( + modelId=model_id, + contentType="application/json", + accept="application/json", + body=json.dumps(body).encode("utf-8"), + ) + + +def _base_result(workload_id, recommendation): + identity = recommendation.get("model_identity") or {} + verification = recommendation["verification"] + return { + "workload_id": workload_id, + "decision_status": recommendation["decision_status"], + "api_path": recommendation.get("api_path"), + "path_model_id": identity.get("path_model_id"), + "invocation_model_id": recommendation.get("invocation_model_id"), + "region": verification["region"], + "status": "not_run", + "checked_at": None, + "response_model_id": None, + "error": None, + } + + +def verify_workload( + workload_id, + recommendation, + mantle_client_factory=None, + runtime_client_factory=None, + openai_responses_client_factory=None, + now=None, +): + result = _base_result(workload_id, recommendation) + if recommendation["decision_status"] != "recommended": + result["status"] = "not_applicable" + return result + + checked_at = now or _utc_now() + result["checked_at"] = checked_at + model_id = result["invocation_model_id"] + if not model_id: + result["status"] = "needs_resolution" + result["error"] = { + "type": "UnresolvedInvocationModelId", + "message": ( + "Resolve an account-invocable inference profile ID before probing; " + "the verifier will not substitute a model ID." + ), + } + return result + + path = result["api_path"] + try: + if path == "mantle_messages": + factory = mantle_client_factory or _default_mantle_client + response = _probe_mantle(factory(result["region"]), model_id) + elif path == "mantle_openai_responses": + factory = openai_responses_client_factory or _default_openai_responses_client + response = _probe_openai_responses(factory(result["region"]), model_id) + elif path == "runtime_converse": + factory = runtime_client_factory or _default_runtime_client + response = _probe_converse(factory(result["region"]), model_id) + elif path == "runtime_invoke": + factory = runtime_client_factory or _default_runtime_client + response = _probe_invoke(factory(result["region"]), model_id) + else: + raise ValueError(f"verification is not implemented for API path: {path}") + except Exception as exc: + result["status"] = "failed" + result["error"] = { + "type": type(exc).__name__, + "message": str(exc), + } + return result + + result["status"] = "passed" + result["response_model_id"] = _response_model_id(response) + return result + + +def verify_recommendation( + recommendation, + workload_ids=None, + mantle_client_factory=None, + runtime_client_factory=None, + openai_responses_client_factory=None, + now=None, +): + selected = set(workload_ids or recommendation["workloads"]) + unknown = sorted(selected - set(recommendation["workloads"])) + if unknown: + raise ValueError(f"workload not found in recommendation: {', '.join(unknown)}") + + generated_at = now or _utc_now() + workloads = {} + for workload_id, workload in recommendation["workloads"].items(): + if workload_id not in selected: + continue + workloads[workload_id] = verify_workload( + workload_id, + workload, + mantle_client_factory=mantle_client_factory, + runtime_client_factory=runtime_client_factory, + openai_responses_client_factory=openai_responses_client_factory, + now=generated_at, + ) + return { + "schema_version": 1, + "recommendation_schema_version": recommendation["schema_version"], + "generated_at": generated_at, + "workloads": workloads, + } + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="probe agent-advisor Bedrock model/path recommendations" + ) + parser.add_argument("recommendation", type=pathlib.Path) + parser.add_argument( + "--output", + type=pathlib.Path, + help="defaults to model-verification.json beside the recommendation", + ) + parser.add_argument( + "--workload", + action="append", + dest="workloads", + help="probe only this workload id; repeat to select more than one", + ) + args = parser.parse_args(argv) + + import jsonschema + + recommendation = json.loads(args.recommendation.read_text()) + jsonschema.validate( + recommendation, + json.loads((SCHEMAS / "model-recommendation.json").read_text()), + ) + result = verify_recommendation(recommendation, workload_ids=args.workloads) + jsonschema.validate( + result, + json.loads((SCHEMAS / "model-verification.json").read_text()), + format_checker=jsonschema.FormatChecker(), + ) + output = args.output or args.recommendation.parent / "model-verification.json" + output.write_text(json.dumps(result, indent=2) + "\n") + + statuses = {item["status"] for item in result["workloads"].values()} + failed = statuses.intersection({"failed", "needs_resolution"}) + print( + f"RESULT={'failed' if failed else 'ok'} " + f"WORKLOADS={len(result['workloads'])}" + ) + return 2 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/migrate/plugins/migration-to-aws/tools/atx-bundle/build.ts b/migrate/plugins/migration-to-aws/tools/atx-bundle/build.ts new file mode 100644 index 00000000..393035a6 --- /dev/null +++ b/migrate/plugins/migration-to-aws/tools/atx-bundle/build.ts @@ -0,0 +1,271 @@ +// build.ts — generate and verify the standalone ATX Custom bundle for agent-advisor. +// +// WHY: the plugin keeps the FULL skill (including the Migration Plan stage, which loads the +// sibling gcp-to-aws engine). The ATX bundle must be self-contained — an ATX transformation +// definition has no sibling skills to climb into. Rather than fork the prose (which drifts — +// see the cached_stale enum bug and the arm64 rate copies), the bundle is a STRICT SUBSET of +// the canonical skill plus ONE generated entry file: +// +// transformation_definition.md = atx/agent-advisor/preamble.md + skills/agent-advisor/SKILL.md +// +// Every other bundle file is byte-identical to its canonical source. The sibling-reference scan +// below is what makes "the ATX build does not depend on gcp-to-aws" a machine-checked property +// instead of a promise. +// +// Usage: +// node tools/atx-bundle/build.ts # CHECK mode: verify only, exit 1 on any ERROR +// node tools/atx-bundle/build.ts --write # BUILD mode: write the bundle, then verify +// +// Zero-dep: runs under Node 24 native TS type-stripping (same as the other tools here). + +import { + copyFileSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { dirname, join } from "node:path"; + +const PLUGIN = "migrate/plugins/migration-to-aws"; +const SKILL = join(PLUGIN, "skills/agent-advisor"); +const ATX = join(PLUGIN, "atx/agent-advisor"); +const MANIFEST = join(ATX, "manifest.txt"); +const PREAMBLE = join(ATX, "preamble.md"); +const OUT = ".tmp/atx-bundle/agent-advisor"; + +// Canonical files that intentionally never ship to ATX. A canonical file matching none of these +// and absent from the manifest is an ERROR — new files must be classified, not silently dropped. +const EXCLUDE_RULES: { pattern: RegExp; why: string }[] = [ + { pattern: /^scripts\/test_/, why: "unit tests — not needed at runtime" }, +]; + +// Terminal / pseudo phases: legal `_advances_to` targets that are states, not phase directories. +const TERMINAL_PHASES = new Set(["complete"]); + +// Sibling-skill references, split by severity because the two kinds fail differently: +// +// LOAD — a path the runtime would actually resolve and read. In the bundle there is no sibling +// to read, so the phase would break. ERROR. +// CITE — prose naming a sibling skill (a provenance note, or "run that skill next"). Nothing +// breaks, but it dangles for an ATX reader who has no such skill. WARN. +// +// `${CLAUDE_PLUGIN_ROOT}/skills/agent-advisor/...` is the skill addressing ITSELF — not a sibling +// dependency — so the load pattern carries a negative lookahead for its own name. +const SIBLING_LOAD_PATTERNS = [ + /\$\{CLAUDE_PLUGIN_ROOT\}\/skills\/(?!agent-advisor)[a-z-]+/, + /\$GCP_BASE/, +]; +const SIBLING_CITE_PATTERNS = [ + /skills\/gcp-to-aws/, + /skills\/heroku-to-aws/, + /skills\/vercel-to-aws/, + /skills\/llm-to-bedrock/, + /skills\/shared/, +]; +const SIBLING_ALLOWED_PREFIX = "references/phases/migration-plan/"; + +// Build artefacts and virtualenvs live next to the canonical files but are not part of the skill. +const IGNORE_DIRS = new Set(["__pycache__", ".venv", ".pytest_cache", "node_modules"]); + +// The capability gate marker that migration-plan.md must carry for the bundle to be honest about +// the missing engine. Keep this string in sync with the phase file. +const CAPABILITY_GATE_MARKER = "ATX capability gate"; + +// Prose pointers at Claude Code slash commands. An ATX user cannot run these, so they should be +// reworded to be environment-neutral. Tracked as WARN so the bundle still builds. +const SLASH_COMMAND_PATTERN = /\/migration-to-aws:[a-z-]+/g; + +const write = process.argv.includes("--write"); +const errors: string[] = []; +const warnings: string[] = []; + +function walk(root: string, rel = ""): string[] { + const abs = join(root, rel); + if (!existsSync(abs)) return []; + const out: string[] = []; + for (const entry of readdirSync(abs)) { + if (IGNORE_DIRS.has(entry)) continue; + const r = rel ? join(rel, entry) : entry; + if (statSync(join(root, r)).isDirectory()) out.push(...walk(root, r)); + else out.push(r); + } + return out; +} + +function readManifest(): string[] { + if (!existsSync(MANIFEST)) { + errors.push(`manifest missing: ${MANIFEST}`); + return []; + } + return readFileSync(MANIFEST, "utf8") + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.length > 0 && !l.startsWith("#")); +} + +const manifest = readManifest(); +const manifestSet = new Set(manifest); + +// ---------------------------------------------------------------- check 1: manifest <-> canonical +const canonical = walk(SKILL).filter((f) => !f.startsWith(".") && !f.includes("/.")); +const canonicalSet = new Set(canonical); + +for (const rel of manifest) { + if (!canonicalSet.has(rel)) { + errors.push(`manifest lists a file that does not exist in the canonical skill: ${rel}`); + } + const excluded = EXCLUDE_RULES.find((r) => r.pattern.test(rel)); + if (excluded) { + errors.push(`manifest lists ${rel}, but an exclusion rule covers it (${excluded.why})`); + } +} +for (const rel of canonical) { + if (manifestSet.has(rel)) continue; + if (EXCLUDE_RULES.some((r) => r.pattern.test(rel))) continue; + errors.push( + `canonical file is neither in the manifest nor excluded — classify it: ${rel}`, + ); +} + +// ------------------------------------------------------- check 2: no sibling-skill dependencies +for (const rel of manifest) { + const abs = join(SKILL, rel); + if (!existsSync(abs)) continue; + if (rel.startsWith(SIBLING_ALLOWED_PREFIX)) continue; // gated stage — see check 3 + const text = readFileSync(abs, "utf8"); + for (const pattern of SIBLING_LOAD_PATTERNS) { + const hit = text.match(pattern); + if (hit) { + errors.push( + `${rel} RESOLVES a sibling-skill path (${hit[0]}) — nothing to read in a standalone bundle`, + ); + } + } + for (const pattern of SIBLING_CITE_PATTERNS) { + const hit = text.match(pattern); + if (hit) { + warnings.push(`${rel} names a sibling skill in prose (${hit[0]}) — dangles for an ATX reader`); + } + } +} + +// ------------------------------------- check 3: the Migration Plan phase carries the capability gate +const gatePhase = "references/phases/migration-plan/migration-plan.md"; +if (manifestSet.has(gatePhase)) { + const text = existsSync(join(SKILL, gatePhase)) ? readFileSync(join(SKILL, gatePhase), "utf8") : ""; + if (!text.includes(CAPABILITY_GATE_MARKER)) { + errors.push( + `${gatePhase} must carry the "${CAPABILITY_GATE_MARKER}" marker: without it the bundle ` + + `would try to load the absent gcp-to-aws engine instead of resolving the stage to ` + + `not_applicable`, + ); + } +} + +// ------------------------------------------------- check 4: the phase graph closes inside the bundle +const phaseFiles = manifest.filter((f) => /^references\/phases\/.*\.md$/.test(f)); +const phaseDirs = new Set( + phaseFiles.map((f) => f.split("/")[2]).filter((d): d is string => Boolean(d)), +); +for (const rel of phaseFiles) { + const text = readFileSync(join(SKILL, rel), "utf8"); + for (const m of text.matchAll(/^\s*_file:\s*(phases\/[^\s]+)/gm)) { + const target = join("references", m[1]!); + if (!manifestSet.has(target)) { + errors.push(`${rel} loads a fragment missing from the bundle: ${m[1]}`); + } + } + for (const m of text.matchAll(/^_advances_to:\s*([a-z-]+)/gm)) { + if (!phaseDirs.has(m[1]!) && !TERMINAL_PHASES.has(m[1]!)) { + errors.push(`${rel} advances to a phase missing from the bundle: ${m[1]}`); + } + } +} + +// ---------------------------------------- check 5: runtime scripts survive a host without uv +// A TOP-LEVEL third-party import makes the script unimportable on a bare python3 host — ERROR. +// A lazy import (inside a function, or guarded by try/except ImportError) only fails on the code +// path that needs it, which the ATX preamble may have disabled anyway — WARN, with the caller +// responsible for provisioning the dep (`uv run --with ...`) or degrading. +const THIRD_PARTY = /(jsonschema|yaml|requests|boto3|pydantic|anthropic)\b/; +for (const rel of manifest.filter((f) => /^scripts\/.*\.py$/.test(f))) { + const text = readFileSync(join(SKILL, rel), "utf8"); + for (const m of text.matchAll(/^(\s*)(?:import|from)\s+([A-Za-z_][\w.]*)/gm)) { + const [, indent, mod] = m; + if (!THIRD_PARTY.test(mod!)) continue; + if (indent!.length === 0) { + errors.push( + `${rel} imports ${mod} at top level — the script cannot even be loaded on a bare python3 host`, + ); + } else { + warnings.push( + `${rel} lazily imports ${mod} — that code path needs uv (or the dep preinstalled) on the ATX host`, + ); + } + } +} + +// ------------------------------------------------------------- check 6: preamble present, entry sane +if (!existsSync(PREAMBLE)) { + errors.push(`preamble missing: ${PREAMBLE}`); +} +if (!manifestSet.has("SKILL.md")) { + errors.push("manifest must list SKILL.md — it is the body of transformation_definition.md"); +} + +// ------------------------------------------------------------------- WARN: Claude Code slash commands +for (const rel of manifest) { + const abs = join(SKILL, rel); + if (!existsSync(abs)) continue; + const hits = new Set(readFileSync(abs, "utf8").match(SLASH_COMMAND_PATTERN) ?? []); + if (hits.size > 0) { + warnings.push(`${rel} points at Claude Code slash command(s) ${[...hits].join(", ")}`); + } +} + +// ------------------------------------------------------------------------------------ build mode +if (write) { + rmSync(OUT, { recursive: true, force: true }); + mkdirSync(OUT, { recursive: true }); + let copied = 0; + for (const rel of manifest) { + const src = join(SKILL, rel); + if (!existsSync(src)) continue; + if (rel === "SKILL.md") continue; // becomes transformation_definition.md + const dest = join(OUT, rel); + mkdirSync(dirname(dest), { recursive: true }); + copyFileSync(src, dest); + copied += 1; + } + if (existsSync(PREAMBLE) && existsSync(join(SKILL, "SKILL.md"))) { + // Strip SKILL.md's YAML frontmatter: `name`/`description` are Claude Code plugin metadata + // (they drive skill discovery there). A transformation registry takes the name and description + // as its own publish arguments, so carrying the block would leave a stray YAML island in the + // middle of the definition. + const skillText = readFileSync(join(SKILL, "SKILL.md"), "utf8"); + const body = skillText.startsWith("---\n") + ? skillText.slice(skillText.indexOf("\n---", 4) + 4).replace(/^\n+/, "") + : skillText; + const entry = `${readFileSync(PREAMBLE, "utf8").trimEnd()}\n\n${body}`; + writeFileSync(join(OUT, "transformation_definition.md"), entry); + } + console.log(`built ${OUT}: ${copied} copied + transformation_definition.md`); + console.log( + `entry point: ${OUT}/transformation_definition.md (relative paths resolve against ${OUT}/)`, + ); +} + +// --------------------------------------------------------------------------------------- report +for (const w of warnings) console.log(`WARN ${w}`); +if (errors.length > 0) { + for (const e of errors) console.error(`ERROR ${e}`); + console.error(`\natx:check FAILED — ${errors.length} error(s), ${warnings.length} warning(s)`); + process.exit(1); +} +console.log( + `atx:check OK — ${manifest.length} files in the bundle, ${warnings.length} warning(s)`, +); diff --git a/mise.toml b/mise.toml index ae4ad51b..ff43b7e2 100644 --- a/mise.toml +++ b/mise.toml @@ -78,6 +78,14 @@ run = "node migrate/plugins/migration-to-aws/tools/fixtures-check.ts" description = "Report pricing caches past their own declared freshness window (warn-only; use --strict in a scheduled workflow to fail)" run = "node migrate/plugins/migration-to-aws/tools/pricing-staleness.ts" +[tasks."atx:build"] +description = "Build the standalone ATX Custom bundle for agent-advisor into .tmp/atx-bundle/ (then verify)" +run = "node migrate/plugins/migration-to-aws/tools/atx-bundle/build.ts --write" + +[tasks."atx:check"] +description = "Verify the ATX bundle stays a self-contained strict subset of the canonical skill (CI)" +run = "node migrate/plugins/migration-to-aws/tools/atx-bundle/build.ts" + [tasks.lint] description = "Run all linters" run = [ @@ -85,6 +93,7 @@ run = [ { task = "lint:types" }, { task = "lint:frontmatter" }, { task = "shared:check" }, + { task = "atx:check" }, { task = "fixtures:assert" }, { task = "fixtures:check" }, { task = "pricing:staleness" }, @@ -98,7 +107,7 @@ run = [ [tasks."security:bandit"] description = "Run Bandit (two-pass: migration-to-aws plugin with its own skip config; rest of repo with full strictness — mirrors the CI bandit job in .github/workflows/security-scanners.yml)" run = [ - "bandit -r migrate/plugins/migration-to-aws -c migrate/plugins/migration-to-aws/bandit.yml -x migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/.venv,migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/.venv,migrate/plugins/migration-to-aws/tests", + "bandit -r migrate/plugins/migration-to-aws -c migrate/plugins/migration-to-aws/bandit.yml -x migrate/plugins/migration-to-aws/skills/agent-advisor/scripts/.venv,migrate/plugins/migration-to-aws/skills/llm-to-bedrock/scripts/.venv,migrate/plugins/migration-to-aws/fixtures/agent-advisor-anthropic-demo/.venv,migrate/plugins/migration-to-aws/tests", "bandit -r . --exclude ./.tmp,./node_modules,./migrate/plugins/migration-to-aws", ]