{"owner":"JoshuaC215","repo":"agent-service-toolkit","hasSkills":true,"totalSkillsCount":7,"totalTokensCount":14678,"categories":["anthropic-skill","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/skills/dependency-refresh/references/live-e2e.md",".claude/skills/model-refresh/SKILL.md",".claude/skills/dependency-refresh/SKILL.md",".claude/skills/dependency-refresh/references/coupling-constraints.md",".claude/skills/smoke-test/SKILL.md","CLAUDE.md",".claude/skills/maintainer-response/SKILL.md"],"skills":{".claude/skills/dependency-refresh/references/live-e2e.md":"# Live end-to-end verification (no API key needed)\n\nUnit tests mock the LLM and transport, so a running-service check is the only\nway to catch integration regressions from bumps to FastAPI/uvicorn/langgraph/\nthe checkpointers, and a real browser pass is the only thing that catches\nStreamlit-stack UI breakage.\n\n## Fake-model HTTP ladder\n\nThe service ships a fake model (`USE_FAKE_MODEL=true`) that satisfies the \"at\nleast one LLM key\" startup check and returns a canned reply — so you can drive\nthe real HTTP API on the upgraded stack without provider credentials. Leaving\n`AUTH_SECRET` unset keeps the endpoints unauthenticated (no bearer token).\n\nRun it natively (fastest — exercises the same `run_service.py`/uvicorn\nentrypoint and the full dependency stack, just not containerized):\n\n```sh\nPYTHONPATH=src USE_FAKE_MODEL=true HOST=127.0.0.1 PORT=8080 uv run python src/run_service.py &\n# then, once /health is up:\ncurl -s localhost:8080/health                       # {\"status\":\"ok\"}  -> app + lifespan booted\ncurl -s localhost:8080/info                          # agents/models listed\ncurl -s -XPOST localhost:8080/invoke   -H 'content-type: application/json' \\\n     -d '{\"message\":\"hi\",\"agent_id\":\"chatbot\",\"model\":\"fake\"}'         # -> completion + run_id\ncurl -s -N -XPOST localhost:8080/stream -H 'content-type: application/json' \\\n     -d '{\"message\":\"hi\",\"agent_id\":\"chatbot\",\"model\":\"fake\",\"stream_tokens\":true}'  # SSE tokens\n# persistence: invoke twice with the same \"thread_id\", then POST /history for that thread\n# and confirm the checkpointer returns the prior turns (validates the checkpointer packages).\n```\n\nWhat each check validates: `/health` + `/info` = FastAPI/uvicorn/pydantic boot\nand agent wiring; `/invoke` = a full graph run (langgraph + langchain +\nlangsmith `run_id`); `/stream` = the SSE `StreamingResponse` path; `/history`\non a reused `thread_id` = the checkpointer\n(langgraph-checkpoint-sqlite/aiosqlite by default).\n\n## Streamlit UI e2e (browser)\n\nCI never drives the actual Streamlit interface — pytest mocks the transport and\nthe docker CI job only checks health endpoints — so a real browser pass is the\nonly thing that catches Streamlit/pandas/pyarrow-level UI breakage from a bump.\nWith the fake-model service from above still running:\n\n```sh\nuv run streamlit run src/streamlit_app.py --server.headless true --server.port 8501 &\n# quick single-message smoke:\nuv run --with playwright python scripts/smoke_live_app.py http://localhost:8501\n# wider coverage (multi-turn resume, settings selectors, feedback widget, streaming toggle, ...):\nuv run --with playwright python scripts/e2e_ui_tests.py http://localhost:8501\n```\n\n`smoke_live_app.py` sends one chat message and verifies a streamed response\nrenders and settles. `e2e_ui_tests.py` is a small suite of key user journeys\nbuilt on the same idea — run `--list` to see them, or pass names to run a\nsubset. Both exit 0 on pass and save a diagnostic screenshot on failure, and\nboth are URL-parameterized so the same commands also run against the deployed\napp (omit the URL, or set `LIVE_APP_URL`). Run at least the smoke before\nopening any PR that bumps `streamlit`, its rendering stack (pandas, pyarrow,\npillow, altair), or the client/schema code the UI consumes; run the fuller\nsuite when a bump could plausibly touch chat history, settings, or the\nfeedback/streaming paths.\n\n## Containerized test\n\nTo validate the image itself (base image + in-image `uv sync`), run\n`docker compose up --build` and hit the same endpoints on the mapped ports —\nthis mirrors the `test-docker` CI job. It requires pulling the Python slim\nbase image, so it needs outbound Docker Hub access (not available in every\nsandbox; CI handles it via docker-in-docker).\n",".claude/skills/model-refresh/SKILL.md":"---\nname: model-refresh\ndescription: >-\n  Periodically audit the LLM model catalog in src/schema/models.py against what\n  each provider currently ships: add newly released models, remove/flag ones the\n  provider has deprecated or discourages, and re-point DEFAULT_MODEL fallbacks at\n  a current model. Use when asked to \"check for new models\", \"update the model\n  list\", \"refresh the model catalog\", or on the scheduled model-refresh trigger.\n---\n\n# Model Catalog Refresh\n\nKeep `src/schema/models.py` (the `AllModelEnum` catalog) current: add models a\nprovider shipped since the last refresh, remove ones the provider has deprecated\nor is actively steering users away from, and make sure `src/core/settings.py`'s\nper-provider `DEFAULT_MODEL` fallbacks point at a model that still exists.\n\n## Where model config lives\n\n| What | File |\n|---|---|\n| The enum of every supported model, per provider, with the provider's docs URL in each class docstring | `src/schema/models.py` (`AllModelEnum`) |\n| Per-provider default model + which models are \"available\" when a key is set | `src/core/settings.py` (`Settings.model_post_init`) |\n| Enum → API model-string mapping, and provider-specific construction quirks (temperature, streaming, tool-binding) | `src/core/llm.py` (`_MODEL_TABLE`, `get_model`) |\n| Unit tests asserting `get_model` builds the right LangChain class per model | `tests/core/test_llm.py` |\n| Settings/default-model tests | `tests/core/test_settings.py` |\n| Live smoke test (real API calls) | `scripts/check_live_models.py` |\n\n## Workflow\n\n1. **Survey.** For each provider block in `src/schema/models.py`, fetch the docs\n   URL already in that class's docstring and diff its current model list against\n   the enum values present. These docstring URLs are the **single canonical\n   source** for where to look — don't hardcode a second copy of them elsewhere,\n   since provider doc URLs shift over time and duplicated links silently rot.\n   - **If a docstring URL 404s, redirects to a generic landing page, or otherwise\n     no longer points at a model listing:** find the current canonical URL for\n     that provider's model docs and update the docstring to it as part of this\n     same change, then continue the survey from the corrected page. Don't skip\n     the provider just because the old link broke.\n   - A few providers need a second check beyond the docstring URL: Azure OpenAI\n     lags OpenAI's own releases (it's deployment-based — check what base models\n     Azure currently supports), Anthropic has a separate deprecations page worth\n     checking for retirement dates, and Vertex AI model paths sometimes differ\n     from the Gemini API's own names for the same model (e.g. `models/gemini-2.5-flash`\n     vs `gemini-2.5-flash`) — check both columns. Ollama takes a user-supplied\n     model name at runtime, so there's nothing to add there; just confirm the\n     generic pass-through in `llm.py` still works.\n   - **`docs.aws.amazon.com` returns HTTP 403 to `WebFetch`** (it blocks the\n     fetcher's user agent — this is not an egress-policy denial). Use `WebSearch`\n     for Bedrock model IDs instead; it reads the same doc pages server-side and\n     surfaces the model-card and inference-profile-support pages. The per-model\n     card pages (`.../model-card-anthropic-claude-<model>.html`) list the exact\n     `modelId` and profile IDs.\n2. **Classify each gap:**\n   - *New model, generally available* → add it.\n   - *New model, preview/experimental* → use judgment; this repo has taken preview\n     models before (e.g. `gemini-3-pro-preview`) when there's no GA alternative yet,\n     but prefer GA when one exists.\n   - *Existing model deprecated or sunset by the provider* → remove it, unless it's\n     the only model left for that provider (flag that case instead of leaving the\n     catalog empty).\n   - *Existing model merely superseded but still served* → leave it unless the\n     provider's docs explicitly say to migrate off it.\n3. **Match existing naming conventions** when adding a member:\n   - Enum member names are `SCREAMING_SNAKE_CASE`, usually family+size, e.g.\n     `SONNET_45`, `GEMINI_25_PRO`, `LLAMA_33_70B`. Version numbers drop dots\n     (`4.5` → `45`).\n   - Enum values are the exact string the provider's API expects\n     (`claude-sonnet-4-5`, `gemini-2.5-pro`, `gpt-5.1`) — copy verbatim from the\n     provider docs, don't guess.\n   - Values must be unique across *all* the enums in `AllModelEnum` —\n     `tests/schema/test_models.py` enforces this. Where two providers serve the\n     same upstream model one side needs a distinguishing form: `VertexAIModelName`\n     uses the `models/` prefix, so keep it on new Vertex entries.\n   - Keep provider families grouped and roughly ordered by size/generation within\n     a class, matching how they read today.\n4. **Apply changes across every coupled location** — do not edit only the enum:\n   - `src/schema/models.py`: add/remove the `StrEnum` member. Keep the docstring\n     URL current (see step 1).\n   - `src/core/settings.py` `model_post_init`: if the removed model was a\n     `DEFAULT_MODEL` fallback, repoint it at a remaining (ideally cheap/fast) model\n     for that provider — match the intent of the current defaults (e.g.\n     Haiku/Flash/Nano-tier, not the flagship).\n   - `src/core/llm.py`: nothing to do for a plain rename/add (the `_MODEL_TABLE`\n     and `if model_name in ...Name` dispatch are enum-driven), but check for any\n     provider-specific special-casing (see the Groq safeguard-model branch) that\n     might need to apply to the new model too.\n   - `tests/core/test_llm.py`, `tests/core/test_settings.py`: update any test that\n     hardcodes a model value being removed; add a case for a notable new model if\n     it has special handling (temperature override, tool-binding quirk, etc).\n   - `.env.example`, other docs: update only if they name a specific model that\n     changed (most don't).\n5. **Do not touch provider credentials or add live network calls during the\n   survey step.** This step is pure research + code edit, no API keys needed.\n6. **Live-test what you changed** (needs API keys — see \"Live testing\" below).\n7. **Summarize** what was added/removed/repointed and why, citing the provider\n   doc for each change. Flag any model rename explicitly — it's a breaking change\n   for existing deployments pinning the old enum value in `DEFAULT_MODEL`/\n   `AVAILABLE_MODELS` env config, not something to swap silently. Commit and push\n   per the repo's normal git workflow; open a PR only if asked.\n\n## Providers you can't live-verify (Bedrock, Azure, Vertex service-account, DeepSeek, OpenRouter)\n\nNot having a key for a provider is **not** a reason to skip it — a stale or\nbroken catalog entry is worse than a doc-sourced one. Update these from docs\njust like the rest, and in the PR mark them explicitly as **doc-only /\nunverified**, citing the provider page and calling out any caveat below so the\nnext person with a key knows exactly what to spot-check. Only leave a provider\nuntouched when the docs themselves are ambiguous *and* the change would be a\nproduct decision (e.g. adding a whole new pricing tier), not a freshness update.\n\n- **AWS Bedrock** — the enum *value* is passed straight to `ChatBedrock(model_id=...)`,\n  so it must be a real Bedrock ID, not a friendly label. Two gotchas:\n  1. The latest Claude models are **not invocable on-demand by their base model\n     ID** — a bare `anthropic.claude-...` call 400s with \"on-demand throughput\n     isn't supported.\" They must go through a cross-region **inference profile**:\n     the base ID prefixed with a geo (`us.`/`eu.`/`apac.`) or `global.`. Prefer\n     `global.` (routes dynamically, region-agnostic — the best fit for a catalog\n     value with no region context) and note in the PR that single-region\n     deployments not enrolled in Global CRIS should swap the prefix for their geo.\n  2. Bedrock inherits the same **sampling-parameter restrictions** as the direct\n     Anthropic API — e.g. a Sonnet-5-class model rejects `temperature`. If you\n     point a Bedrock entry at such a model, mirror the no-`temperature` branch\n     that already exists for it in `llm.py`'s Anthropic and Bedrock dispatch.\n  If AWS credentials happen to be present, `boto3.client(\"bedrock\").list_foundation_models()`\n  is the fastest way to confirm real IDs — but note that Bedrock access is a\n  separate enablement from plain AWS creds, so this can fail with an auth error\n  even when other AWS calls would work.\n- **Azure OpenAI** is the one genuinely heavier lift, because it's deployment-based\n  and the catalog is coupled in more places than the enum:\n  - `settings.py` `model_post_init` hardcodes a `required_models` set (currently\n    `{\"gpt-4o\", \"gpt-4o-mini\"}`) that it validates the `AZURE_OPENAI_DEPLOYMENT_MAP`\n    against — bumping the enum means bumping that set and the `.env.example`\n    deployment-map sample and the ~7 Azure cases in `tests/core/test_settings.py`.\n  - `llm.py` hardcodes `temperature=0.5` for the Azure path, but Azure's GPT-5-era\n    **reasoning** variants reject `temperature` (400). If you move Azure onto one,\n    add a no-`temperature` branch like the Anthropic/Bedrock Sonnet-5 handling.\n  - Changing an Azure enum value is a **breaking change** to every user's\n    deployment map (they name deployments after these keys). Treat an Azure\n    generation bump as its own reviewed change, and flag the deployment-map break\n    loudly — don't fold it silently into a routine refresh.\n\n## Live testing\n\n`scripts/check_live_models.py` sends a trivial one-word prompt to every model of\nevery provider that has credentials configured in the environment, and reports\nPASS/FAIL/SKIP per model. It is deliberately outside the pytest suite (real\nnetwork calls, tiny real cost) — run it by hand or from a scheduled trigger with\nkeys populated:\n\n```sh\nPYTHONPATH=src uv run python scripts/check_live_models.py                    # all configured providers\nPYTHONPATH=src uv run python scripts/check_live_models.py --provider anthropic google\n```\n\nA `SKIP` line means no credentials were present for that provider — that's\nexpected and not a failure. Only treat `FAIL` rows as build-blocking. Cost is\nnegligible (a handful of few-token completions per provider) but it's real spend\nagainst real keys — don't wire it into CI or run it on every commit.\n\nIf no provider credentials are configured in the current environment, skip this\nstep entirely rather than failing — the survey/edit step is still fully useful on\nits own.\n",".claude/skills/dependency-refresh/SKILL.md":"---\nname: dependency-refresh\ndescription: >-\n  Run a dependency & version refresh for this repo: Python libraries in\n  pyproject.toml/uv.lock, Docker base images, GitHub Actions pins, the uv CLI\n  version (CI + README + Dockerfiles), the supported Python range, and the\n  infra images (postgres/mongo/LangFuse) used by compose and the smoke tests.\n  Use when asked to \"update dependencies\", \"bump versions\", \"do a dependency\n  refresh\", or on the scheduled monthly dependency-refresh run. Self-contained\n  playbook; prior state (deferred majors, cooldown dates) lives in the previous\n  refresh PR, which Step 0 tells you how to find.\n---\n\n# Dependency & Version Refresh\n\nThis skill is the complete playbook. Running state — what moved each round and\nwhich majors are deferred — lives in **refresh PR descriptions**, not in a file.\n\nSupporting references in this skill's directory — read them at the step that\nneeds them, not up front:\n\n- `references/coupling-constraints.md` — project-specific coupling constraints\n  and gotchas learned in past refreshes (langchain⇄langgraph lockstep,\n  checkpointer majors, Actions runtime deprecations, uv quirks, …). Skim the\n  headings before triage; read fully when a resolver conflict or CI surprise hits.\n- `references/live-e2e.md` — the live end-to-end verification recipe\n  (fake-model HTTP ladder + Streamlit browser tests).\n\n## Conventions: PRs are the state record\n\nEvery refresh produces exactly one PR that both delivers the changes and\nrecords the state the *next* refresh starts from:\n\n- **Branch:** `claude/dependency-refresh-YYYY-MM-DD`\n- **Title:** `chore(deps): dependency refresh YYYY-MM-DD`\n- **Body sections** (the template):\n\n```markdown\n## What moved\n<pin raises, lock-only movement, Actions/uv CLI, infra images, base images —\ngrouped, with one-line release-note checks for anything 0.x or major>\n\n## Deferred majors & cooldowns\n<carry this table forward from the previous refresh PR: drop rows that landed\n(note them under \"What moved\"), keep rows still deferred, add newly discovered\nmajors. Every key-dependency major gets a row the round it is first seen.>\n\n| Upgrade | From → To | Major released | Eligible from | Notes / ROI |\n| --- | --- | --- | --- | --- |\n\n## Verification\n<checks run and their results, including which smoke targets were re-run and\nwhich were deliberately skipped and why>\n```\n\nNever stack extra state elsewhere: no journal files, no backlog docs. If a\nlearning is *reusable knowledge* (a coupling constraint, a gotcha), record it in\n`references/coupling-constraints.md` in the same PR — that file is knowledge,\nnot state.\n\n## Step 0 — Recover state from the last refresh PR\n\nBefore touching any pin, find the most recent refresh PR and read its body:\n\n1. Search PRs (state: all, newest first) with query\n   `\"dependency refresh\" in:title` scoped to this repo.\n2. Fallback: search by head-branch prefix `head:claude/dependency-refresh`.\n3. Last resort: `git log` on `main` for commits touching `uv.lock`, then look up\n   their PRs.\n\nFrom the latest one (or two, if the latest was narrow), extract: the deferred\nmajors table, anything \"deliberately held back\", and verification caveats\n(e.g. \"MongoDB untested\"). That table seeds this round's triage; carry it\nforward per the template above.\n\n## Cooldown policy\n\nTwo layers, one mechanism where possible:\n\n**1. Global 14-day resolver cooldown (enforced by uv).** `pyproject.toml` has:\n\n```toml\n[tool.uv]\nexclude-newer = \"14 days\"\n```\n\nEvery resolution (`uv lock`, `uv lock --upgrade`, `uv sync` re-locks) only\nconsiders releases at least 14 days old, so a compromised or broken\nfresh release can't enter the lock during its highest-risk window. This\nautomatically limits pin raises too: raising a `~=` pin to a version younger\nthan 14 days makes the resolve fail with an `exclude-newer` hint — that is the\npolicy working, not an error to route around. Hold that bump for the next\nround instead of overriding.\n\n**Security exception (the only sanctioned override):** if a release fixes a\nvulnerability that affects this repo (Dependabot PR, advisory), take it\nimmediately regardless of age — add a temporary per-package override with a\ncomment, and remove it in the next refresh once the release is older than the\nglobal cooldown:\n\n```toml\n[tool.uv.exclude-newer-package]\nsomepkg = \"0 days\"  # security fix CVE-XXXX; remove once >14 days old\n```\n\nCooldown mechanics to know: the span is recorded in `uv.lock`\n(`exclude-newer-span`), re-locks are stable, and it needs the repo's pinned uv\nversion — see the uv entries in `references/coupling-constraints.md`.\n\n**2. Three-month major cooldown for key dependencies (enforced by process).**\nKey dependencies are the ones where a bad major is expensive or sticky:\n\n- **Infra images:** `postgres`, `mongo` (compose/smoke tests) — majors can\n  change on-disk formats.\n- **App platform:** `streamlit`, `fastapi`, `pydantic`.\n- **Core agent stack:** `langgraph` + `langchain` and the coupled\n  `langgraph-checkpoint-*` packages.\n\nA new major of a key dependency is not adopted until **at least 3 months after\nits X.0.0 release date**, and then only in its own dedicated PR (never inside a\nsafe-bumps round). The round that first sees the new major adds a row to the\ndeferred table with the major's release date and the computed \"eligible from\"\ndate; subsequent rounds carry the row until it lands or is rejected. Eligible ≠\nautomatic: when the date arrives, do the normal major triage (below) before\nlanding it. Non-key majors need no 3-month wait — just ordinary triage.\n\n## Where versions live\n\n| What | File |\n| --- | --- |\n| Runtime deps + version pins | `pyproject.toml` → `[project] dependencies` |\n| Dev tooling (ruff, pyrefly, pytest, …) | `pyproject.toml` → `[dependency-groups] dev` |\n| Minimal client/Streamlit deps (a subset, **kept in sync** with the main list) | `pyproject.toml` → `[dependency-groups] client` |\n| Fully resolved versions (source of truth for what installs) | `uv.lock` |\n| Resolver cooldown | `pyproject.toml` → `[tool.uv] exclude-newer` |\n| Lint/format target Python | `pyproject.toml` → `[tool.ruff] target-version` |\n| CI test matrix | `.github/workflows/test.yml` (`python-version`) |\n| Container base image | `docker/Dockerfile.app`, `docker/Dockerfile.service` |\n| Supported Python range + classifiers | `pyproject.toml` → `requires-python`, `classifiers` |\n| GitHub Actions versions (`actions/checkout`, `setup-python`, `setup-uv`, `docker/*`, `codecov-action`, …) | `.github/workflows/*.yml` (`uses:`) |\n| `uv` CLI version — **four places, keep in sync** | `.github/workflows/test.yml` (both `setup-uv` steps' `version:`), `docker/Dockerfile.app` + `docker/Dockerfile.service` (`pip install uv==`), `README.md` quickstart (`curl .../uv/<version>/install.sh`) |\n| Infra images used by compose + smoke tests | `compose.yaml` (`postgres:` tag), `docker/compose.mongo.yaml` (`mongo:` tag), `scripts/smoke_test.sh` (`LANGFUSE_REF`) |\n\n## The workflow\n\n1. **Recover state** (Step 0 above).\n2. **Survey.** Compare resolved versions in `uv.lock` against the latest on\n   PyPI (`https://pypi.org/pypi/<package>/json` → `info.version`,\n   `info.requires_dist`, `info.requires_python`; each release's file\n   `upload_time_iso_8601` gives the age for cooldown checks). Also survey the\n   non-Python surfaces from the table above: Actions pins, uv CLI, Docker base\n   images, infra image tags, `LANGFUSE_REF`.\n3. **Triage** each candidate (principles below), honoring both cooldowns.\n   New key-dependency majors get a deferred-table row, not a bump.\n4. **Apply safe bumps** by editing pins in `pyproject.toml`. Preserve the\n   existing style: `~=` for app deps, `>=` floors for loosely-pinned libs.\n5. **Re-resolve:** `uv lock --upgrade`. Read conflicts carefully — they name\n   the exact transitive constraint (or cooldown) blocking a bump; check\n   `references/coupling-constraints.md` for known ones.\n6. **Reconcile pins to the lock.** Where the resolver picked higher than the\n   written pin (common for `>=` floors), raise the written pin to match — after\n   which `uv lock` must report no changes. **Sanity-check each reconciled floor\n   against what actual dependents require** before writing it down (grep the\n   package's block in `uv.lock` for reverse-dep `specifier`s) — otherwise a\n   floor nothing needs can block a future upgrade.\n7. **Sync + static verify:** `uv sync --frozen`, then `uv run ruff check .`,\n   `uv run ruff format --check .`, `uv run pyrefly check`, `uv run pytest`.\n8. **Live e2e:** follow `references/live-e2e.md` (fake-model HTTP ladder;\n   Streamlit browser smoke when the UI stack moved). If a bump touches\n   checkpointers, AG-UI, or LangFuse, also run the relevant\n   `scripts/smoke_test.sh` target (see the smoke-test skill; needs the Docker\n   daemon — `sudo dockerd &` if it isn't running).\n9. **PR:** branch, title, and body per the conventions above. Never push to\n   main. Record new reusable gotchas in the coupling reference in the same PR.\n\n## Triage principles\n\n- **Minor/patch bumps** within a major are generally safe — batch them.\n- **Major bumps** (and pre-1.0 `0.x` minors, which don't promise SemVer\n  stability): assess the actual code/behavior change against how the repo\n  *uses* the package, weigh ROI, and hold non-trivial ones for their own PR.\n  Grep for the import first — often the repo touches only a tiny, stable part\n  of the API.\n- **Key-dependency majors** additionally wait out the 3-month cooldown.\n- **Version-coupled packages** move in lockstep and a bump in one drags others\n  — the known couplings are in `references/coupling-constraints.md`; the\n  resolver will find the rest.\n- **Transitive-only deps** (nothing in the repo imports them) are lower risk\n  for our code; their risk lives in whatever consumes them (e.g.\n  Streamlit ↔ pandas/pyarrow). Don't add them to `[project] dependencies`\n  just to pin them.\n\n## Python version policy\n\nFollow CPython's cycle: a new minor each October, ~5 years of support (~18\nmonths bugfix, then security-only). Keep the declared range\n(`requires-python`), classifiers, CI matrix, ruff `target-version`, and Docker\nbase images mutually consistent — all listed in the table above. Adopt a new\nminor once the dependency stack resolves and passes on it; drop a minor when\nit nears security-EOL or a needed dependency drops it first (numpy is usually\nthe leading indicator). Verify support claims on a real interpreter of that\nversion (`uv python install 3.X`, then the full check ladder) — and use a\nstable patch release, not an `rc` (see the coupling reference).\n",".claude/skills/dependency-refresh/references/coupling-constraints.md":"# Coupling constraints & gotchas\n\nProject-specific constraints hit during real refreshes — check here before\nfighting a resolver conflict or a CI surprise, and add new ones in the same PR\nthat hits them. Key points only; verify current specifics against the resolver\nand release notes.\n\n## uv & the cooldown mechanism\n\n- **Relative `exclude-newer` needs a recent uv.** Older uv warns\n  (`Failed to parse pyproject.toml during settings discovery`), ignores the\n  whole `[tool.uv]` table, and a re-lock with it silently strips the cooldown\n  metadata from `uv.lock`. Always lock with the repo's pinned uv version.\n- **A cooldown conflict looks like an unsatisfiable pin** — `uv lock` fails\n  with an `exclude-newer` hint. That's enforcement, not breakage: hold the\n  bump, or (security fixes only) add a commented temporary\n  `[tool.uv.exclude-newer-package]` override per SKILL.md.\n- **Re-locks refetch index metadata**, so unrelated wheel entries can drift out\n  of `uv.lock` with no version change. Harmless for supported platforms.\n\n## GitHub Actions\n\n- **Node runtime deprecations show up as CI warnings, not lockfile conflicts**,\n  and pinned `@vN` refs don't auto-upgrade. Check each action's `runs.using`\n  — including the nested `uses:` lines inside composite actions — and bump to\n  a major that ships the current runtime. Watch for behavior changes riding\n  along (e.g. `docker/build-push-action` turning on provenance attestations\n  by default).\n- **Some actions stop publishing floating major tags** for supply-chain\n  hardening (`astral-sh/setup-uv` as of v8) and must be pinned to an exact\n  release.\n\n## LangChain / LangGraph ecosystem\n\n- **`langchain` ⇄ `langgraph` move in lockstep** — the meta-package pins a\n  narrow `langgraph` range, so neither can be bumped past the other.\n- **`langgraph` ⇄ `langgraph-checkpoint` ⇄ the checkpointer packages** upgrade\n  as one coupled unit, and checkpointer majors can change stored-checkpoint\n  compatibility or drop APIs (the mongodb saver dropped its async class;\n  `src/memory/mongodb.py` bridges it). The test suite mocks the real backends,\n  so verify old checkpoints still read under new savers manually.\n- **LLM SDK bumps chain**: `langchain-<provider>` pulls a newer provider SDK\n  which drags its own floors (e.g. `openai` → `jiter`). Expect to move the\n  whole chain together.\n- **Provider adapters can cap shared libs** (e.g. `langchain-google-vertexai`\n  capping `pyarrow`) — a floor that won't move is usually one of these.\n- **Adapter majors can change real-API behavior** (transports, structured\n  output defaults) that the fake-model e2e can't exercise — check those paths\n  with a real provider key before trusting the bump in production.\n- **`langfuse` SDK usage here is tiny** (`CallbackHandler` +\n  `auth_check()`), so SDK majors are usually version-bumps; the heavyweight\n  self-host infra requirements belong to the Langfuse *server*, which this\n  repo doesn't bundle.\n\n## Tooling & misc\n\n- **`ruff` formatter output can change on minor bumps** — expect one-time\n  `ruff format` diffs, not just new lint rules.\n- **Uncapped dev-tool majors flood in via `uv lock --upgrade`.** Cap\n  temporarily to keep a disruptive major out of a routine round, and lift the\n  cap in a focused pass.\n- **`numpy` leads Python-version drops** — its floor is usually the first hard\n  constraint when deciding when to drop an old Python minor.\n- **Don't write resolver picks down as requirements.** Pin-reconciliation once\n  produced a `>=` floor on a transitive-only package that nothing required,\n  which later blocked an unrelated upgrade. Transitive-only packages don't\n  belong in `[project] dependencies` at all.\n\n## Python interpreter\n\n- **Validate new Python minors on a stable patch release, not an rc** — rc\n  builds have hit already-fixed interpreter/pydantic bugs that don't reproduce\n  on stable.\n",".claude/skills/smoke-test/SKILL.md":"---\nname: smoke-test\ndescription: >-\n  Run and interpret scripts/smoke_test.sh, the on-demand checks for optional\n  dependencies that CI and the default docker compose don't cover: the Postgres\n  and MongoDB checkpointers, the AG-UI endpoint, and LangFuse tracing. Use when\n  you've touched the memory/checkpointer layer, service startup/health, the\n  AG-UI adapter, LangFuse tracing, or bumped one of those dependencies — and want\n  to confirm the real integration still works without waiting for a full CI run.\n  Also use when deciding WHETHER such a check is even warranted for a change.\n---\n\n# Optional-dependency smoke tests\n\n`scripts/smoke_test.sh` exercises the parts of this project that aren't in the fast\nunit suite or the default CI path because they need real infrastructure: the\n**Postgres** and **MongoDB** checkpointers, the **AG-UI** endpoint, and **LangFuse**\ntracing. It spins up the real dependency in Docker, runs the service against it, and\nverifies the integration end-to-end, then tears everything down.\n\n\"Smoke test\" here means a quick *is-this-obviously-broken* confidence check, not\nexhaustive integration coverage. It's deliberately cheap to run one target and skip the\nrest.\n\n## First: should you run this at all?\n\nMost changes do **not** need it. It pulls images, starts containers, and takes minutes —\nthat time and token cost isn't worth it for work that doesn't touch these paths. Skip it\nfor: adding or editing an agent graph, prompts, or tools; docs; client/Streamlit-only\nchanges; anything already covered by `uv run pytest` and normal CI.\n\n**Run the relevant target when your change touches its path:**\n\n| You changed… | Run |\n| --- | --- |\n| `src/memory/*`, `initialize_database`, a saver/store class, checkpointer wiring | `postgres` and/or `mongo` |\n| `langgraph-checkpoint-postgres` / `-mongodb`, `pymongo`, `psycopg` versions | `postgres` / `mongo` |\n| `src/service/agui.py`, the AG-UI adapter, `ag-ui-*` deps | `agui` |\n| LangFuse tracing (the `CallbackHandler` wiring, `/health` auth check), `langfuse` dep | `langfuse` |\n| Service startup / lifespan / `/health` broadly | the most relevant one or two |\n| Nothing specific — periodic pre-release confidence pass | `all` |\n\nPrefer the **narrowest** target that covers your change. Run `all` (which includes the\nheavy LangFuse stack) only for a deliberate full pass — not as a reflex.\n\n## Running it\n\n```sh\n./scripts/smoke_test.sh                 # default: postgres, mongo, agui\n./scripts/smoke_test.sh mongo           # a single target\n./scripts/smoke_test.sh postgres agui   # a subset\n./scripts/smoke_test.sh langfuse        # heavy: ~5GB stack, run on its own\n./scripts/smoke_test.sh all             # everything, incl. langfuse\n```\n\n`langfuse` is excluded from the default run because it starts LangFuse's full 6-service\nself-host stack (~5GB of images). A green run ends with `--- All smoke tests passed ---`;\nexit code is non-zero on any failure or an unknown target.\n\n## Interpreting the result — and why a green ≠ \"it works\" trap is real\n\nThe whole design point is that **a passing API test does not prove the intended\ndependency was used.** The persistence test just checks that history survives across two\ninvokes, which is true for *any* working checkpointer — including SQLite. So if\n`DATABASE_TYPE=mongo` silently didn't take effect (dropped env, a fallback), the pytest\nstep would still pass while nothing touched Mongo.\n\nThat's why each target does a **second, independent check** that the specific dependency\nwas really exercised, and that's the line you should actually trust:\n\n- `✓ verified: N postgres checkpoint rows for this run's thread`\n- `✓ verified: N mongo checkpoint documents for this run's thread`\n- `✓ verified: N LangFuse traces recorded for this run`\n- `✓ verified: AG-UI streamed a complete run with the expected response`\n\nEach uses a **unique per-run thread id** (`SMOKE_THREAD_ID`), so the count reflects *this*\nrun's data even against a non-empty volume. A `✗ FAIL: … was NOT exercised as intended`\nmeans the dependency wasn't actually hit — treat that as a real failure, not flakiness.\n\nOther guards worth knowing when you read a failure:\n- **Port already in use:** `start_service` refuses to run if something is already on\n  `:8080`, so the health check can't accidentally pass against an unrelated process.\n- **Service died on startup:** it fails fast and dumps the service log instead of waiting\n  out the full timeout.\n- **LangFuse ingestion is async:** traces flow through a worker → ClickHouse pipeline, so\n  the script polls the traces API for a bit (usually a few seconds) before deciding.\n\n## Cloud-environment quirks (Claude Code on the web / sandboxes)\n\nThis script is meant to be runnable by an agent in a sandboxed cloud environment. A few\nthings bite there specifically:\n\n- **Docker daemon may not be running** (and can stop between steps). If `docker` errors\n  with \"Cannot connect to the daemon,\" start it: `(dockerd > /tmp/dockerd.log 2>&1 &)`\n  then wait a few seconds.\n- **Don't build the service image in-sandbox.** Container builds can't reach package\n  registries through the egress proxy (TLS/CA), so `docker compose build` fails. This is\n  exactly why the script runs the service on the **host** via `uv` and only puts the\n  *dependencies* in Docker. Don't \"fix\" it by baking the proxy CA into the committed\n  Dockerfile — that's sandbox-specific.\n- **Registry allowlist for LangFuse.** The LangFuse stack's `minio` image comes from\n  `cgr.dev` (Chainguard). In a restricted-egress environment add **`cgr.dev`** to the\n  network allowlist, or the pull 403s. (If a pull still 403s afterward on a *different*\n  host, add whatever host it names — some registries serve blobs from a separate CDN.)\n- **`pkill -f run_service.py` will kill your own shell.** `-f` matches full command lines,\n  and your command text contains that string, so it self-matches. To free port 8080 use\n  `fuser -k 8080/tcp` instead.\n- **Long foreground `sleep` loops can be killed** by the sandbox. When running the script\n  (or waiting on it), launch it in the background (`nohup … &`) and poll its log file\n  rather than blocking in the foreground.\n- **`docker compose exec` needs `.env`; `docker exec` doesn't.** The host-based flow\n  deliberately omits `.env`, and `docker compose exec` re-parses the full config (which\n  references `env_file: .env`) and fails without it. The script queries containers via\n  `docker exec <id>` (id from `docker compose ps -q`) to sidestep this.\n\n## Extending it\n\nThe script is built around one principle: **every target proves the real dependency was\nused, not just that the API returned something.** Keep that when adding a target.\n\nTo add a new optional-dependency target:\n1. Add a `smoke_<name>()` function following the existing shape: start the dependency\n   (Docker), `start_service` on the host with the right env, run an API-level check, then\n   a **dependency-identity check** (query the dependency directly for this run's data via\n   `assert_positive_count`, or assert on captured output like the AG-UI target).\n2. Register it in the `case` in the target loop and, if it should be part of a full pass,\n   in the `all` expansion. Decide whether it's light enough for the default set (DB-style)\n   or heavy enough to be opt-in (LangFuse-style).\n3. If it needs an add-on compose file, follow the `docker/compose.<dep>.yaml` pattern; if\n   it's a large external stack, prefer fetching upstream's compose pinned to a tag (as the\n   `langfuse` target does via `LANGFUSE_REF`) rather than vendoring it.\n\nKeep these out of the CI docker job: they live in `tests/smoke/` (not `tests/integration/`,\nwhich CI's `test-docker` job scopes to) and are marked `@pytest.mark.docker` so they're\nskipped unless `--run-docker` is passed.\n","CLAUDE.md":"# CLAUDE.md\n\n## Comment style\n\n- Default to no comments in code you add or edit.\n- Only add a comment when the *why* is non-obvious: a hidden constraint, a subtle invariant, or a workaround for a specific bug. Never explain *what* the code does — clear naming should do that.\n- Never write multi-paragraph comment blocks or over-explain inline. Match the terseness of the surrounding code.\n- Exception: brand-new files may start with a short, genuinely useful module/file-level docstring. This does not license verbose inline comments throughout the rest of the file.\n- When editing an existing file, match its existing comment density and style rather than introducing a heavier style than what's already there.\n\n## Maintainer scaffolding vs. template content\n\nThis repo is a GitHub template. `.github/workflows/template-cleanup.yml` strips\nmaintainer-only scaffolding from downstream clones on first instantiation, driven by\nconventions rather than a hardcoded list. Put new scaffolding where the cleanup already\ncovers it:\n\n- Maintainer automation (skills, hooks, scheduled runs) goes in `.claude/` or\n  `docs/maintenance/` — whole-directory buckets, removed entirely, auto-covered.\n- A maintainer-only *workflow* must carry the owner-guard\n  `if: github.repository == 'JoshuaC215/agent-service-toolkit'` line; the cleanup\n  removes any guarded workflow, so this is auto-covered too.\n- ONLY when a maintainer-only file must live in a shared dir (`scripts/`, etc.) and\n  can't carry the guard marker do you add it to the explicit list in\n  `template-cleanup.yml`. Adding such a file without updating the cleanup is the one\n  case that leaks scaffolding into clones.\n",".claude/skills/maintainer-response/SKILL.md":"---\nname: maintainer-response\ndescription: >-\n  Draft replies to GitHub issues and pull requests on JoshuaC215/agent-service-toolkit\n  in the authentic voice of the maintainer (JoshuaC215). Use when triaging, responding\n  to, or reviewing issues/PRs on this repo — e.g. \"respond to issue 290\", \"review the\n  open PRs\", \"draft a reply to this bug report\", or any batch maintainer triage. Produces\n  human-in-the-loop drafts for review; it NEVER posts to GitHub on its own.\n---\n\n# Maintainer Response (JoshuaC215)\n\nDraft issue/PR replies that sound like Joshua and reflect how he actually runs this\nproject. Output is always a **draft for human review** plus a short rationale. You do\nnot post comments, merge, close, or label unless the human explicitly tells you to in a\nseparate, follow-up instruction.\n\n## Operating rules (read first)\n\n1. **Never post or mutate GitHub state by default.** Draft only. Posting, closing,\n   merging, and labeling require an explicit go-ahead naming the specific item.\n2. **Research before drafting.** Read the full issue/PR body, every existing comment,\n   and the actual code/diff being discussed. Confirm whether Joshua has already replied —\n   if he has, you are continuing a thread, not opening one. **Also pull the item's\n   cross-references (linked issues/PRs, siblings tackling the same thing) and read\n   Joshua's prior comments across all of them before drafting — see \"Relate items before\n   drafting\" below.** An item is never triaged in isolation.\n3. **Never fabricate technical claims.** If you assert a file behaves a certain way,\n   verify it in the repo first (cite `path:line`). If you can't verify or can't\n   reproduce, say so and ask for a repro — that is what Joshua does. **For fork PRs whose\n   diff you can't open locally, cite `main` line numbers for context but phrase the fix\n   as \"from the description\" and don't assert what the patch actually does.**\n4. **Flag, don't bluff.** When a decision is genuinely the maintainer's call (accept vs\n   decline a feature, breaking changes, roadmap), present it as a decision for the human\n   with a recommendation, not a fait accompli.\n5. **Match scope to effort.** A vague one-line issue gets a short reply; a substantial,\n   well-tested PR gets real technical review.\n6. **Be concise — don't reiterate, don't lecture (this is the #1 AI tell).** When you're\n   simply agreeing with a contributor, *agree and state the asks* — do not restate the issue,\n   re-explain the mechanism, or quote `file:line` back at someone who clearly already knows\n   their own code. \"Looks good. Two asks before merge: …\" beats a paragraph re-deriving the\n   bug. Reserve file:line citations and root-cause explanations for when they add real\n   information (correcting a misunderstanding, justifying a decline, substantiating a concern\n   the contributor hasn't raised) — not as a demonstration that you read the diff. Shorter and\n   plainer almost always reads as more human. **Never explain back a caveat the contributor\n   already raised in their own PR/issue** (if their PR body already says \"this is\n   defense-in-depth, the real fix is JWTs,\" don't restate that at them — a 3-word\n   acknowledgment plus the actual ask is the whole comment). If a draft for a simple accept or\n   change-request runs more than a few sentences, cut it.\n\n## Relate items before drafting (clusters, not silos)\n\nIssues and PRs on this repo cluster: one feature request usually spawns one or more PRs,\nand multiple contributors often attack the same problem from different angles. **Treating\neach number as an isolated ticket is the single most common failure here** — it produces\ndrafts that suggest closing an issue whose linked PR moved last week, or reply to a PR in\na way that ignores or contradicts feedback Joshua already left on the parent issue. In a\nbatch, do the mapping *first*, before you draft a single reply.\n\n**Build the cluster map first.** For every item in scope, pull cross-references both\ndirections:\n- **PR → issue:** `Fixes/Closes/Resolves #NNN`, plus any `#NNN` referenced in the body or\n  clearly implied by what the diff does.\n- **Issue → PRs:** PRs that reference it, *and* PRs touching the same feature/files even\n  with no explicit link (e.g. two different chat-history PRs against one chat-history issue).\n- **Sibling PRs:** two or more PRs implementing the same request are a cluster even when\n  they never reference each other.\n\n**Then treat each cluster as one thing:**\n1. An issue plus its PRs and their siblings is **one cluster with one coherent position** —\n   not N independent drafts that can contradict each other or the maintainer's earlier call.\n2. **Read Joshua's prior comments across the whole cluster before drafting any member.** A\n   design preference or decision he already stated on the issue (or on a sibling PR) is\n   authoritative: every draft in the cluster must be **consistent with it and build on it** —\n   don't re-open a question he already answered, and don't reply to a PR as if the issue's\n   discussion didn't happen. If a draft would contradict his earlier stance, you misread the\n   thread — re-read it. When two contributors are mid-flight on the same feature, say so and\n   steer them to converge (\"you and @other are both on this — let's settle on #NNN's approach\")\n   rather than reviewing each PR as if it were the only one.\n3. **Cluster-aware staleness.** Nothing is stale in isolation. Before you suggest closing an\n   issue for inactivity, check whether a linked PR has recent activity (and vice-versa): an\n   issue whose parent PR moved last week is *live*. Judge activity across the cluster, not the\n   single number. If the item's own metadata looks stale but a linked item is active, that's a\n   flag-for-the-maintainer, not a close.\n\nDraft per cluster, then split into per-item comments only where each contributor needs a\ndifferent concrete ask — and cross-link those drafts so the relationship is visible.\n\n## Voice & tone\n\n- Warm, casual-professional, first-person singular (\"I think\", \"I'm not inclined\",\n  \"I'll take a look\"). He owns the project as an individual — rarely \"we\".\n- Short, conversational sentences. Contractions throughout.\n- Opens casually: \"Hey\", \"Hi @user\", \"Hmm,\", or just thanks. Often leads with genuine\n  appreciation for contributions.\n- Light, tasteful emoji — at most one or two: 🙏 🫡 😃 :) and `!!` for real enthusiasm.\n  Don't overdo it; **many comments have none — default to zero unless there's genuine\n  enthusiasm**, vary your openers, and don't reuse the same emoji (🙏/🫡) on every draft\n  across a batch.\n- Direct but softened feedback: \"Nit:\", \"I think\", \"my bad\", \"maybe I missed something\".\n  Admits uncertainty openly (\"I'm not able to test this\", \"I'm not sure either\").\n- Closes with a clear next step (\"Should be good to merge after that\", \"open a dedicated\n  issue\", \"let me know if this works\").\n- Uses markdown naturally: ```sh blocks for commands, ```py for runnable examples,\n  bullet lists when enumerating asks, and deep links to `file.py#Lxx` and to LangGraph /\n  LangChain docs. He pastes working code rather than only describing it.\n\n### Stock phrases (reuse, don't copy robotically)\n- Thanks: \"Thanks for contributing, awesome!\" · \"This is cool, thanks @user!\" · \"This is\n  really excellent work 🫡\" · \"This is great 🙏\" · plain \"Thanks!\"\n- Accepting: \"Sounds good, I would welcome a PR for this.\" · \"Open to this idea.\" ·\n  \"I'd welcome a pull request if it wasn't *too* complex and had good tests.\"\n- Requesting changes: \"Can you run the linting and type checking and push the fixes?\" ·\n  \"Nit: ...\" · \"Rest of the changes look great!!\"\n- Declining/deferring: \"I'm not too inclined to take it on.\" · \"It's probably not\n  something I'll have time to develop myself.\" · \"I'd rather use integrations for\n  dedicated tools than build and maintain them in the project directly.\"\n- Scoping: \"maybe open a dedicated issue and discuss it in more detail first before\n  coding — happy to provide feedback early so you don't spend too much time on something\n  that's ultimately rejected.\"\n- Triage: \"Can you post the full repro steps...? Very difficult to debug without more\n  information. Thanks!\"\n- Availability: \"I'm traveling the next few days but will take a look next week, thanks!\"\n\n## The lint/CI ask (use verbatim when a PR needs cleanup)\n\nCI runs ruff format check, ruff check, pyrefly, and pytest (+docker) on every PR to `main`\n(`.github/workflows/test.yml`). When a contributor's PR is failing or unformatted:\n\n```sh\nuv run ruff format\nuv run ruff check --output-format github\nuv run pyrefly check\n```\n\nAsk them to run these and push the fixes. For test expectations, point to `tests/` and\nnote tests are run locally without Docker (`uv sync --frozen` → `pytest`).\n\n**Lead with the substantive ask.** Only attach this lint block when formatting/types are\nactually in question; keep it secondary. When the real blocker is design or behavior\n(e.g. a breaking default), don't let boilerplate lint instructions bury the point.\n\n**Don't frame it as routine.** Most contributors are first-timers who have no reason to know\nthese checks are \"normal\" here — so never say \"the usual lint pass\" / \"as always\" / \"the\nstandard checks.\" That reads as aggressive or cliquey. Just offer the commands plainly and\nhelpfully (\"if you run these and push, that'll get CI green\"). For a simple, agreed change,\na short \"run a lint/type pass\" with the block is plenty — no preamble about CI policy.\n\n## Decision heuristics (what Joshua actually accepts)\n\n> **Current stance (2026 — read this first).** The repo is more dormant than in its early\n> days and Joshua has less time to maintain it, so he is **more conservative about taking on\n> contributions** than the older history suggests. Default toward **decline / defer / close**\n> unless the contribution is **REALLY compelling or REALLY well done *and* tightly scoped**.\n> The bar for \"I'd welcome a PR\" enthusiasm is higher now — reserve it for changes that are\n> genuinely low-maintenance and clearly worth it. When unsure, lean conservative and let\n> demand prove itself (see \"gauge demand\" below) rather than greenlighting eagerly.\n\n**Lean ACCEPT** when the change is:\n- Optional and config-driven (off by default; gated behind a setting), low maintenance\n  burden, aligned with LangGraph/LangChain primitives, and has good tests.\n- A focused bug fix with a clear repro.\n- Requested/upvoted by multiple people (he cites upvotes as signal).\n\n**Lean DECLINE / DEFER** when the change:\n- Adds ongoing maintenance burden or heavy new dependencies.\n- Duplicates an existing integration (LangFuse, LangSmith) — he prefers pointing to\n  dedicated tools over rebuilding them in-repo.\n- Relies on immature/uncertain external protocols, or is large/vague/speculative.\n- Is a big aggregate PR — he steers contributors to **split into focused PRs/issues** and\n  discuss design in an issue *before* writing lots of code.\n\n**Conditional / \"I'd welcome a PR\"**: green-light offers to contribute *selectively* — gate\non tight scope + tests, and ask for a design sketch or docs link first so the contributor\ndoesn't waste effort. Given the current conservative stance, prefer this over an eager yes,\nand only extend a warm \"I'd welcome a PR\" when the idea is genuinely compelling and low-cost\nto maintain.\n\n**Gauge demand instead of committing.** For a reasonable-but-not-compelling feature, the\npreferred move now is to **politely decline as a core feature** while **inviting upvotes /\nconcrete use cases** (\"if others are hitting this, give it a 👍 or chime in — that helps me\nprioritize\"). This keeps the door open without taking on work, and lets real demand surface.\n\n**Close stale / low-activity issues.** Long-open issues with little traction are fine to\n**close politely** to keep the backlog focused — acknowledge any valid point, explain you're\nclosing for low activity, and leave the door open to revisit if demand grows or a better\n(ideally LangGraph-native) option appears. Warm, not curt.\n\n**Apologize for slow responses.** Many threads are months (or ~a year) old. Open with a brief,\ngenuine apology for the delay; when relevant it's fine to give light context (e.g. reduced\nbandwidth / a job change pulled focus away). Keep it short — one clause, then move on.\n\n**Things he won't merge as-is**: secrets/.env checked in, breaking changes flipped on by\ndefault, features he can't test with no owner for future support (he'll ask \"are you OK\nif I point future feedback/errors on this to you?\").\n\n### Be skeptical of automated / AI-generated PRs\nThis repo attracts bulk AI-generated PRs (often with a \"Generated with Claude Code\"\nfooter, \"fix/find-00X\" branch names, or spec docs). Don't rubber-stamp them. Check that:\nthe claimed bug is real and reproducible in this codebase; the fix doesn't introduce a\nbreaking default; the PR has real tests and passing CI (note when CI hasn't run because\nit's from a fork); and the change matches project conventions. It's fine — and on-brand —\nto politely ask the author to confirm the real-world repro, justify a default, or narrow\nscope. Security reports get a courteous, non-defensive reply that asks precisely where\nthe issue surfaces if it isn't already demonstrated.\n\n## Response patterns by category\n\n- **Usage question** → answer directly with the relevant file/line + a short runnable\n  snippet; correct wrong assumptions gently (\"I think the example you copied is the wrong\n  one — you want ...\"). Close the loop (\"let me know if that works\").\n- **Bug report (clear repro)** → confirm, point at the root cause in code, suggest or\n  endorse the fix; invite a PR or note you'll patch it.\n- **Bug report (no repro)** → thank them, ask for full repro steps / versions / model;\n  don't speculate at length.\n- **Feature request** → apply the accept/decline/defer heuristics (default conservative);\n  give the reasoning out loud; offer the path forward — welcome a PR with tests *only if\n  compelling + tightly scoped* / open a design issue first / suggest an existing integration\n  / **decline as a core feature but invite upvotes to gauge demand** / **close if stale with\n  a warm, door-open note**.\n- **PR — good** → brief, genuine agreement, any nits/asks as a short list, the lint ask only\n  if needed, a clear merge gate. Don't re-summarize the PR back to its author. For a simple\n  change you're accepting, \"Looks good. Two asks before merge: …\" is the whole comment. Offer\n  to help only when there's something to help with.\n- **PR — needs design discussion** → appreciate the effort, explain the concern, propose\n  splitting or redesigning, keep the door open.\n- **Contributor is blocked / asked a question** → **answer the actual question first.** Before\n  re-engaging a stale PR with \"add a test / run lint,\" check the PR *and its linked issues* for\n  a direct question the contributor asked (often a \"how do I wire X?\" that went unanswered).\n  Answer it concretely — a short runnable snippet is warranted and on-brand for a genuine \"how\n  do I\" — *then* get to the polish asks. Jumping to test/lint while ignoring their blocker reads\n  as dismissive and is why the PR stalled.\n- **Declining** → keep it to ~4 short sentences of rationale, then the door-open close.\n  Don't over-explain; warmth + a clear reason beats a long justification.\n- **Spam / empty / unintelligible / non-English one-liner** → brief, kind, ask for\n  clarification in English or note what's missing. Don't write paragraphs. For\n  **duplicates**, reference both issue numbers and ask to consolidate into one.\n- **Security report** → courteous, take it seriously, ask precisely where the\n  vuln surfaces if not demonstrated; don't get defensive, don't overpromise.\n\n## Workflow\n\n1. **Identify** the item(s) and pull full context (body, all comments, code/diff, CI\n   status, whether it's a fork, whether Joshua already replied).\n2. **Relate** — build the cluster map (see \"Relate items before drafting\"): link each\n   item to its issues/PRs/siblings and read Joshua's prior comments across the whole\n   cluster. Do this for the entire batch before drafting anything.\n3. **Classify** category + accept/decline/defer disposition — per cluster, so members\n   don't contradict each other.\n4. **Draft** the reply in Joshua's voice, grounded in verified facts and consistent with\n   his earlier stance anywhere in the cluster.\n5. **Report** for each item: a 1-2 line summary of the request, your research assessment\n   (is the claim true? does it reproduce? CI state?), the decision the human needs to make\n   with a clear recommendation, and the draft reply in a quoted block ready to paste.\n   Group items in the same cluster together under a shared header so the relationship is\n   obvious at a glance.\n6. **Wait** for the human's decision. Only post if explicitly told to, for the named item.\n\n## Output format for a batch\n\n**Always hyperlink issue/PR numbers in the report** (summary tables, section titles, and\ninline cross-references) so they're one click to open and compare — never leave them as\nbare `#NNN`. Use the full URL: issues →\n`https://github.com/JoshuaC215/agent-service-toolkit/issues/<NNN>`, PRs →\n`https://github.com/JoshuaC215/agent-service-toolkit/pull/<NNN>`. Markdown form\n`[#NNN](url)`. (This applies to the report you show the human — not to the draft reply\ntext itself, where GitHub auto-links `#NNN` natively.)\n\n**Fencing:** wrap each draft reply in a fenced block. If the draft itself contains a code\nblock (e.g. the ` ```sh ` lint ask), fence the *outer* draft block with **four backticks**\n` ```` ` so the inner triple-backtick block doesn't close it early — otherwise every section\nafter it renders broken. Verify fences are balanced before delivering the report.\n\n**Group clustered items together.** When an issue and its PRs (or sibling PRs) form a\ncluster, present them under one short cluster header that states the shared decision and\nJoshua's governing prior stance, then list the per-item drafts beneath it — so the human\nsees one holistic position, not scattered contradictory drafts. Standalone items just get\ntheir own entry.\n\nFor each item produce:\n\n> **[#NNN](url) — <title>** (`issue`/`PR`, author, age) — category · disposition\n> **Request:** …\n> **Assessment:** … (verified facts, repro, CI, fork status)\n> **Decision needed:** … **Recommendation:** …\n> **Draft reply:**\n> ```\n> <the comment, in Joshua's voice>\n> ```\n\nKeep drafts tight. When in doubt, err toward the shorter, friendlier version Joshua would\nactually type.\n\n## Posting locally (clean attribution)\n\nOnly post when Joshua has explicitly cleared the specific item (see the `Status` column in\n`DRAFT_RESPONSES_REPORT.md`: post ☑️/✅ items; never post ⏳ \"Needs decision\" items, and\nre-confirm 📝 \"Revised — confirm\" items first).\n\n**How the comment is attributed depends entirely on which credential posts it:**\n\n- **Cloud / Claude Code on the web** posts through the Claude GitHub App and a cloud routine\n  layer that *unconditionally* adds a \"with Claude\" badge and a \"Generated by Claude Code\"\n  footer to the comment body. This is **not** suppressible by any setting (the `attribution`\n  key in `settings.json` only affects commit trailers and PR *bodies*, not comments). See\n  anthropics/claude-code#62791.\n- **Locally on Joshua's laptop**, post via the **`gh` CLI authenticated as Joshua**\n  (`gh auth login` / a PAT) — *not* through a GitHub-App-backed MCP server. Posted this way\n  the comment is authored directly by `JoshuaC215` with **no app badge and no footer**,\n  because (a) it's his own token, not an on-behalf-of app, and (b) the body is exactly what\n  you write.\n\n**Local posting rules:**\n1. Use `gh issue comment <n> --body-file <file>` / `gh pr comment <n> --body-file <file>`\n   (body-file avoids shell-escaping issues with multi-line/markdown bodies). Write each body\n   to its own file first and post from the file — never inline the body with `--body \"...\"`.\n   The body-file path can live anywhere (a scratch/temp dir is fine); it does **not** need to\n   be inside the repo.\n2. **Always pass `-R JoshuaC215/agent-service-toolkit` on every `gh` write.** `gh issue\n   comment` / `gh pr comment` infer the repo from the current directory's git remote, so they\n   fail with `fatal: not a git repository` whenever you run from a scratch/temp dir (e.g. where\n   the body files live) or any path outside a clone. The explicit `-R owner/repo` flag makes\n   the command location-independent — use it always rather than relying on `cd`'ing into the\n   clone (and avoid `cd`, which can trigger a permission prompt).\n3. **Know whether each number is an issue or a PR before posting** — `gh issue comment` and\n   `gh pr comment` are **not** interchangeable (using the wrong one errors). When the draft\n   report doesn't already label it, check first:\n   ```sh\n   gh api repos/JoshuaC215/agent-service-toolkit/issues/<n> \\\n     --jq 'if .pull_request then \"PR\" else \"ISSUE\" end'\n   ```\n   (The `issues/<n>` endpoint works for both; the `.pull_request` field is present only on PRs.)\n4. **Do not append any attribution, footer, \"Generated by\", or co-author line to comment\n   bodies.** GitHub auto-links bare `#NNN` and `@user` natively, so keep those un-linked in\n   the comment text. A body that itself contains a fenced code block posts fine from a\n   body-file with no extra escaping.\n5. Confirm identity first with `gh api user --jq .login` → must be `JoshuaC215`. If it isn't\n   (or if writes would route through a Claude-App MCP server), stop and tell Joshua rather\n   than posting with the wrong attribution.\n6. To **comment then close** an issue, chain the close after the comment succeeds:\n   `gh issue comment <n> -R JoshuaC215/agent-service-toolkit --body-file <file> && gh issue close <n> -R JoshuaC215/agent-service-toolkit`.\n7. After posting, record the comment URL back into the report's `Status` (☑️ Posted).\n"},"files":{".claude/skills/dependency-refresh/references/live-e2e.md":"# Live end-to-end verification (no API key needed)\n\nUnit tests mock the LLM and transport, so a running-service check is the only\nway to catch integration regressions from bumps to FastAPI/uvicorn/langgraph/\nthe checkpointers, and a real browser pass is the only thing that catches\nStreamlit-stack UI breakage.\n\n## Fake-model HTTP ladder\n\nThe service ships a fake model (`USE_FAKE_MODEL=true`) that satisfies the \"at\nleast one LLM key\" startup check and returns a canned reply — so you can drive\nthe real HTTP API on the upgraded stack without provider credentials. Leaving\n`AUTH_SECRET` unset keeps the endpoints unauthenticated (no bearer token).\n\nRun it natively (fastest — exercises the same `run_service.py`/uvicorn\nentrypoint and the full dependency stack, just not containerized):\n\n```sh\nPYTHONPATH=src USE_FAKE_MODEL=true HOST=127.0.0.1 PORT=8080 uv run python src/run_service.py &\n# then, once /health is up:\ncurl -s localhost:8080/health                       # {\"status\":\"ok\"}  -> app + lifespan booted\ncurl -s localhost:8080/info                          # agents/models listed\ncurl -s -XPOST localhost:8080/invoke   -H 'content-type: application/json' \\\n     -d '{\"message\":\"hi\",\"agent_id\":\"chatbot\",\"model\":\"fake\"}'         # -> completion + run_id\ncurl -s -N -XPOST localhost:8080/stream -H 'content-type: application/json' \\\n     -d '{\"message\":\"hi\",\"agent_id\":\"chatbot\",\"model\":\"fake\",\"stream_tokens\":true}'  # SSE tokens\n# persistence: invoke twice with the same \"thread_id\", then POST /history for that thread\n# and confirm the checkpointer returns the prior turns (validates the checkpointer packages).\n```\n\nWhat each check validates: `/health` + `/info` = FastAPI/uvicorn/pydantic boot\nand agent wiring; `/invoke` = a full graph run (langgraph + langchain +\nlangsmith `run_id`); `/stream` = the SSE `StreamingResponse` path; `/history`\non a reused `thread_id` = the checkpointer\n(langgraph-checkpoint-sqlite/aiosqlite by default).\n\n## Streamlit UI e2e (browser)\n\nCI never drives the actual Streamlit interface — pytest mocks the transport and\nthe docker CI job only checks health endpoints — so a real browser pass is the\nonly thing that catches Streamlit/pandas/pyarrow-level UI breakage from a bump.\nWith the fake-model service from above still running:\n\n```sh\nuv run streamlit run src/streamlit_app.py --server.headless true --server.port 8501 &\n# quick single-message smoke:\nuv run --with playwright python scripts/smoke_live_app.py http://localhost:8501\n# wider coverage (multi-turn resume, settings selectors, feedback widget, streaming toggle, ...):\nuv run --with playwright python scripts/e2e_ui_tests.py http://localhost:8501\n```\n\n`smoke_live_app.py` sends one chat message and verifies a streamed response\nrenders and settles. `e2e_ui_tests.py` is a small suite of key user journeys\nbuilt on the same idea — run `--list` to see them, or pass names to run a\nsubset. Both exit 0 on pass and save a diagnostic screenshot on failure, and\nboth are URL-parameterized so the same commands also run against the deployed\napp (omit the URL, or set `LIVE_APP_URL`). Run at least the smoke before\nopening any PR that bumps `streamlit`, its rendering stack (pandas, pyarrow,\npillow, altair), or the client/schema code the UI consumes; run the fuller\nsuite when a bump could plausibly touch chat history, settings, or the\nfeedback/streaming paths.\n\n## Containerized test\n\nTo validate the image itself (base image + in-image `uv sync`), run\n`docker compose up --build` and hit the same endpoints on the mapped ports —\nthis mirrors the `test-docker` CI job. It requires pulling the Python slim\nbase image, so it needs outbound Docker Hub access (not available in every\nsandbox; CI handles it via docker-in-docker).\n",".claude/skills/model-refresh/SKILL.md":"---\nname: model-refresh\ndescription: >-\n  Periodically audit the LLM model catalog in src/schema/models.py against what\n  each provider currently ships: add newly released models, remove/flag ones the\n  provider has deprecated or discourages, and re-point DEFAULT_MODEL fallbacks at\n  a current model. Use when asked to \"check for new models\", \"update the model\n  list\", \"refresh the model catalog\", or on the scheduled model-refresh trigger.\n---\n\n# Model Catalog Refresh\n\nKeep `src/schema/models.py` (the `AllModelEnum` catalog) current: add models a\nprovider shipped since the last refresh, remove ones the provider has deprecated\nor is actively steering users away from, and make sure `src/core/settings.py`'s\nper-provider `DEFAULT_MODEL` fallbacks point at a model that still exists.\n\n## Where model config lives\n\n| What | File |\n|---|---|\n| The enum of every supported model, per provider, with the provider's docs URL in each class docstring | `src/schema/models.py` (`AllModelEnum`) |\n| Per-provider default model + which models are \"available\" when a key is set | `src/core/settings.py` (`Settings.model_post_init`) |\n| Enum → API model-string mapping, and provider-specific construction quirks (temperature, streaming, tool-binding) | `src/core/llm.py` (`_MODEL_TABLE`, `get_model`) |\n| Unit tests asserting `get_model` builds the right LangChain class per model | `tests/core/test_llm.py` |\n| Settings/default-model tests | `tests/core/test_settings.py` |\n| Live smoke test (real API calls) | `scripts/check_live_models.py` |\n\n## Workflow\n\n1. **Survey.** For each provider block in `src/schema/models.py`, fetch the docs\n   URL already in that class's docstring and diff its current model list against\n   the enum values present. These docstring URLs are the **single canonical\n   source** for where to look — don't hardcode a second copy of them elsewhere,\n   since provider doc URLs shift over time and duplicated links silently rot.\n   - **If a docstring URL 404s, redirects to a generic landing page, or otherwise\n     no longer points at a model listing:** find the current canonical URL for\n     that provider's model docs and update the docstring to it as part of this\n     same change, then continue the survey from the corrected page. Don't skip\n     the provider just because the old link broke.\n   - A few providers need a second check beyond the docstring URL: Azure OpenAI\n     lags OpenAI's own releases (it's deployment-based — check what base models\n     Azure currently supports), Anthropic has a separate deprecations page worth\n     checking for retirement dates, and Vertex AI model paths sometimes differ\n     from the Gemini API's own names for the same model (e.g. `models/gemini-2.5-flash`\n     vs `gemini-2.5-flash`) — check both columns. Ollama takes a user-supplied\n     model name at runtime, so there's nothing to add there; just confirm the\n     generic pass-through in `llm.py` still works.\n   - **`docs.aws.amazon.com` returns HTTP 403 to `WebFetch`** (it blocks the\n     fetcher's user agent — this is not an egress-policy denial). Use `WebSearch`\n     for Bedrock model IDs instead; it reads the same doc pages server-side and\n     surfaces the model-card and inference-profile-support pages. The per-model\n     card pages (`.../model-card-anthropic-claude-<model>.html`) list the exact\n     `modelId` and profile IDs.\n2. **Classify each gap:**\n   - *New model, generally available* → add it.\n   - *New model, preview/experimental* → use judgment; this repo has taken preview\n     models before (e.g. `gemini-3-pro-preview`) when there's no GA alternative yet,\n     but prefer GA when one exists.\n   - *Existing model deprecated or sunset by the provider* → remove it, unless it's\n     the only model left for that provider (flag that case instead of leaving the\n     catalog empty).\n   - *Existing model merely superseded but still served* → leave it unless the\n     provider's docs explicitly say to migrate off it.\n3. **Match existing naming conventions** when adding a member:\n   - Enum member names are `SCREAMING_SNAKE_CASE`, usually family+size, e.g.\n     `SONNET_45`, `GEMINI_25_PRO`, `LLAMA_33_70B`. Version numbers drop dots\n     (`4.5` → `45`).\n   - Enum values are the exact string the provider's API expects\n     (`claude-sonnet-4-5`, `gemini-2.5-pro`, `gpt-5.1`) — copy verbatim from the\n     provider docs, don't guess.\n   - Values must be unique across *all* the enums in `AllModelEnum` —\n     `tests/schema/test_models.py` enforces this. Where two providers serve the\n     same upstream model one side needs a distinguishing form: `VertexAIModelName`\n     uses the `models/` prefix, so keep it on new Vertex entries.\n   - Keep provider families grouped and roughly ordered by size/generation within\n     a class, matching how they read today.\n4. **Apply changes across every coupled location** — do not edit only the enum:\n   - `src/schema/models.py`: add/remove the `StrEnum` member. Keep the docstring\n     URL current (see step 1).\n   - `src/core/settings.py` `model_post_init`: if the removed model was a\n     `DEFAULT_MODEL` fallback, repoint it at a remaining (ideally cheap/fast) model\n     for that provider — match the intent of the current defaults (e.g.\n     Haiku/Flash/Nano-tier, not the flagship).\n   - `src/core/llm.py`: nothing to do for a plain rename/add (the `_MODEL_TABLE`\n     and `if model_name in ...Name` dispatch are enum-driven), but check for any\n     provider-specific special-casing (see the Groq safeguard-model branch) that\n     might need to apply to the new model too.\n   - `tests/core/test_llm.py`, `tests/core/test_settings.py`: update any test that\n     hardcodes a model value being removed; add a case for a notable new model if\n     it has special handling (temperature override, tool-binding quirk, etc).\n   - `.env.example`, other docs: update only if they name a specific model that\n     changed (most don't).\n5. **Do not touch provider credentials or add live network calls during the\n   survey step.** This step is pure research + code edit, no API keys needed.\n6. **Live-test what you changed** (needs API keys — see \"Live testing\" below).\n7. **Summarize** what was added/removed/repointed and why, citing the provider\n   doc for each change. Flag any model rename explicitly — it's a breaking change\n   for existing deployments pinning the old enum value in `DEFAULT_MODEL`/\n   `AVAILABLE_MODELS` env config, not something to swap silently. Commit and push\n   per the repo's normal git workflow; open a PR only if asked.\n\n## Providers you can't live-verify (Bedrock, Azure, Vertex service-account, DeepSeek, OpenRouter)\n\nNot having a key for a provider is **not** a reason to skip it — a stale or\nbroken catalog entry is worse than a doc-sourced one. Update these from docs\njust like the rest, and in the PR mark them explicitly as **doc-only /\nunverified**, citing the provider page and calling out any caveat below so the\nnext person with a key knows exactly what to spot-check. Only leave a provider\nuntouched when the docs themselves are ambiguous *and* the change would be a\nproduct decision (e.g. adding a whole new pricing tier), not a freshness update.\n\n- **AWS Bedrock** — the enum *value* is passed straight to `ChatBedrock(model_id=...)`,\n  so it must be a real Bedrock ID, not a friendly label. Two gotchas:\n  1. The latest Claude models are **not invocable on-demand by their base model\n     ID** — a bare `anthropic.claude-...` call 400s with \"on-demand throughput\n     isn't supported.\" They must go through a cross-region **inference profile**:\n     the base ID prefixed with a geo (`us.`/`eu.`/`apac.`) or `global.`. Prefer\n     `global.` (routes dynamically, region-agnostic — the best fit for a catalog\n     value with no region context) and note in the PR that single-region\n     deployments not enrolled in Global CRIS should swap the prefix for their geo.\n  2. Bedrock inherits the same **sampling-parameter restrictions** as the direct\n     Anthropic API — e.g. a Sonnet-5-class model rejects `temperature`. If you\n     point a Bedrock entry at such a model, mirror the no-`temperature` branch\n     that already exists for it in `llm.py`'s Anthropic and Bedrock dispatch.\n  If AWS credentials happen to be present, `boto3.client(\"bedrock\").list_foundation_models()`\n  is the fastest way to confirm real IDs — but note that Bedrock access is a\n  separate enablement from plain AWS creds, so this can fail with an auth error\n  even when other AWS calls would work.\n- **Azure OpenAI** is the one genuinely heavier lift, because it's deployment-based\n  and the catalog is coupled in more places than the enum:\n  - `settings.py` `model_post_init` hardcodes a `required_models` set (currently\n    `{\"gpt-4o\", \"gpt-4o-mini\"}`) that it validates the `AZURE_OPENAI_DEPLOYMENT_MAP`\n    against — bumping the enum means bumping that set and the `.env.example`\n    deployment-map sample and the ~7 Azure cases in `tests/core/test_settings.py`.\n  - `llm.py` hardcodes `temperature=0.5` for the Azure path, but Azure's GPT-5-era\n    **reasoning** variants reject `temperature` (400). If you move Azure onto one,\n    add a no-`temperature` branch like the Anthropic/Bedrock Sonnet-5 handling.\n  - Changing an Azure enum value is a **breaking change** to every user's\n    deployment map (they name deployments after these keys). Treat an Azure\n    generation bump as its own reviewed change, and flag the deployment-map break\n    loudly — don't fold it silently into a routine refresh.\n\n## Live testing\n\n`scripts/check_live_models.py` sends a trivial one-word prompt to every model of\nevery provider that has credentials configured in the environment, and reports\nPASS/FAIL/SKIP per model. It is deliberately outside the pytest suite (real\nnetwork calls, tiny real cost) — run it by hand or from a scheduled trigger with\nkeys populated:\n\n```sh\nPYTHONPATH=src uv run python scripts/check_live_models.py                    # all configured providers\nPYTHONPATH=src uv run python scripts/check_live_models.py --provider anthropic google\n```\n\nA `SKIP` line means no credentials were present for that provider — that's\nexpected and not a failure. Only treat `FAIL` rows as build-blocking. Cost is\nnegligible (a handful of few-token completions per provider) but it's real spend\nagainst real keys — don't wire it into CI or run it on every commit.\n\nIf no provider credentials are configured in the current environment, skip this\nstep entirely rather than failing — the survey/edit step is still fully useful on\nits own.\n",".claude/skills/dependency-refresh/SKILL.md":"---\nname: dependency-refresh\ndescription: >-\n  Run a dependency & version refresh for this repo: Python libraries in\n  pyproject.toml/uv.lock, Docker base images, GitHub Actions pins, the uv CLI\n  version (CI + README + Dockerfiles), the supported Python range, and the\n  infra images (postgres/mongo/LangFuse) used by compose and the smoke tests.\n  Use when asked to \"update dependencies\", \"bump versions\", \"do a dependency\n  refresh\", or on the scheduled monthly dependency-refresh run. Self-contained\n  playbook; prior state (deferred majors, cooldown dates) lives in the previous\n  refresh PR, which Step 0 tells you how to find.\n---\n\n# Dependency & Version Refresh\n\nThis skill is the complete playbook. Running state — what moved each round and\nwhich majors are deferred — lives in **refresh PR descriptions**, not in a file.\n\nSupporting references in this skill's directory — read them at the step that\nneeds them, not up front:\n\n- `references/coupling-constraints.md` — project-specific coupling constraints\n  and gotchas learned in past refreshes (langchain⇄langgraph lockstep,\n  checkpointer majors, Actions runtime deprecations, uv quirks, …). Skim the\n  headings before triage; read fully when a resolver conflict or CI surprise hits.\n- `references/live-e2e.md` — the live end-to-end verification recipe\n  (fake-model HTTP ladder + Streamlit browser tests).\n\n## Conventions: PRs are the state record\n\nEvery refresh produces exactly one PR that both delivers the changes and\nrecords the state the *next* refresh starts from:\n\n- **Branch:** `claude/dependency-refresh-YYYY-MM-DD`\n- **Title:** `chore(deps): dependency refresh YYYY-MM-DD`\n- **Body sections** (the template):\n\n```markdown\n## What moved\n<pin raises, lock-only movement, Actions/uv CLI, infra images, base images —\ngrouped, with one-line release-note checks for anything 0.x or major>\n\n## Deferred majors & cooldowns\n<carry this table forward from the previous refresh PR: drop rows that landed\n(note them under \"What moved\"), keep rows still deferred, add newly discovered\nmajors. Every key-dependency major gets a row the round it is first seen.>\n\n| Upgrade | From → To | Major released | Eligible from | Notes / ROI |\n| --- | --- | --- | --- | --- |\n\n## Verification\n<checks run and their results, including which smoke targets were re-run and\nwhich were deliberately skipped and why>\n```\n\nNever stack extra state elsewhere: no journal files, no backlog docs. If a\nlearning is *reusable knowledge* (a coupling constraint, a gotcha), record it in\n`references/coupling-constraints.md` in the same PR — that file is knowledge,\nnot state.\n\n## Step 0 — Recover state from the last refresh PR\n\nBefore touching any pin, find the most recent refresh PR and read its body:\n\n1. Search PRs (state: all, newest first) with query\n   `\"dependency refresh\" in:title` scoped to this repo.\n2. Fallback: search by head-branch prefix `head:claude/dependency-refresh`.\n3. Last resort: `git log` on `main` for commits touching `uv.lock`, then look up\n   their PRs.\n\nFrom the latest one (or two, if the latest was narrow), extract: the deferred\nmajors table, anything \"deliberately held back\", and verification caveats\n(e.g. \"MongoDB untested\"). That table seeds this round's triage; carry it\nforward per the template above.\n\n## Cooldown policy\n\nTwo layers, one mechanism where possible:\n\n**1. Global 14-day resolver cooldown (enforced by uv).** `pyproject.toml` has:\n\n```toml\n[tool.uv]\nexclude-newer = \"14 days\"\n```\n\nEvery resolution (`uv lock`, `uv lock --upgrade`, `uv sync` re-locks) only\nconsiders releases at least 14 days old, so a compromised or broken\nfresh release can't enter the lock during its highest-risk window. This\nautomatically limits pin raises too: raising a `~=` pin to a version younger\nthan 14 days makes the resolve fail with an `exclude-newer` hint — that is the\npolicy working, not an error to route around. Hold that bump for the next\nround instead of overriding.\n\n**Security exception (the only sanctioned override):** if a release fixes a\nvulnerability that affects this repo (Dependabot PR, advisory), take it\nimmediately regardless of age — add a temporary per-package override with a\ncomment, and remove it in the next refresh once the release is older than the\nglobal cooldown:\n\n```toml\n[tool.uv.exclude-newer-package]\nsomepkg = \"0 days\"  # security fix CVE-XXXX; remove once >14 days old\n```\n\nCooldown mechanics to know: the span is recorded in `uv.lock`\n(`exclude-newer-span`), re-locks are stable, and it needs the repo's pinned uv\nversion — see the uv entries in `references/coupling-constraints.md`.\n\n**2. Three-month major cooldown for key dependencies (enforced by process).**\nKey dependencies are the ones where a bad major is expensive or sticky:\n\n- **Infra images:** `postgres`, `mongo` (compose/smoke tests) — majors can\n  change on-disk formats.\n- **App platform:** `streamlit`, `fastapi`, `pydantic`.\n- **Core agent stack:** `langgraph` + `langchain` and the coupled\n  `langgraph-checkpoint-*` packages.\n\nA new major of a key dependency is not adopted until **at least 3 months after\nits X.0.0 release date**, and then only in its own dedicated PR (never inside a\nsafe-bumps round). The round that first sees the new major adds a row to the\ndeferred table with the major's release date and the computed \"eligible from\"\ndate; subsequent rounds carry the row until it lands or is rejected. Eligible ≠\nautomatic: when the date arrives, do the normal major triage (below) before\nlanding it. Non-key majors need no 3-month wait — just ordinary triage.\n\n## Where versions live\n\n| What | File |\n| --- | --- |\n| Runtime deps + version pins | `pyproject.toml` → `[project] dependencies` |\n| Dev tooling (ruff, pyrefly, pytest, …) | `pyproject.toml` → `[dependency-groups] dev` |\n| Minimal client/Streamlit deps (a subset, **kept in sync** with the main list) | `pyproject.toml` → `[dependency-groups] client` |\n| Fully resolved versions (source of truth for what installs) | `uv.lock` |\n| Resolver cooldown | `pyproject.toml` → `[tool.uv] exclude-newer` |\n| Lint/format target Python | `pyproject.toml` → `[tool.ruff] target-version` |\n| CI test matrix | `.github/workflows/test.yml` (`python-version`) |\n| Container base image | `docker/Dockerfile.app`, `docker/Dockerfile.service` |\n| Supported Python range + classifiers | `pyproject.toml` → `requires-python`, `classifiers` |\n| GitHub Actions versions (`actions/checkout`, `setup-python`, `setup-uv`, `docker/*`, `codecov-action`, …) | `.github/workflows/*.yml` (`uses:`) |\n| `uv` CLI version — **four places, keep in sync** | `.github/workflows/test.yml` (both `setup-uv` steps' `version:`), `docker/Dockerfile.app` + `docker/Dockerfile.service` (`pip install uv==`), `README.md` quickstart (`curl .../uv/<version>/install.sh`) |\n| Infra images used by compose + smoke tests | `compose.yaml` (`postgres:` tag), `docker/compose.mongo.yaml` (`mongo:` tag), `scripts/smoke_test.sh` (`LANGFUSE_REF`) |\n\n## The workflow\n\n1. **Recover state** (Step 0 above).\n2. **Survey.** Compare resolved versions in `uv.lock` against the latest on\n   PyPI (`https://pypi.org/pypi/<package>/json` → `info.version`,\n   `info.requires_dist`, `info.requires_python`; each release's file\n   `upload_time_iso_8601` gives the age for cooldown checks). Also survey the\n   non-Python surfaces from the table above: Actions pins, uv CLI, Docker base\n   images, infra image tags, `LANGFUSE_REF`.\n3. **Triage** each candidate (principles below), honoring both cooldowns.\n   New key-dependency majors get a deferred-table row, not a bump.\n4. **Apply safe bumps** by editing pins in `pyproject.toml`. Preserve the\n   existing style: `~=` for app deps, `>=` floors for loosely-pinned libs.\n5. **Re-resolve:** `uv lock --upgrade`. Read conflicts carefully — they name\n   the exact transitive constraint (or cooldown) blocking a bump; check\n   `references/coupling-constraints.md` for known ones.\n6. **Reconcile pins to the lock.** Where the resolver picked higher than the\n   written pin (common for `>=` floors), raise the written pin to match — after\n   which `uv lock` must report no changes. **Sanity-check each reconciled floor\n   against what actual dependents require** before writing it down (grep the\n   package's block in `uv.lock` for reverse-dep `specifier`s) — otherwise a\n   floor nothing needs can block a future upgrade.\n7. **Sync + static verify:** `uv sync --frozen`, then `uv run ruff check .`,\n   `uv run ruff format --check .`, `uv run pyrefly check`, `uv run pytest`.\n8. **Live e2e:** follow `references/live-e2e.md` (fake-model HTTP ladder;\n   Streamlit browser smoke when the UI stack moved). If a bump touches\n   checkpointers, AG-UI, or LangFuse, also run the relevant\n   `scripts/smoke_test.sh` target (see the smoke-test skill; needs the Docker\n   daemon — `sudo dockerd &` if it isn't running).\n9. **PR:** branch, title, and body per the conventions above. Never push to\n   main. Record new reusable gotchas in the coupling reference in the same PR.\n\n## Triage principles\n\n- **Minor/patch bumps** within a major are generally safe — batch them.\n- **Major bumps** (and pre-1.0 `0.x` minors, which don't promise SemVer\n  stability): assess the actual code/behavior change against how the repo\n  *uses* the package, weigh ROI, and hold non-trivial ones for their own PR.\n  Grep for the import first — often the repo touches only a tiny, stable part\n  of the API.\n- **Key-dependency majors** additionally wait out the 3-month cooldown.\n- **Version-coupled packages** move in lockstep and a bump in one drags others\n  — the known couplings are in `references/coupling-constraints.md`; the\n  resolver will find the rest.\n- **Transitive-only deps** (nothing in the repo imports them) are lower risk\n  for our code; their risk lives in whatever consumes them (e.g.\n  Streamlit ↔ pandas/pyarrow). Don't add them to `[project] dependencies`\n  just to pin them.\n\n## Python version policy\n\nFollow CPython's cycle: a new minor each October, ~5 years of support (~18\nmonths bugfix, then security-only). Keep the declared range\n(`requires-python`), classifiers, CI matrix, ruff `target-version`, and Docker\nbase images mutually consistent — all listed in the table above. Adopt a new\nminor once the dependency stack resolves and passes on it; drop a minor when\nit nears security-EOL or a needed dependency drops it first (numpy is usually\nthe leading indicator). Verify support claims on a real interpreter of that\nversion (`uv python install 3.X`, then the full check ladder) — and use a\nstable patch release, not an `rc` (see the coupling reference).\n",".claude/skills/dependency-refresh/references/coupling-constraints.md":"# Coupling constraints & gotchas\n\nProject-specific constraints hit during real refreshes — check here before\nfighting a resolver conflict or a CI surprise, and add new ones in the same PR\nthat hits them. Key points only; verify current specifics against the resolver\nand release notes.\n\n## uv & the cooldown mechanism\n\n- **Relative `exclude-newer` needs a recent uv.** Older uv warns\n  (`Failed to parse pyproject.toml during settings discovery`), ignores the\n  whole `[tool.uv]` table, and a re-lock with it silently strips the cooldown\n  metadata from `uv.lock`. Always lock with the repo's pinned uv version.\n- **A cooldown conflict looks like an unsatisfiable pin** — `uv lock` fails\n  with an `exclude-newer` hint. That's enforcement, not breakage: hold the\n  bump, or (security fixes only) add a commented temporary\n  `[tool.uv.exclude-newer-package]` override per SKILL.md.\n- **Re-locks refetch index metadata**, so unrelated wheel entries can drift out\n  of `uv.lock` with no version change. Harmless for supported platforms.\n\n## GitHub Actions\n\n- **Node runtime deprecations show up as CI warnings, not lockfile conflicts**,\n  and pinned `@vN` refs don't auto-upgrade. Check each action's `runs.using`\n  — including the nested `uses:` lines inside composite actions — and bump to\n  a major that ships the current runtime. Watch for behavior changes riding\n  along (e.g. `docker/build-push-action` turning on provenance attestations\n  by default).\n- **Some actions stop publishing floating major tags** for supply-chain\n  hardening (`astral-sh/setup-uv` as of v8) and must be pinned to an exact\n  release.\n\n## LangChain / LangGraph ecosystem\n\n- **`langchain` ⇄ `langgraph` move in lockstep** — the meta-package pins a\n  narrow `langgraph` range, so neither can be bumped past the other.\n- **`langgraph` ⇄ `langgraph-checkpoint` ⇄ the checkpointer packages** upgrade\n  as one coupled unit, and checkpointer majors can change stored-checkpoint\n  compatibility or drop APIs (the mongodb saver dropped its async class;\n  `src/memory/mongodb.py` bridges it). The test suite mocks the real backends,\n  so verify old checkpoints still read under new savers manually.\n- **LLM SDK bumps chain**: `langchain-<provider>` pulls a newer provider SDK\n  which drags its own floors (e.g. `openai` → `jiter`). Expect to move the\n  whole chain together.\n- **Provider adapters can cap shared libs** (e.g. `langchain-google-vertexai`\n  capping `pyarrow`) — a floor that won't move is usually one of these.\n- **Adapter majors can change real-API behavior** (transports, structured\n  output defaults) that the fake-model e2e can't exercise — check those paths\n  with a real provider key before trusting the bump in production.\n- **`langfuse` SDK usage here is tiny** (`CallbackHandler` +\n  `auth_check()`), so SDK majors are usually version-bumps; the heavyweight\n  self-host infra requirements belong to the Langfuse *server*, which this\n  repo doesn't bundle.\n\n## Tooling & misc\n\n- **`ruff` formatter output can change on minor bumps** — expect one-time\n  `ruff format` diffs, not just new lint rules.\n- **Uncapped dev-tool majors flood in via `uv lock --upgrade`.** Cap\n  temporarily to keep a disruptive major out of a routine round, and lift the\n  cap in a focused pass.\n- **`numpy` leads Python-version drops** — its floor is usually the first hard\n  constraint when deciding when to drop an old Python minor.\n- **Don't write resolver picks down as requirements.** Pin-reconciliation once\n  produced a `>=` floor on a transitive-only package that nothing required,\n  which later blocked an unrelated upgrade. Transitive-only packages don't\n  belong in `[project] dependencies` at all.\n\n## Python interpreter\n\n- **Validate new Python minors on a stable patch release, not an rc** — rc\n  builds have hit already-fixed interpreter/pydantic bugs that don't reproduce\n  on stable.\n",".claude/skills/smoke-test/SKILL.md":"---\nname: smoke-test\ndescription: >-\n  Run and interpret scripts/smoke_test.sh, the on-demand checks for optional\n  dependencies that CI and the default docker compose don't cover: the Postgres\n  and MongoDB checkpointers, the AG-UI endpoint, and LangFuse tracing. Use when\n  you've touched the memory/checkpointer layer, service startup/health, the\n  AG-UI adapter, LangFuse tracing, or bumped one of those dependencies — and want\n  to confirm the real integration still works without waiting for a full CI run.\n  Also use when deciding WHETHER such a check is even warranted for a change.\n---\n\n# Optional-dependency smoke tests\n\n`scripts/smoke_test.sh` exercises the parts of this project that aren't in the fast\nunit suite or the default CI path because they need real infrastructure: the\n**Postgres** and **MongoDB** checkpointers, the **AG-UI** endpoint, and **LangFuse**\ntracing. It spins up the real dependency in Docker, runs the service against it, and\nverifies the integration end-to-end, then tears everything down.\n\n\"Smoke test\" here means a quick *is-this-obviously-broken* confidence check, not\nexhaustive integration coverage. It's deliberately cheap to run one target and skip the\nrest.\n\n## First: should you run this at all?\n\nMost changes do **not** need it. It pulls images, starts containers, and takes minutes —\nthat time and token cost isn't worth it for work that doesn't touch these paths. Skip it\nfor: adding or editing an agent graph, prompts, or tools; docs; client/Streamlit-only\nchanges; anything already covered by `uv run pytest` and normal CI.\n\n**Run the relevant target when your change touches its path:**\n\n| You changed… | Run |\n| --- | --- |\n| `src/memory/*`, `initialize_database`, a saver/store class, checkpointer wiring | `postgres` and/or `mongo` |\n| `langgraph-checkpoint-postgres` / `-mongodb`, `pymongo`, `psycopg` versions | `postgres` / `mongo` |\n| `src/service/agui.py`, the AG-UI adapter, `ag-ui-*` deps | `agui` |\n| LangFuse tracing (the `CallbackHandler` wiring, `/health` auth check), `langfuse` dep | `langfuse` |\n| Service startup / lifespan / `/health` broadly | the most relevant one or two |\n| Nothing specific — periodic pre-release confidence pass | `all` |\n\nPrefer the **narrowest** target that covers your change. Run `all` (which includes the\nheavy LangFuse stack) only for a deliberate full pass — not as a reflex.\n\n## Running it\n\n```sh\n./scripts/smoke_test.sh                 # default: postgres, mongo, agui\n./scripts/smoke_test.sh mongo           # a single target\n./scripts/smoke_test.sh postgres agui   # a subset\n./scripts/smoke_test.sh langfuse        # heavy: ~5GB stack, run on its own\n./scripts/smoke_test.sh all             # everything, incl. langfuse\n```\n\n`langfuse` is excluded from the default run because it starts LangFuse's full 6-service\nself-host stack (~5GB of images). A green run ends with `--- All smoke tests passed ---`;\nexit code is non-zero on any failure or an unknown target.\n\n## Interpreting the result — and why a green ≠ \"it works\" trap is real\n\nThe whole design point is that **a passing API test does not prove the intended\ndependency was used.** The persistence test just checks that history survives across two\ninvokes, which is true for *any* working checkpointer — including SQLite. So if\n`DATABASE_TYPE=mongo` silently didn't take effect (dropped env, a fallback), the pytest\nstep would still pass while nothing touched Mongo.\n\nThat's why each target does a **second, independent check** that the specific dependency\nwas really exercised, and that's the line you should actually trust:\n\n- `✓ verified: N postgres checkpoint rows for this run's thread`\n- `✓ verified: N mongo checkpoint documents for this run's thread`\n- `✓ verified: N LangFuse traces recorded for this run`\n- `✓ verified: AG-UI streamed a complete run with the expected response`\n\nEach uses a **unique per-run thread id** (`SMOKE_THREAD_ID`), so the count reflects *this*\nrun's data even against a non-empty volume. A `✗ FAIL: … was NOT exercised as intended`\nmeans the dependency wasn't actually hit — treat that as a real failure, not flakiness.\n\nOther guards worth knowing when you read a failure:\n- **Port already in use:** `start_service` refuses to run if something is already on\n  `:8080`, so the health check can't accidentally pass against an unrelated process.\n- **Service died on startup:** it fails fast and dumps the service log instead of waiting\n  out the full timeout.\n- **LangFuse ingestion is async:** traces flow through a worker → ClickHouse pipeline, so\n  the script polls the traces API for a bit (usually a few seconds) before deciding.\n\n## Cloud-environment quirks (Claude Code on the web / sandboxes)\n\nThis script is meant to be runnable by an agent in a sandboxed cloud environment. A few\nthings bite there specifically:\n\n- **Docker daemon may not be running** (and can stop between steps). If `docker` errors\n  with \"Cannot connect to the daemon,\" start it: `(dockerd > /tmp/dockerd.log 2>&1 &)`\n  then wait a few seconds.\n- **Don't build the service image in-sandbox.** Container builds can't reach package\n  registries through the egress proxy (TLS/CA), so `docker compose build` fails. This is\n  exactly why the script runs the service on the **host** via `uv` and only puts the\n  *dependencies* in Docker. Don't \"fix\" it by baking the proxy CA into the committed\n  Dockerfile — that's sandbox-specific.\n- **Registry allowlist for LangFuse.** The LangFuse stack's `minio` image comes from\n  `cgr.dev` (Chainguard). In a restricted-egress environment add **`cgr.dev`** to the\n  network allowlist, or the pull 403s. (If a pull still 403s afterward on a *different*\n  host, add whatever host it names — some registries serve blobs from a separate CDN.)\n- **`pkill -f run_service.py` will kill your own shell.** `-f` matches full command lines,\n  and your command text contains that string, so it self-matches. To free port 8080 use\n  `fuser -k 8080/tcp` instead.\n- **Long foreground `sleep` loops can be killed** by the sandbox. When running the script\n  (or waiting on it), launch it in the background (`nohup … &`) and poll its log file\n  rather than blocking in the foreground.\n- **`docker compose exec` needs `.env`; `docker exec` doesn't.** The host-based flow\n  deliberately omits `.env`, and `docker compose exec` re-parses the full config (which\n  references `env_file: .env`) and fails without it. The script queries containers via\n  `docker exec <id>` (id from `docker compose ps -q`) to sidestep this.\n\n## Extending it\n\nThe script is built around one principle: **every target proves the real dependency was\nused, not just that the API returned something.** Keep that when adding a target.\n\nTo add a new optional-dependency target:\n1. Add a `smoke_<name>()` function following the existing shape: start the dependency\n   (Docker), `start_service` on the host with the right env, run an API-level check, then\n   a **dependency-identity check** (query the dependency directly for this run's data via\n   `assert_positive_count`, or assert on captured output like the AG-UI target).\n2. Register it in the `case` in the target loop and, if it should be part of a full pass,\n   in the `all` expansion. Decide whether it's light enough for the default set (DB-style)\n   or heavy enough to be opt-in (LangFuse-style).\n3. If it needs an add-on compose file, follow the `docker/compose.<dep>.yaml` pattern; if\n   it's a large external stack, prefer fetching upstream's compose pinned to a tag (as the\n   `langfuse` target does via `LANGFUSE_REF`) rather than vendoring it.\n\nKeep these out of the CI docker job: they live in `tests/smoke/` (not `tests/integration/`,\nwhich CI's `test-docker` job scopes to) and are marked `@pytest.mark.docker` so they're\nskipped unless `--run-docker` is passed.\n","CLAUDE.md":"# CLAUDE.md\n\n## Comment style\n\n- Default to no comments in code you add or edit.\n- Only add a comment when the *why* is non-obvious: a hidden constraint, a subtle invariant, or a workaround for a specific bug. Never explain *what* the code does — clear naming should do that.\n- Never write multi-paragraph comment blocks or over-explain inline. Match the terseness of the surrounding code.\n- Exception: brand-new files may start with a short, genuinely useful module/file-level docstring. This does not license verbose inline comments throughout the rest of the file.\n- When editing an existing file, match its existing comment density and style rather than introducing a heavier style than what's already there.\n\n## Maintainer scaffolding vs. template content\n\nThis repo is a GitHub template. `.github/workflows/template-cleanup.yml` strips\nmaintainer-only scaffolding from downstream clones on first instantiation, driven by\nconventions rather than a hardcoded list. Put new scaffolding where the cleanup already\ncovers it:\n\n- Maintainer automation (skills, hooks, scheduled runs) goes in `.claude/` or\n  `docs/maintenance/` — whole-directory buckets, removed entirely, auto-covered.\n- A maintainer-only *workflow* must carry the owner-guard\n  `if: github.repository == 'JoshuaC215/agent-service-toolkit'` line; the cleanup\n  removes any guarded workflow, so this is auto-covered too.\n- ONLY when a maintainer-only file must live in a shared dir (`scripts/`, etc.) and\n  can't carry the guard marker do you add it to the explicit list in\n  `template-cleanup.yml`. Adding such a file without updating the cleanup is the one\n  case that leaks scaffolding into clones.\n",".claude/skills/maintainer-response/SKILL.md":"---\nname: maintainer-response\ndescription: >-\n  Draft replies to GitHub issues and pull requests on JoshuaC215/agent-service-toolkit\n  in the authentic voice of the maintainer (JoshuaC215). Use when triaging, responding\n  to, or reviewing issues/PRs on this repo — e.g. \"respond to issue 290\", \"review the\n  open PRs\", \"draft a reply to this bug report\", or any batch maintainer triage. Produces\n  human-in-the-loop drafts for review; it NEVER posts to GitHub on its own.\n---\n\n# Maintainer Response (JoshuaC215)\n\nDraft issue/PR replies that sound like Joshua and reflect how he actually runs this\nproject. Output is always a **draft for human review** plus a short rationale. You do\nnot post comments, merge, close, or label unless the human explicitly tells you to in a\nseparate, follow-up instruction.\n\n## Operating rules (read first)\n\n1. **Never post or mutate GitHub state by default.** Draft only. Posting, closing,\n   merging, and labeling require an explicit go-ahead naming the specific item.\n2. **Research before drafting.** Read the full issue/PR body, every existing comment,\n   and the actual code/diff being discussed. Confirm whether Joshua has already replied —\n   if he has, you are continuing a thread, not opening one. **Also pull the item's\n   cross-references (linked issues/PRs, siblings tackling the same thing) and read\n   Joshua's prior comments across all of them before drafting — see \"Relate items before\n   drafting\" below.** An item is never triaged in isolation.\n3. **Never fabricate technical claims.** If you assert a file behaves a certain way,\n   verify it in the repo first (cite `path:line`). If you can't verify or can't\n   reproduce, say so and ask for a repro — that is what Joshua does. **For fork PRs whose\n   diff you can't open locally, cite `main` line numbers for context but phrase the fix\n   as \"from the description\" and don't assert what the patch actually does.**\n4. **Flag, don't bluff.** When a decision is genuinely the maintainer's call (accept vs\n   decline a feature, breaking changes, roadmap), present it as a decision for the human\n   with a recommendation, not a fait accompli.\n5. **Match scope to effort.** A vague one-line issue gets a short reply; a substantial,\n   well-tested PR gets real technical review.\n6. **Be concise — don't reiterate, don't lecture (this is the #1 AI tell).** When you're\n   simply agreeing with a contributor, *agree and state the asks* — do not restate the issue,\n   re-explain the mechanism, or quote `file:line` back at someone who clearly already knows\n   their own code. \"Looks good. Two asks before merge: …\" beats a paragraph re-deriving the\n   bug. Reserve file:line citations and root-cause explanations for when they add real\n   information (correcting a misunderstanding, justifying a decline, substantiating a concern\n   the contributor hasn't raised) — not as a demonstration that you read the diff. Shorter and\n   plainer almost always reads as more human. **Never explain back a caveat the contributor\n   already raised in their own PR/issue** (if their PR body already says \"this is\n   defense-in-depth, the real fix is JWTs,\" don't restate that at them — a 3-word\n   acknowledgment plus the actual ask is the whole comment). If a draft for a simple accept or\n   change-request runs more than a few sentences, cut it.\n\n## Relate items before drafting (clusters, not silos)\n\nIssues and PRs on this repo cluster: one feature request usually spawns one or more PRs,\nand multiple contributors often attack the same problem from different angles. **Treating\neach number as an isolated ticket is the single most common failure here** — it produces\ndrafts that suggest closing an issue whose linked PR moved last week, or reply to a PR in\na way that ignores or contradicts feedback Joshua already left on the parent issue. In a\nbatch, do the mapping *first*, before you draft a single reply.\n\n**Build the cluster map first.** For every item in scope, pull cross-references both\ndirections:\n- **PR → issue:** `Fixes/Closes/Resolves #NNN`, plus any `#NNN` referenced in the body or\n  clearly implied by what the diff does.\n- **Issue → PRs:** PRs that reference it, *and* PRs touching the same feature/files even\n  with no explicit link (e.g. two different chat-history PRs against one chat-history issue).\n- **Sibling PRs:** two or more PRs implementing the same request are a cluster even when\n  they never reference each other.\n\n**Then treat each cluster as one thing:**\n1. An issue plus its PRs and their siblings is **one cluster with one coherent position** —\n   not N independent drafts that can contradict each other or the maintainer's earlier call.\n2. **Read Joshua's prior comments across the whole cluster before drafting any member.** A\n   design preference or decision he already stated on the issue (or on a sibling PR) is\n   authoritative: every draft in the cluster must be **consistent with it and build on it** —\n   don't re-open a question he already answered, and don't reply to a PR as if the issue's\n   discussion didn't happen. If a draft would contradict his earlier stance, you misread the\n   thread — re-read it. When two contributors are mid-flight on the same feature, say so and\n   steer them to converge (\"you and @other are both on this — let's settle on #NNN's approach\")\n   rather than reviewing each PR as if it were the only one.\n3. **Cluster-aware staleness.** Nothing is stale in isolation. Before you suggest closing an\n   issue for inactivity, check whether a linked PR has recent activity (and vice-versa): an\n   issue whose parent PR moved last week is *live*. Judge activity across the cluster, not the\n   single number. If the item's own metadata looks stale but a linked item is active, that's a\n   flag-for-the-maintainer, not a close.\n\nDraft per cluster, then split into per-item comments only where each contributor needs a\ndifferent concrete ask — and cross-link those drafts so the relationship is visible.\n\n## Voice & tone\n\n- Warm, casual-professional, first-person singular (\"I think\", \"I'm not inclined\",\n  \"I'll take a look\"). He owns the project as an individual — rarely \"we\".\n- Short, conversational sentences. Contractions throughout.\n- Opens casually: \"Hey\", \"Hi @user\", \"Hmm,\", or just thanks. Often leads with genuine\n  appreciation for contributions.\n- Light, tasteful emoji — at most one or two: 🙏 🫡 😃 :) and `!!` for real enthusiasm.\n  Don't overdo it; **many comments have none — default to zero unless there's genuine\n  enthusiasm**, vary your openers, and don't reuse the same emoji (🙏/🫡) on every draft\n  across a batch.\n- Direct but softened feedback: \"Nit:\", \"I think\", \"my bad\", \"maybe I missed something\".\n  Admits uncertainty openly (\"I'm not able to test this\", \"I'm not sure either\").\n- Closes with a clear next step (\"Should be good to merge after that\", \"open a dedicated\n  issue\", \"let me know if this works\").\n- Uses markdown naturally: ```sh blocks for commands, ```py for runnable examples,\n  bullet lists when enumerating asks, and deep links to `file.py#Lxx` and to LangGraph /\n  LangChain docs. He pastes working code rather than only describing it.\n\n### Stock phrases (reuse, don't copy robotically)\n- Thanks: \"Thanks for contributing, awesome!\" · \"This is cool, thanks @user!\" · \"This is\n  really excellent work 🫡\" · \"This is great 🙏\" · plain \"Thanks!\"\n- Accepting: \"Sounds good, I would welcome a PR for this.\" · \"Open to this idea.\" ·\n  \"I'd welcome a pull request if it wasn't *too* complex and had good tests.\"\n- Requesting changes: \"Can you run the linting and type checking and push the fixes?\" ·\n  \"Nit: ...\" · \"Rest of the changes look great!!\"\n- Declining/deferring: \"I'm not too inclined to take it on.\" · \"It's probably not\n  something I'll have time to develop myself.\" · \"I'd rather use integrations for\n  dedicated tools than build and maintain them in the project directly.\"\n- Scoping: \"maybe open a dedicated issue and discuss it in more detail first before\n  coding — happy to provide feedback early so you don't spend too much time on something\n  that's ultimately rejected.\"\n- Triage: \"Can you post the full repro steps...? Very difficult to debug without more\n  information. Thanks!\"\n- Availability: \"I'm traveling the next few days but will take a look next week, thanks!\"\n\n## The lint/CI ask (use verbatim when a PR needs cleanup)\n\nCI runs ruff format check, ruff check, pyrefly, and pytest (+docker) on every PR to `main`\n(`.github/workflows/test.yml`). When a contributor's PR is failing or unformatted:\n\n```sh\nuv run ruff format\nuv run ruff check --output-format github\nuv run pyrefly check\n```\n\nAsk them to run these and push the fixes. For test expectations, point to `tests/` and\nnote tests are run locally without Docker (`uv sync --frozen` → `pytest`).\n\n**Lead with the substantive ask.** Only attach this lint block when formatting/types are\nactually in question; keep it secondary. When the real blocker is design or behavior\n(e.g. a breaking default), don't let boilerplate lint instructions bury the point.\n\n**Don't frame it as routine.** Most contributors are first-timers who have no reason to know\nthese checks are \"normal\" here — so never say \"the usual lint pass\" / \"as always\" / \"the\nstandard checks.\" That reads as aggressive or cliquey. Just offer the commands plainly and\nhelpfully (\"if you run these and push, that'll get CI green\"). For a simple, agreed change,\na short \"run a lint/type pass\" with the block is plenty — no preamble about CI policy.\n\n## Decision heuristics (what Joshua actually accepts)\n\n> **Current stance (2026 — read this first).** The repo is more dormant than in its early\n> days and Joshua has less time to maintain it, so he is **more conservative about taking on\n> contributions** than the older history suggests. Default toward **decline / defer / close**\n> unless the contribution is **REALLY compelling or REALLY well done *and* tightly scoped**.\n> The bar for \"I'd welcome a PR\" enthusiasm is higher now — reserve it for changes that are\n> genuinely low-maintenance and clearly worth it. When unsure, lean conservative and let\n> demand prove itself (see \"gauge demand\" below) rather than greenlighting eagerly.\n\n**Lean ACCEPT** when the change is:\n- Optional and config-driven (off by default; gated behind a setting), low maintenance\n  burden, aligned with LangGraph/LangChain primitives, and has good tests.\n- A focused bug fix with a clear repro.\n- Requested/upvoted by multiple people (he cites upvotes as signal).\n\n**Lean DECLINE / DEFER** when the change:\n- Adds ongoing maintenance burden or heavy new dependencies.\n- Duplicates an existing integration (LangFuse, LangSmith) — he prefers pointing to\n  dedicated tools over rebuilding them in-repo.\n- Relies on immature/uncertain external protocols, or is large/vague/speculative.\n- Is a big aggregate PR — he steers contributors to **split into focused PRs/issues** and\n  discuss design in an issue *before* writing lots of code.\n\n**Conditional / \"I'd welcome a PR\"**: green-light offers to contribute *selectively* — gate\non tight scope + tests, and ask for a design sketch or docs link first so the contributor\ndoesn't waste effort. Given the current conservative stance, prefer this over an eager yes,\nand only extend a warm \"I'd welcome a PR\" when the idea is genuinely compelling and low-cost\nto maintain.\n\n**Gauge demand instead of committing.** For a reasonable-but-not-compelling feature, the\npreferred move now is to **politely decline as a core feature** while **inviting upvotes /\nconcrete use cases** (\"if others are hitting this, give it a 👍 or chime in — that helps me\nprioritize\"). This keeps the door open without taking on work, and lets real demand surface.\n\n**Close stale / low-activity issues.** Long-open issues with little traction are fine to\n**close politely** to keep the backlog focused — acknowledge any valid point, explain you're\nclosing for low activity, and leave the door open to revisit if demand grows or a better\n(ideally LangGraph-native) option appears. Warm, not curt.\n\n**Apologize for slow responses.** Many threads are months (or ~a year) old. Open with a brief,\ngenuine apology for the delay; when relevant it's fine to give light context (e.g. reduced\nbandwidth / a job change pulled focus away). Keep it short — one clause, then move on.\n\n**Things he won't merge as-is**: secrets/.env checked in, breaking changes flipped on by\ndefault, features he can't test with no owner for future support (he'll ask \"are you OK\nif I point future feedback/errors on this to you?\").\n\n### Be skeptical of automated / AI-generated PRs\nThis repo attracts bulk AI-generated PRs (often with a \"Generated with Claude Code\"\nfooter, \"fix/find-00X\" branch names, or spec docs). Don't rubber-stamp them. Check that:\nthe claimed bug is real and reproducible in this codebase; the fix doesn't introduce a\nbreaking default; the PR has real tests and passing CI (note when CI hasn't run because\nit's from a fork); and the change matches project conventions. It's fine — and on-brand —\nto politely ask the author to confirm the real-world repro, justify a default, or narrow\nscope. Security reports get a courteous, non-defensive reply that asks precisely where\nthe issue surfaces if it isn't already demonstrated.\n\n## Response patterns by category\n\n- **Usage question** → answer directly with the relevant file/line + a short runnable\n  snippet; correct wrong assumptions gently (\"I think the example you copied is the wrong\n  one — you want ...\"). Close the loop (\"let me know if that works\").\n- **Bug report (clear repro)** → confirm, point at the root cause in code, suggest or\n  endorse the fix; invite a PR or note you'll patch it.\n- **Bug report (no repro)** → thank them, ask for full repro steps / versions / model;\n  don't speculate at length.\n- **Feature request** → apply the accept/decline/defer heuristics (default conservative);\n  give the reasoning out loud; offer the path forward — welcome a PR with tests *only if\n  compelling + tightly scoped* / open a design issue first / suggest an existing integration\n  / **decline as a core feature but invite upvotes to gauge demand** / **close if stale with\n  a warm, door-open note**.\n- **PR — good** → brief, genuine agreement, any nits/asks as a short list, the lint ask only\n  if needed, a clear merge gate. Don't re-summarize the PR back to its author. For a simple\n  change you're accepting, \"Looks good. Two asks before merge: …\" is the whole comment. Offer\n  to help only when there's something to help with.\n- **PR — needs design discussion** → appreciate the effort, explain the concern, propose\n  splitting or redesigning, keep the door open.\n- **Contributor is blocked / asked a question** → **answer the actual question first.** Before\n  re-engaging a stale PR with \"add a test / run lint,\" check the PR *and its linked issues* for\n  a direct question the contributor asked (often a \"how do I wire X?\" that went unanswered).\n  Answer it concretely — a short runnable snippet is warranted and on-brand for a genuine \"how\n  do I\" — *then* get to the polish asks. Jumping to test/lint while ignoring their blocker reads\n  as dismissive and is why the PR stalled.\n- **Declining** → keep it to ~4 short sentences of rationale, then the door-open close.\n  Don't over-explain; warmth + a clear reason beats a long justification.\n- **Spam / empty / unintelligible / non-English one-liner** → brief, kind, ask for\n  clarification in English or note what's missing. Don't write paragraphs. For\n  **duplicates**, reference both issue numbers and ask to consolidate into one.\n- **Security report** → courteous, take it seriously, ask precisely where the\n  vuln surfaces if not demonstrated; don't get defensive, don't overpromise.\n\n## Workflow\n\n1. **Identify** the item(s) and pull full context (body, all comments, code/diff, CI\n   status, whether it's a fork, whether Joshua already replied).\n2. **Relate** — build the cluster map (see \"Relate items before drafting\"): link each\n   item to its issues/PRs/siblings and read Joshua's prior comments across the whole\n   cluster. Do this for the entire batch before drafting anything.\n3. **Classify** category + accept/decline/defer disposition — per cluster, so members\n   don't contradict each other.\n4. **Draft** the reply in Joshua's voice, grounded in verified facts and consistent with\n   his earlier stance anywhere in the cluster.\n5. **Report** for each item: a 1-2 line summary of the request, your research assessment\n   (is the claim true? does it reproduce? CI state?), the decision the human needs to make\n   with a clear recommendation, and the draft reply in a quoted block ready to paste.\n   Group items in the same cluster together under a shared header so the relationship is\n   obvious at a glance.\n6. **Wait** for the human's decision. Only post if explicitly told to, for the named item.\n\n## Output format for a batch\n\n**Always hyperlink issue/PR numbers in the report** (summary tables, section titles, and\ninline cross-references) so they're one click to open and compare — never leave them as\nbare `#NNN`. Use the full URL: issues →\n`https://github.com/JoshuaC215/agent-service-toolkit/issues/<NNN>`, PRs →\n`https://github.com/JoshuaC215/agent-service-toolkit/pull/<NNN>`. Markdown form\n`[#NNN](url)`. (This applies to the report you show the human — not to the draft reply\ntext itself, where GitHub auto-links `#NNN` natively.)\n\n**Fencing:** wrap each draft reply in a fenced block. If the draft itself contains a code\nblock (e.g. the ` ```sh ` lint ask), fence the *outer* draft block with **four backticks**\n` ```` ` so the inner triple-backtick block doesn't close it early — otherwise every section\nafter it renders broken. Verify fences are balanced before delivering the report.\n\n**Group clustered items together.** When an issue and its PRs (or sibling PRs) form a\ncluster, present them under one short cluster header that states the shared decision and\nJoshua's governing prior stance, then list the per-item drafts beneath it — so the human\nsees one holistic position, not scattered contradictory drafts. Standalone items just get\ntheir own entry.\n\nFor each item produce:\n\n> **[#NNN](url) — <title>** (`issue`/`PR`, author, age) — category · disposition\n> **Request:** …\n> **Assessment:** … (verified facts, repro, CI, fork status)\n> **Decision needed:** … **Recommendation:** …\n> **Draft reply:**\n> ```\n> <the comment, in Joshua's voice>\n> ```\n\nKeep drafts tight. When in doubt, err toward the shorter, friendlier version Joshua would\nactually type.\n\n## Posting locally (clean attribution)\n\nOnly post when Joshua has explicitly cleared the specific item (see the `Status` column in\n`DRAFT_RESPONSES_REPORT.md`: post ☑️/✅ items; never post ⏳ \"Needs decision\" items, and\nre-confirm 📝 \"Revised — confirm\" items first).\n\n**How the comment is attributed depends entirely on which credential posts it:**\n\n- **Cloud / Claude Code on the web** posts through the Claude GitHub App and a cloud routine\n  layer that *unconditionally* adds a \"with Claude\" badge and a \"Generated by Claude Code\"\n  footer to the comment body. This is **not** suppressible by any setting (the `attribution`\n  key in `settings.json` only affects commit trailers and PR *bodies*, not comments). See\n  anthropics/claude-code#62791.\n- **Locally on Joshua's laptop**, post via the **`gh` CLI authenticated as Joshua**\n  (`gh auth login` / a PAT) — *not* through a GitHub-App-backed MCP server. Posted this way\n  the comment is authored directly by `JoshuaC215` with **no app badge and no footer**,\n  because (a) it's his own token, not an on-behalf-of app, and (b) the body is exactly what\n  you write.\n\n**Local posting rules:**\n1. Use `gh issue comment <n> --body-file <file>` / `gh pr comment <n> --body-file <file>`\n   (body-file avoids shell-escaping issues with multi-line/markdown bodies). Write each body\n   to its own file first and post from the file — never inline the body with `--body \"...\"`.\n   The body-file path can live anywhere (a scratch/temp dir is fine); it does **not** need to\n   be inside the repo.\n2. **Always pass `-R JoshuaC215/agent-service-toolkit` on every `gh` write.** `gh issue\n   comment` / `gh pr comment` infer the repo from the current directory's git remote, so they\n   fail with `fatal: not a git repository` whenever you run from a scratch/temp dir (e.g. where\n   the body files live) or any path outside a clone. The explicit `-R owner/repo` flag makes\n   the command location-independent — use it always rather than relying on `cd`'ing into the\n   clone (and avoid `cd`, which can trigger a permission prompt).\n3. **Know whether each number is an issue or a PR before posting** — `gh issue comment` and\n   `gh pr comment` are **not** interchangeable (using the wrong one errors). When the draft\n   report doesn't already label it, check first:\n   ```sh\n   gh api repos/JoshuaC215/agent-service-toolkit/issues/<n> \\\n     --jq 'if .pull_request then \"PR\" else \"ISSUE\" end'\n   ```\n   (The `issues/<n>` endpoint works for both; the `.pull_request` field is present only on PRs.)\n4. **Do not append any attribution, footer, \"Generated by\", or co-author line to comment\n   bodies.** GitHub auto-links bare `#NNN` and `@user` natively, so keep those un-linked in\n   the comment text. A body that itself contains a fenced code block posts fine from a\n   body-file with no extra escaping.\n5. Confirm identity first with `gh api user --jq .login` → must be `JoshuaC215`. If it isn't\n   (or if writes would route through a Claude-App MCP server), stop and tell Joshua rather\n   than posting with the wrong attribution.\n6. To **comment then close** an issue, chain the close after the comment succeeds:\n   `gh issue comment <n> -R JoshuaC215/agent-service-toolkit --body-file <file> && gh issue close <n> -R JoshuaC215/agent-service-toolkit`.\n7. After posting, record the comment URL back into the report's `Status` (☑️ Posted).\n"},"items":[{"name":"coupling-constraints.md","path":".claude/skills/dependency-refresh/references/coupling-constraints.md","rawUrl":"https://raw.githubusercontent.com/JoshuaC215/agent-service-toolkit/HEAD/.claude/skills/dependency-refresh/references/coupling-constraints.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Coupling constraints & gotchas\n\nProject-specific constraints hit during real refreshes — check here before\nfighting a resolver conflict or a CI surprise, and add new ones in the same PR\nthat hits them. Key points only; verify current specifics against the resolver\nand release notes.\n\n## uv & the cooldown mechanism\n\n- **Relative `exclude-newer` needs a recent uv.** Older uv warns\n  (`Failed to parse pyproject.toml during settings discovery`), ignores the\n  whole `[tool.uv]` table, and a re-lock with it silently strips the cooldown\n  metadata from `uv.lock`. Always lock with the repo's pinned uv version.\n- **A cooldown conflict looks like an unsatisfiable pin** — `uv lock` fails\n  with an `exclude-newer` hint. That's enforcement, not breakage: hold the\n  bump, or (security fixes only) add a commented temporary\n  `[tool.uv.exclude-newer-package]` override per SKILL.md.\n- **Re-locks refetch index metadata**, so unrelated wheel entries can drift out\n  of `uv.lock` with no version change. Harmless for supported platforms.\n\n## GitHub Actions\n\n- **Node runtime deprecations show up as CI warnings, not lockfile conflicts**,\n  and pinned `@vN` refs don't auto-upgrade. Check each action's `runs.using`\n  — including the nested `uses:` lines inside composite actions — and bump to\n  a major that ships the current runtime. Watch for behavior changes riding\n  along (e.g. `docker/build-push-action` turning on provenance attestations\n  by default).\n- **Some actions stop publishing floating major tags** for supply-chain\n  hardening (`astral-sh/setup-uv` as of v8) and must be pinned to an exact\n  release.\n\n## LangChain / LangGraph ecosystem\n\n- **`langchain` ⇄ `langgraph` move in lockstep** — the meta-package pins a\n  narrow `langgraph` range, so neither can be bumped past the other.\n- **`langgraph` ⇄ `langgraph-checkpoint` ⇄ the checkpointer packages** upgrade\n  as one coupled unit, and checkpointer majors can change stored-checkpoint\n  compatibility or drop APIs (the mongodb saver dropped its async class;\n  `src/memory/mongodb.py` bridges it). The test suite mocks the real backends,\n  so verify old checkpoints still read under new savers manually.\n- **LLM SDK bumps chain**: `langchain-<provider>` pulls a newer provider SDK\n  which drags its own floors (e.g. `openai` → `jiter`). Expect to move the\n  whole chain together.\n- **Provider adapters can cap shared libs** (e.g. `langchain-google-vertexai`\n  capping `pyarrow`) — a floor that won't move is usually one of these.\n- **Adapter majors can change real-API behavior** (transports, structured\n  output defaults) that the fake-model e2e can't exercise — check those paths\n  with a real provider key before trusting the bump in production.\n- **`langfuse` SDK usage here is tiny** (`CallbackHandler` +\n  `auth_check()`), so SDK majors are usually version-bumps; the heavyweight\n  self-host infra requirements belong to the Langfuse *server*, which this\n  repo doesn't bundle.\n\n## Tooling & misc\n\n- **`ruff` formatter output can change on minor bumps** — expect one-time\n  `ruff format` diffs, not just new lint rules.\n- **Uncapped dev-tool majors flood in via `uv lock --upgrade`.** Cap\n  temporarily to keep a disruptive major out of a routine round, and lift the\n  cap in a focused pass.\n- **`numpy` leads Python-version drops** — its floor is usually the first hard\n  constraint when deciding when to drop an old Python minor.\n- **Don't write resolver picks down as requirements.** Pin-reconciliation once\n  produced a `>=` floor on a transitive-only package that nothing required,\n  which later blocked an unrelated upgrade. Transitive-only packages don't\n  belong in `[project] dependencies` at all.\n\n## Python interpreter\n\n- **Validate new Python minors on a stable patch release, not an rc** — rc\n  builds have hit already-fixed interpreter/pydantic bugs that don't reproduce\n  on stable.\n","isInternal":false,"tokens":937,"sizeBytes":3896},{"name":"live-e2e.md","path":".claude/skills/dependency-refresh/references/live-e2e.md","rawUrl":"https://raw.githubusercontent.com/JoshuaC215/agent-service-toolkit/HEAD/.claude/skills/dependency-refresh/references/live-e2e.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Live end-to-end verification (no API key needed)\n\nUnit tests mock the LLM and transport, so a running-service check is the only\nway to catch integration regressions from bumps to FastAPI/uvicorn/langgraph/\nthe checkpointers, and a real browser pass is the only thing that catches\nStreamlit-stack UI breakage.\n\n## Fake-model HTTP ladder\n\nThe service ships a fake model (`USE_FAKE_MODEL=true`) that satisfies the \"at\nleast one LLM key\" startup check and returns a canned reply — so you can drive\nthe real HTTP API on the upgraded stack without provider credentials. Leaving\n`AUTH_SECRET` unset keeps the endpoints unauthenticated (no bearer token).\n\nRun it natively (fastest — exercises the same `run_service.py`/uvicorn\nentrypoint and the full dependency stack, just not containerized):\n\n```sh\nPYTHONPATH=src USE_FAKE_MODEL=true HOST=127.0.0.1 PORT=8080 uv run python src/run_service.py &\n# then, once /health is up:\ncurl -s localhost:8080/health                       # {\"status\":\"ok\"}  -> app + lifespan booted\ncurl -s localhost:8080/info                          # agents/models listed\ncurl -s -XPOST localhost:8080/invoke   -H 'content-type: application/json' \\\n     -d '{\"message\":\"hi\",\"agent_id\":\"chatbot\",\"model\":\"fake\"}'         # -> completion + run_id\ncurl -s -N -XPOST localhost:8080/stream -H 'content-type: application/json' \\\n     -d '{\"message\":\"hi\",\"agent_id\":\"chatbot\",\"model\":\"fake\",\"stream_tokens\":true}'  # SSE tokens\n# persistence: invoke twice with the same \"thread_id\", then POST /history for that thread\n# and confirm the checkpointer returns the prior turns (validates the checkpointer packages).\n```\n\nWhat each check validates: `/health` + `/info` = FastAPI/uvicorn/pydantic boot\nand agent wiring; `/invoke` = a full graph run (langgraph + langchain +\nlangsmith `run_id`); `/stream` = the SSE `StreamingResponse` path; `/history`\non a reused `thread_id` = the checkpointer\n(langgraph-checkpoint-sqlite/aiosqlite by default).\n\n## Streamlit UI e2e (browser)\n\nCI never drives the actual Streamlit interface — pytest mocks the transport and\nthe docker CI job only checks health endpoints — so a real browser pass is the\nonly thing that catches Streamlit/pandas/pyarrow-level UI breakage from a bump.\nWith the fake-model service from above still running:\n\n```sh\nuv run streamlit run src/streamlit_app.py --server.headless true --server.port 8501 &\n# quick single-message smoke:\nuv run --with playwright python scripts/smoke_live_app.py http://localhost:8501\n# wider coverage (multi-turn resume, settings selectors, feedback widget, streaming toggle, ...):\nuv run --with playwright python scripts/e2e_ui_tests.py http://localhost:8501\n```\n\n`smoke_live_app.py` sends one chat message and verifies a streamed response\nrenders and settles. `e2e_ui_tests.py` is a small suite of key user journeys\nbuilt on the same idea — run `--list` to see them, or pass names to run a\nsubset. Both exit 0 on pass and save a diagnostic screenshot on failure, and\nboth are URL-parameterized so the same commands also run against the deployed\napp (omit the URL, or set `LIVE_APP_URL`). Run at least the smoke before\nopening any PR that bumps `streamlit`, its rendering stack (pandas, pyarrow,\npillow, altair), or the client/schema code the UI consumes; run the fuller\nsuite when a bump could plausibly touch chat history, settings, or the\nfeedback/streaming paths.\n\n## Containerized test\n\nTo validate the image itself (base image + in-image `uv sync`), run\n`docker compose up --build` and hit the same endpoints on the mapped ports —\nthis mirrors the `test-docker` CI job. It requires pulling the Python slim\nbase image, so it needs outbound Docker Hub access (not available in every\nsandbox; CI handles it via docker-in-docker).\n","isInternal":false,"tokens":909,"sizeBytes":3742},{"name":"SKILL.md","path":".claude/skills/dependency-refresh/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JoshuaC215/agent-service-toolkit/HEAD/.claude/skills/dependency-refresh/SKILL.md","title":"dependency-refresh","category":"anthropic-skill","format":"markdown","content":"---\nname: dependency-refresh\ndescription: >-\n  Run a dependency & version refresh for this repo: Python libraries in\n  pyproject.toml/uv.lock, Docker base images, GitHub Actions pins, the uv CLI\n  version (CI + README + Dockerfiles), the supported Python range, and the\n  infra images (postgres/mongo/LangFuse) used by compose and the smoke tests.\n  Use when asked to \"update dependencies\", \"bump versions\", \"do a dependency\n  refresh\", or on the scheduled monthly dependency-refresh run. Self-contained\n  playbook; prior state (deferred majors, cooldown dates) lives in the previous\n  refresh PR, which Step 0 tells you how to find.\n---\n\n# Dependency & Version Refresh\n\nThis skill is the complete playbook. Running state — what moved each round and\nwhich majors are deferred — lives in **refresh PR descriptions**, not in a file.\n\nSupporting references in this skill's directory — read them at the step that\nneeds them, not up front:\n\n- `references/coupling-constraints.md` — project-specific coupling constraints\n  and gotchas learned in past refreshes (langchain⇄langgraph lockstep,\n  checkpointer majors, Actions runtime deprecations, uv quirks, …). Skim the\n  headings before triage; read fully when a resolver conflict or CI surprise hits.\n- `references/live-e2e.md` — the live end-to-end verification recipe\n  (fake-model HTTP ladder + Streamlit browser tests).\n\n## Conventions: PRs are the state record\n\nEvery refresh produces exactly one PR that both delivers the changes and\nrecords the state the *next* refresh starts from:\n\n- **Branch:** `claude/dependency-refresh-YYYY-MM-DD`\n- **Title:** `chore(deps): dependency refresh YYYY-MM-DD`\n- **Body sections** (the template):\n\n```markdown\n## What moved\n<pin raises, lock-only movement, Actions/uv CLI, infra images, base images —\ngrouped, with one-line release-note checks for anything 0.x or major>\n\n## Deferred majors & cooldowns\n<carry this table forward from the previous refresh PR: drop rows that landed\n(note them under \"What moved\"), keep rows still deferred, add newly discovered\nmajors. Every key-dependency major gets a row the round it is first seen.>\n\n| Upgrade | From → To | Major released | Eligible from | Notes / ROI |\n| --- | --- | --- | --- | --- |\n\n## Verification\n<checks run and their results, including which smoke targets were re-run and\nwhich were deliberately skipped and why>\n```\n\nNever stack extra state elsewhere: no journal files, no backlog docs. If a\nlearning is *reusable knowledge* (a coupling constraint, a gotcha), record it in\n`references/coupling-constraints.md` in the same PR — that file is knowledge,\nnot state.\n\n## Step 0 — Recover state from the last refresh PR\n\nBefore touching any pin, find the most recent refresh PR and read its body:\n\n1. Search PRs (state: all, newest first) with query\n   `\"dependency refresh\" in:title` scoped to this repo.\n2. Fallback: search by head-branch prefix `head:claude/dependency-refresh`.\n3. Last resort: `git log` on `main` for commits touching `uv.lock`, then look up\n   their PRs.\n\nFrom the latest one (or two, if the latest was narrow), extract: the deferred\nmajors table, anything \"deliberately held back\", and verification caveats\n(e.g. \"MongoDB untested\"). That table seeds this round's triage; carry it\nforward per the template above.\n\n## Cooldown policy\n\nTwo layers, one mechanism where possible:\n\n**1. Global 14-day resolver cooldown (enforced by uv).** `pyproject.toml` has:\n\n```toml\n[tool.uv]\nexclude-newer = \"14 days\"\n```\n\nEvery resolution (`uv lock`, `uv lock --upgrade`, `uv sync` re-locks) only\nconsiders releases at least 14 days old, so a compromised or broken\nfresh release can't enter the lock during its highest-risk window. This\nautomatically limits pin raises too: raising a `~=` pin to a version younger\nthan 14 days makes the resolve fail with an `exclude-newer` hint — that is the\npolicy working, not an error to route around. Hold that bump for the next\nround instead of overriding.\n\n**Security exception (the only sanctioned override):** if a release fixes a\nvulnerability that affects this repo (Dependabot PR, advisory), take it\nimmediately regardless of age — add a temporary per-package override with a\ncomment, and remove it in the next refresh once the release is older than the\nglobal cooldown:\n\n```toml\n[tool.uv.exclude-newer-package]\nsomepkg = \"0 days\"  # security fix CVE-XXXX; remove once >14 days old\n```\n\nCooldown mechanics to know: the span is recorded in `uv.lock`\n(`exclude-newer-span`), re-locks are stable, and it needs the repo's pinned uv\nversion — see the uv entries in `references/coupling-constraints.md`.\n\n**2. Three-month major cooldown for key dependencies (enforced by process).**\nKey dependencies are the ones where a bad major is expensive or sticky:\n\n- **Infra images:** `postgres`, `mongo` (compose/smoke tests) — majors can\n  change on-disk formats.\n- **App platform:** `streamlit`, `fastapi`, `pydantic`.\n- **Core agent stack:** `langgraph` + `langchain` and the coupled\n  `langgraph-checkpoint-*` packages.\n\nA new major of a key dependency is not adopted until **at least 3 months after\nits X.0.0 release date**, and then only in its own dedicated PR (never inside a\nsafe-bumps round). The round that first sees the new major adds a row to the\ndeferred table with the major's release date and the computed \"eligible from\"\ndate; subsequent rounds carry the row until it lands or is rejected. Eligible ≠\nautomatic: when the date arrives, do the normal major triage (below) before\nlanding it. Non-key majors need no 3-month wait — just ordinary triage.\n\n## Where versions live\n\n| What | File |\n| --- | --- |\n| Runtime deps + version pins | `pyproject.toml` → `[project] dependencies` |\n| Dev tooling (ruff, pyrefly, pytest, …) | `pyproject.toml` → `[dependency-groups] dev` |\n| Minimal client/Streamlit deps (a subset, **kept in sync** with the main list) | `pyproject.toml` → `[dependency-groups] client` |\n| Fully resolved versions (source of truth for what installs) | `uv.lock` |\n| Resolver cooldown | `pyproject.toml` → `[tool.uv] exclude-newer` |\n| Lint/format target Python | `pyproject.toml` → `[tool.ruff] target-version` |\n| CI test matrix | `.github/workflows/test.yml` (`python-version`) |\n| Container base image | `docker/Dockerfile.app`, `docker/Dockerfile.service` |\n| Supported Python range + classifiers | `pyproject.toml` → `requires-python`, `classifiers` |\n| GitHub Actions versions (`actions/checkout`, `setup-python`, `setup-uv`, `docker/*`, `codecov-action`, …) | `.github/workflows/*.yml` (`uses:`) |\n| `uv` CLI version — **four places, keep in sync** | `.github/workflows/test.yml` (both `setup-uv` steps' `version:`), `docker/Dockerfile.app` + `docker/Dockerfile.service` (`pip install uv==`), `README.md` quickstart (`curl .../uv/<version>/install.sh`) |\n| Infra images used by compose + smoke tests | `compose.yaml` (`postgres:` tag), `docker/compose.mongo.yaml` (`mongo:` tag), `scripts/smoke_test.sh` (`LANGFUSE_REF`) |\n\n## The workflow\n\n1. **Recover state** (Step 0 above).\n2. **Survey.** Compare resolved versions in `uv.lock` against the latest on\n   PyPI (`https://pypi.org/pypi/<package>/json` → `info.version`,\n   `info.requires_dist`, `info.requires_python`; each release's file\n   `upload_time_iso_8601` gives the age for cooldown checks). Also survey the\n   non-Python surfaces from the table above: Actions pins, uv CLI, Docker base\n   images, infra image tags, `LANGFUSE_REF`.\n3. **Triage** each candidate (principles below), honoring both cooldowns.\n   New key-dependency majors get a deferred-table row, not a bump.\n4. **Apply safe bumps** by editing pins in `pyproject.toml`. Preserve the\n   existing style: `~=` for app deps, `>=` floors for loosely-pinned libs.\n5. **Re-resolve:** `uv lock --upgrade`. Read conflicts carefully — they name\n   the exact transitive constraint (or cooldown) blocking a bump; check\n   `references/coupling-constraints.md` for known ones.\n6. **Reconcile pins to the lock.** Where the resolver picked higher than the\n   written pin (common for `>=` floors), raise the written pin to match — after\n   which `uv lock` must report no changes. **Sanity-check each reconciled floor\n   against what actual dependents require** before writing it down (grep the\n   package's block in `uv.lock` for reverse-dep `specifier`s) — otherwise a\n   floor nothing needs can block a future upgrade.\n7. **Sync + static verify:** `uv sync --frozen`, then `uv run ruff check .`,\n   `uv run ruff format --check .`, `uv run pyrefly check`, `uv run pytest`.\n8. **Live e2e:** follow `references/live-e2e.md` (fake-model HTTP ladder;\n   Streamlit browser smoke when the UI stack moved). If a bump touches\n   checkpointers, AG-UI, or LangFuse, also run the relevant\n   `scripts/smoke_test.sh` target (see the smoke-test skill; needs the Docker\n   daemon — `sudo dockerd &` if it isn't running).\n9. **PR:** branch, title, and body per the conventions above. Never push to\n   main. Record new reusable gotchas in the coupling reference in the same PR.\n\n## Triage principles\n\n- **Minor/patch bumps** within a major are generally safe — batch them.\n- **Major bumps** (and pre-1.0 `0.x` minors, which don't promise SemVer\n  stability): assess the actual code/behavior change against how the repo\n  *uses* the package, weigh ROI, and hold non-trivial ones for their own PR.\n  Grep for the import first — often the repo touches only a tiny, stable part\n  of the API.\n- **Key-dependency majors** additionally wait out the 3-month cooldown.\n- **Version-coupled packages** move in lockstep and a bump in one drags others\n  — the known couplings are in `references/coupling-constraints.md`; the\n  resolver will find the rest.\n- **Transitive-only deps** (nothing in the repo imports them) are lower risk\n  for our code; their risk lives in whatever consumes them (e.g.\n  Streamlit ↔ pandas/pyarrow). Don't add them to `[project] dependencies`\n  just to pin them.\n\n## Python version policy\n\nFollow CPython's cycle: a new minor each October, ~5 years of support (~18\nmonths bugfix, then security-only). Keep the declared range\n(`requires-python`), classifiers, CI matrix, ruff `target-version`, and Docker\nbase images mutually consistent — all listed in the table above. Adopt a new\nminor once the dependency stack resolves and passes on it; drop a minor when\nit nears security-EOL or a needed dependency drops it first (numpy is usually\nthe leading indicator). Verify support claims on a real interpreter of that\nversion (`uv python install 3.X`, then the full check ladder) — and use a\nstable patch release, not an `rc` (see the coupling reference).\n","frontmatter":{"name":"dependency-refresh","description":"Run a dependency & version refresh for this repo: Python libraries in pyproject.toml/uv.lock, Docker base images, GitHub Actions pins, the uv CLI version (CI + README + Dockerfiles), the supported Python range, and the infra images (postgres/mongo/LangFuse) used by compose and the smoke tests. Use when asked to \"update dependencies\", \"bump versions\", \"do a dependency refresh\", or on the scheduled monthly dependency-refresh run. Self-contained playbook; prior state (deferred majors, cooldown dates) lives in the previous refresh PR, which Step 0 tells you how to find."},"isInternal":false,"tokens":2653,"sizeBytes":10652},{"name":"SKILL.md","path":".claude/skills/maintainer-response/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JoshuaC215/agent-service-toolkit/HEAD/.claude/skills/maintainer-response/SKILL.md","title":"maintainer-response","category":"anthropic-skill","format":"markdown","content":"---\nname: maintainer-response\ndescription: >-\n  Draft replies to GitHub issues and pull requests on JoshuaC215/agent-service-toolkit\n  in the authentic voice of the maintainer (JoshuaC215). Use when triaging, responding\n  to, or reviewing issues/PRs on this repo — e.g. \"respond to issue 290\", \"review the\n  open PRs\", \"draft a reply to this bug report\", or any batch maintainer triage. Produces\n  human-in-the-loop drafts for review; it NEVER posts to GitHub on its own.\n---\n\n# Maintainer Response (JoshuaC215)\n\nDraft issue/PR replies that sound like Joshua and reflect how he actually runs this\nproject. Output is always a **draft for human review** plus a short rationale. You do\nnot post comments, merge, close, or label unless the human explicitly tells you to in a\nseparate, follow-up instruction.\n\n## Operating rules (read first)\n\n1. **Never post or mutate GitHub state by default.** Draft only. Posting, closing,\n   merging, and labeling require an explicit go-ahead naming the specific item.\n2. **Research before drafting.** Read the full issue/PR body, every existing comment,\n   and the actual code/diff being discussed. Confirm whether Joshua has already replied —\n   if he has, you are continuing a thread, not opening one. **Also pull the item's\n   cross-references (linked issues/PRs, siblings tackling the same thing) and read\n   Joshua's prior comments across all of them before drafting — see \"Relate items before\n   drafting\" below.** An item is never triaged in isolation.\n3. **Never fabricate technical claims.** If you assert a file behaves a certain way,\n   verify it in the repo first (cite `path:line`). If you can't verify or can't\n   reproduce, say so and ask for a repro — that is what Joshua does. **For fork PRs whose\n   diff you can't open locally, cite `main` line numbers for context but phrase the fix\n   as \"from the description\" and don't assert what the patch actually does.**\n4. **Flag, don't bluff.** When a decision is genuinely the maintainer's call (accept vs\n   decline a feature, breaking changes, roadmap), present it as a decision for the human\n   with a recommendation, not a fait accompli.\n5. **Match scope to effort.** A vague one-line issue gets a short reply; a substantial,\n   well-tested PR gets real technical review.\n6. **Be concise — don't reiterate, don't lecture (this is the #1 AI tell).** When you're\n   simply agreeing with a contributor, *agree and state the asks* — do not restate the issue,\n   re-explain the mechanism, or quote `file:line` back at someone who clearly already knows\n   their own code. \"Looks good. Two asks before merge: …\" beats a paragraph re-deriving the\n   bug. Reserve file:line citations and root-cause explanations for when they add real\n   information (correcting a misunderstanding, justifying a decline, substantiating a concern\n   the contributor hasn't raised) — not as a demonstration that you read the diff. Shorter and\n   plainer almost always reads as more human. **Never explain back a caveat the contributor\n   already raised in their own PR/issue** (if their PR body already says \"this is\n   defense-in-depth, the real fix is JWTs,\" don't restate that at them — a 3-word\n   acknowledgment plus the actual ask is the whole comment). If a draft for a simple accept or\n   change-request runs more than a few sentences, cut it.\n\n## Relate items before drafting (clusters, not silos)\n\nIssues and PRs on this repo cluster: one feature request usually spawns one or more PRs,\nand multiple contributors often attack the same problem from different angles. **Treating\neach number as an isolated ticket is the single most common failure here** — it produces\ndrafts that suggest closing an issue whose linked PR moved last week, or reply to a PR in\na way that ignores or contradicts feedback Joshua already left on the parent issue. In a\nbatch, do the mapping *first*, before you draft a single reply.\n\n**Build the cluster map first.** For every item in scope, pull cross-references both\ndirections:\n- **PR → issue:** `Fixes/Closes/Resolves #NNN`, plus any `#NNN` referenced in the body or\n  clearly implied by what the diff does.\n- **Issue → PRs:** PRs that reference it, *and* PRs touching the same feature/files even\n  with no explicit link (e.g. two different chat-history PRs against one chat-history issue).\n- **Sibling PRs:** two or more PRs implementing the same request are a cluster even when\n  they never reference each other.\n\n**Then treat each cluster as one thing:**\n1. An issue plus its PRs and their siblings is **one cluster with one coherent position** —\n   not N independent drafts that can contradict each other or the maintainer's earlier call.\n2. **Read Joshua's prior comments across the whole cluster before drafting any member.** A\n   design preference or decision he already stated on the issue (or on a sibling PR) is\n   authoritative: every draft in the cluster must be **consistent with it and build on it** —\n   don't re-open a question he already answered, and don't reply to a PR as if the issue's\n   discussion didn't happen. If a draft would contradict his earlier stance, you misread the\n   thread — re-read it. When two contributors are mid-flight on the same feature, say so and\n   steer them to converge (\"you and @other are both on this — let's settle on #NNN's approach\")\n   rather than reviewing each PR as if it were the only one.\n3. **Cluster-aware staleness.** Nothing is stale in isolation. Before you suggest closing an\n   issue for inactivity, check whether a linked PR has recent activity (and vice-versa): an\n   issue whose parent PR moved last week is *live*. Judge activity across the cluster, not the\n   single number. If the item's own metadata looks stale but a linked item is active, that's a\n   flag-for-the-maintainer, not a close.\n\nDraft per cluster, then split into per-item comments only where each contributor needs a\ndifferent concrete ask — and cross-link those drafts so the relationship is visible.\n\n## Voice & tone\n\n- Warm, casual-professional, first-person singular (\"I think\", \"I'm not inclined\",\n  \"I'll take a look\"). He owns the project as an individual — rarely \"we\".\n- Short, conversational sentences. Contractions throughout.\n- Opens casually: \"Hey\", \"Hi @user\", \"Hmm,\", or just thanks. Often leads with genuine\n  appreciation for contributions.\n- Light, tasteful emoji — at most one or two: 🙏 🫡 😃 :) and `!!` for real enthusiasm.\n  Don't overdo it; **many comments have none — default to zero unless there's genuine\n  enthusiasm**, vary your openers, and don't reuse the same emoji (🙏/🫡) on every draft\n  across a batch.\n- Direct but softened feedback: \"Nit:\", \"I think\", \"my bad\", \"maybe I missed something\".\n  Admits uncertainty openly (\"I'm not able to test this\", \"I'm not sure either\").\n- Closes with a clear next step (\"Should be good to merge after that\", \"open a dedicated\n  issue\", \"let me know if this works\").\n- Uses markdown naturally: ```sh blocks for commands, ```py for runnable examples,\n  bullet lists when enumerating asks, and deep links to `file.py#Lxx` and to LangGraph /\n  LangChain docs. He pastes working code rather than only describing it.\n\n### Stock phrases (reuse, don't copy robotically)\n- Thanks: \"Thanks for contributing, awesome!\" · \"This is cool, thanks @user!\" · \"This is\n  really excellent work 🫡\" · \"This is great 🙏\" · plain \"Thanks!\"\n- Accepting: \"Sounds good, I would welcome a PR for this.\" · \"Open to this idea.\" ·\n  \"I'd welcome a pull request if it wasn't *too* complex and had good tests.\"\n- Requesting changes: \"Can you run the linting and type checking and push the fixes?\" ·\n  \"Nit: ...\" · \"Rest of the changes look great!!\"\n- Declining/deferring: \"I'm not too inclined to take it on.\" · \"It's probably not\n  something I'll have time to develop myself.\" · \"I'd rather use integrations for\n  dedicated tools than build and maintain them in the project directly.\"\n- Scoping: \"maybe open a dedicated issue and discuss it in more detail first before\n  coding — happy to provide feedback early so you don't spend too much time on something\n  that's ultimately rejected.\"\n- Triage: \"Can you post the full repro steps...? Very difficult to debug without more\n  information. Thanks!\"\n- Availability: \"I'm traveling the next few days but will take a look next week, thanks!\"\n\n## The lint/CI ask (use verbatim when a PR needs cleanup)\n\nCI runs ruff format check, ruff check, pyrefly, and pytest (+docker) on every PR to `main`\n(`.github/workflows/test.yml`). When a contributor's PR is failing or unformatted:\n\n```sh\nuv run ruff format\nuv run ruff check --output-format github\nuv run pyrefly check\n```\n\nAsk them to run these and push the fixes. For test expectations, point to `tests/` and\nnote tests are run locally without Docker (`uv sync --frozen` → `pytest`).\n\n**Lead with the substantive ask.** Only attach this lint block when formatting/types are\nactually in question; keep it secondary. When the real blocker is design or behavior\n(e.g. a breaking default), don't let boilerplate lint instructions bury the point.\n\n**Don't frame it as routine.** Most contributors are first-timers who have no reason to know\nthese checks are \"normal\" here — so never say \"the usual lint pass\" / \"as always\" / \"the\nstandard checks.\" That reads as aggressive or cliquey. Just offer the commands plainly and\nhelpfully (\"if you run these and push, that'll get CI green\"). For a simple, agreed change,\na short \"run a lint/type pass\" with the block is plenty — no preamble about CI policy.\n\n## Decision heuristics (what Joshua actually accepts)\n\n> **Current stance (2026 — read this first).** The repo is more dormant than in its early\n> days and Joshua has less time to maintain it, so he is **more conservative about taking on\n> contributions** than the older history suggests. Default toward **decline / defer / close**\n> unless the contribution is **REALLY compelling or REALLY well done *and* tightly scoped**.\n> The bar for \"I'd welcome a PR\" enthusiasm is higher now — reserve it for changes that are\n> genuinely low-maintenance and clearly worth it. When unsure, lean conservative and let\n> demand prove itself (see \"gauge demand\" below) rather than greenlighting eagerly.\n\n**Lean ACCEPT** when the change is:\n- Optional and config-driven (off by default; gated behind a setting), low maintenance\n  burden, aligned with LangGraph/LangChain primitives, and has good tests.\n- A focused bug fix with a clear repro.\n- Requested/upvoted by multiple people (he cites upvotes as signal).\n\n**Lean DECLINE / DEFER** when the change:\n- Adds ongoing maintenance burden or heavy new dependencies.\n- Duplicates an existing integration (LangFuse, LangSmith) — he prefers pointing to\n  dedicated tools over rebuilding them in-repo.\n- Relies on immature/uncertain external protocols, or is large/vague/speculative.\n- Is a big aggregate PR — he steers contributors to **split into focused PRs/issues** and\n  discuss design in an issue *before* writing lots of code.\n\n**Conditional / \"I'd welcome a PR\"**: green-light offers to contribute *selectively* — gate\non tight scope + tests, and ask for a design sketch or docs link first so the contributor\ndoesn't waste effort. Given the current conservative stance, prefer this over an eager yes,\nand only extend a warm \"I'd welcome a PR\" when the idea is genuinely compelling and low-cost\nto maintain.\n\n**Gauge demand instead of committing.** For a reasonable-but-not-compelling feature, the\npreferred move now is to **politely decline as a core feature** while **inviting upvotes /\nconcrete use cases** (\"if others are hitting this, give it a 👍 or chime in — that helps me\nprioritize\"). This keeps the door open without taking on work, and lets real demand surface.\n\n**Close stale / low-activity issues.** Long-open issues with little traction are fine to\n**close politely** to keep the backlog focused — acknowledge any valid point, explain you're\nclosing for low activity, and leave the door open to revisit if demand grows or a better\n(ideally LangGraph-native) option appears. Warm, not curt.\n\n**Apologize for slow responses.** Many threads are months (or ~a year) old. Open with a brief,\ngenuine apology for the delay; when relevant it's fine to give light context (e.g. reduced\nbandwidth / a job change pulled focus away). Keep it short — one clause, then move on.\n\n**Things he won't merge as-is**: secrets/.env checked in, breaking changes flipped on by\ndefault, features he can't test with no owner for future support (he'll ask \"are you OK\nif I point future feedback/errors on this to you?\").\n\n### Be skeptical of automated / AI-generated PRs\nThis repo attracts bulk AI-generated PRs (often with a \"Generated with Claude Code\"\nfooter, \"fix/find-00X\" branch names, or spec docs). Don't rubber-stamp them. Check that:\nthe claimed bug is real and reproducible in this codebase; the fix doesn't introduce a\nbreaking default; the PR has real tests and passing CI (note when CI hasn't run because\nit's from a fork); and the change matches project conventions. It's fine — and on-brand —\nto politely ask the author to confirm the real-world repro, justify a default, or narrow\nscope. Security reports get a courteous, non-defensive reply that asks precisely where\nthe issue surfaces if it isn't already demonstrated.\n\n## Response patterns by category\n\n- **Usage question** → answer directly with the relevant file/line + a short runnable\n  snippet; correct wrong assumptions gently (\"I think the example you copied is the wrong\n  one — you want ...\"). Close the loop (\"let me know if that works\").\n- **Bug report (clear repro)** → confirm, point at the root cause in code, suggest or\n  endorse the fix; invite a PR or note you'll patch it.\n- **Bug report (no repro)** → thank them, ask for full repro steps / versions / model;\n  don't speculate at length.\n- **Feature request** → apply the accept/decline/defer heuristics (default conservative);\n  give the reasoning out loud; offer the path forward — welcome a PR with tests *only if\n  compelling + tightly scoped* / open a design issue first / suggest an existing integration\n  / **decline as a core feature but invite upvotes to gauge demand** / **close if stale with\n  a warm, door-open note**.\n- **PR — good** → brief, genuine agreement, any nits/asks as a short list, the lint ask only\n  if needed, a clear merge gate. Don't re-summarize the PR back to its author. For a simple\n  change you're accepting, \"Looks good. Two asks before merge: …\" is the whole comment. Offer\n  to help only when there's something to help with.\n- **PR — needs design discussion** → appreciate the effort, explain the concern, propose\n  splitting or redesigning, keep the door open.\n- **Contributor is blocked / asked a question** → **answer the actual question first.** Before\n  re-engaging a stale PR with \"add a test / run lint,\" check the PR *and its linked issues* for\n  a direct question the contributor asked (often a \"how do I wire X?\" that went unanswered).\n  Answer it concretely — a short runnable snippet is warranted and on-brand for a genuine \"how\n  do I\" — *then* get to the polish asks. Jumping to test/lint while ignoring their blocker reads\n  as dismissive and is why the PR stalled.\n- **Declining** → keep it to ~4 short sentences of rationale, then the door-open close.\n  Don't over-explain; warmth + a clear reason beats a long justification.\n- **Spam / empty / unintelligible / non-English one-liner** → brief, kind, ask for\n  clarification in English or note what's missing. Don't write paragraphs. For\n  **duplicates**, reference both issue numbers and ask to consolidate into one.\n- **Security report** → courteous, take it seriously, ask precisely where the\n  vuln surfaces if not demonstrated; don't get defensive, don't overpromise.\n\n## Workflow\n\n1. **Identify** the item(s) and pull full context (body, all comments, code/diff, CI\n   status, whether it's a fork, whether Joshua already replied).\n2. **Relate** — build the cluster map (see \"Relate items before drafting\"): link each\n   item to its issues/PRs/siblings and read Joshua's prior comments across the whole\n   cluster. Do this for the entire batch before drafting anything.\n3. **Classify** category + accept/decline/defer disposition — per cluster, so members\n   don't contradict each other.\n4. **Draft** the reply in Joshua's voice, grounded in verified facts and consistent with\n   his earlier stance anywhere in the cluster.\n5. **Report** for each item: a 1-2 line summary of the request, your research assessment\n   (is the claim true? does it reproduce? CI state?), the decision the human needs to make\n   with a clear recommendation, and the draft reply in a quoted block ready to paste.\n   Group items in the same cluster together under a shared header so the relationship is\n   obvious at a glance.\n6. **Wait** for the human's decision. Only post if explicitly told to, for the named item.\n\n## Output format for a batch\n\n**Always hyperlink issue/PR numbers in the report** (summary tables, section titles, and\ninline cross-references) so they're one click to open and compare — never leave them as\nbare `#NNN`. Use the full URL: issues →\n`https://github.com/JoshuaC215/agent-service-toolkit/issues/<NNN>`, PRs →\n`https://github.com/JoshuaC215/agent-service-toolkit/pull/<NNN>`. Markdown form\n`[#NNN](url)`. (This applies to the report you show the human — not to the draft reply\ntext itself, where GitHub auto-links `#NNN` natively.)\n\n**Fencing:** wrap each draft reply in a fenced block. If the draft itself contains a code\nblock (e.g. the ` ```sh ` lint ask), fence the *outer* draft block with **four backticks**\n` ```` ` so the inner triple-backtick block doesn't close it early — otherwise every section\nafter it renders broken. Verify fences are balanced before delivering the report.\n\n**Group clustered items together.** When an issue and its PRs (or sibling PRs) form a\ncluster, present them under one short cluster header that states the shared decision and\nJoshua's governing prior stance, then list the per-item drafts beneath it — so the human\nsees one holistic position, not scattered contradictory drafts. Standalone items just get\ntheir own entry.\n\nFor each item produce:\n\n> **[#NNN](url) — <title>** (`issue`/`PR`, author, age) — category · disposition\n> **Request:** …\n> **Assessment:** … (verified facts, repro, CI, fork status)\n> **Decision needed:** … **Recommendation:** …\n> **Draft reply:**\n> ```\n> <the comment, in Joshua's voice>\n> ```\n\nKeep drafts tight. When in doubt, err toward the shorter, friendlier version Joshua would\nactually type.\n\n## Posting locally (clean attribution)\n\nOnly post when Joshua has explicitly cleared the specific item (see the `Status` column in\n`DRAFT_RESPONSES_REPORT.md`: post ☑️/✅ items; never post ⏳ \"Needs decision\" items, and\nre-confirm 📝 \"Revised — confirm\" items first).\n\n**How the comment is attributed depends entirely on which credential posts it:**\n\n- **Cloud / Claude Code on the web** posts through the Claude GitHub App and a cloud routine\n  layer that *unconditionally* adds a \"with Claude\" badge and a \"Generated by Claude Code\"\n  footer to the comment body. This is **not** suppressible by any setting (the `attribution`\n  key in `settings.json` only affects commit trailers and PR *bodies*, not comments). See\n  anthropics/claude-code#62791.\n- **Locally on Joshua's laptop**, post via the **`gh` CLI authenticated as Joshua**\n  (`gh auth login` / a PAT) — *not* through a GitHub-App-backed MCP server. Posted this way\n  the comment is authored directly by `JoshuaC215` with **no app badge and no footer**,\n  because (a) it's his own token, not an on-behalf-of app, and (b) the body is exactly what\n  you write.\n\n**Local posting rules:**\n1. Use `gh issue comment <n> --body-file <file>` / `gh pr comment <n> --body-file <file>`\n   (body-file avoids shell-escaping issues with multi-line/markdown bodies). Write each body\n   to its own file first and post from the file — never inline the body with `--body \"...\"`.\n   The body-file path can live anywhere (a scratch/temp dir is fine); it does **not** need to\n   be inside the repo.\n2. **Always pass `-R JoshuaC215/agent-service-toolkit` on every `gh` write.** `gh issue\n   comment` / `gh pr comment` infer the repo from the current directory's git remote, so they\n   fail with `fatal: not a git repository` whenever you run from a scratch/temp dir (e.g. where\n   the body files live) or any path outside a clone. The explicit `-R owner/repo` flag makes\n   the command location-independent — use it always rather than relying on `cd`'ing into the\n   clone (and avoid `cd`, which can trigger a permission prompt).\n3. **Know whether each number is an issue or a PR before posting** — `gh issue comment` and\n   `gh pr comment` are **not** interchangeable (using the wrong one errors). When the draft\n   report doesn't already label it, check first:\n   ```sh\n   gh api repos/JoshuaC215/agent-service-toolkit/issues/<n> \\\n     --jq 'if .pull_request then \"PR\" else \"ISSUE\" end'\n   ```\n   (The `issues/<n>` endpoint works for both; the `.pull_request` field is present only on PRs.)\n4. **Do not append any attribution, footer, \"Generated by\", or co-author line to comment\n   bodies.** GitHub auto-links bare `#NNN` and `@user` natively, so keep those un-linked in\n   the comment text. A body that itself contains a fenced code block posts fine from a\n   body-file with no extra escaping.\n5. Confirm identity first with `gh api user --jq .login` → must be `JoshuaC215`. If it isn't\n   (or if writes would route through a Claude-App MCP server), stop and tell Joshua rather\n   than posting with the wrong attribution.\n6. To **comment then close** an issue, chain the close after the comment succeeds:\n   `gh issue comment <n> -R JoshuaC215/agent-service-toolkit --body-file <file> && gh issue close <n> -R JoshuaC215/agent-service-toolkit`.\n7. After posting, record the comment URL back into the report's `Status` (☑️ Posted).\n","frontmatter":{"name":"maintainer-response","description":"Draft replies to GitHub issues and pull requests on JoshuaC215/agent-service-toolkit in the authentic voice of the maintainer (JoshuaC215). Use when triaging, responding to, or reviewing issues/PRs on this repo — e.g. \"respond to issue 290\", \"review the open PRs\", \"draft a reply to this bug report\", or any batch maintainer triage. Produces human-in-the-loop drafts for review; it NEVER posts to GitHub on its own."},"isInternal":false,"tokens":5328,"sizeBytes":22086},{"name":"SKILL.md","path":".claude/skills/model-refresh/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JoshuaC215/agent-service-toolkit/HEAD/.claude/skills/model-refresh/SKILL.md","title":"model-refresh","category":"anthropic-skill","format":"markdown","content":"---\nname: model-refresh\ndescription: >-\n  Periodically audit the LLM model catalog in src/schema/models.py against what\n  each provider currently ships: add newly released models, remove/flag ones the\n  provider has deprecated or discourages, and re-point DEFAULT_MODEL fallbacks at\n  a current model. Use when asked to \"check for new models\", \"update the model\n  list\", \"refresh the model catalog\", or on the scheduled model-refresh trigger.\n---\n\n# Model Catalog Refresh\n\nKeep `src/schema/models.py` (the `AllModelEnum` catalog) current: add models a\nprovider shipped since the last refresh, remove ones the provider has deprecated\nor is actively steering users away from, and make sure `src/core/settings.py`'s\nper-provider `DEFAULT_MODEL` fallbacks point at a model that still exists.\n\n## Where model config lives\n\n| What | File |\n|---|---|\n| The enum of every supported model, per provider, with the provider's docs URL in each class docstring | `src/schema/models.py` (`AllModelEnum`) |\n| Per-provider default model + which models are \"available\" when a key is set | `src/core/settings.py` (`Settings.model_post_init`) |\n| Enum → API model-string mapping, and provider-specific construction quirks (temperature, streaming, tool-binding) | `src/core/llm.py` (`_MODEL_TABLE`, `get_model`) |\n| Unit tests asserting `get_model` builds the right LangChain class per model | `tests/core/test_llm.py` |\n| Settings/default-model tests | `tests/core/test_settings.py` |\n| Live smoke test (real API calls) | `scripts/check_live_models.py` |\n\n## Workflow\n\n1. **Survey.** For each provider block in `src/schema/models.py`, fetch the docs\n   URL already in that class's docstring and diff its current model list against\n   the enum values present. These docstring URLs are the **single canonical\n   source** for where to look — don't hardcode a second copy of them elsewhere,\n   since provider doc URLs shift over time and duplicated links silently rot.\n   - **If a docstring URL 404s, redirects to a generic landing page, or otherwise\n     no longer points at a model listing:** find the current canonical URL for\n     that provider's model docs and update the docstring to it as part of this\n     same change, then continue the survey from the corrected page. Don't skip\n     the provider just because the old link broke.\n   - A few providers need a second check beyond the docstring URL: Azure OpenAI\n     lags OpenAI's own releases (it's deployment-based — check what base models\n     Azure currently supports), Anthropic has a separate deprecations page worth\n     checking for retirement dates, and Vertex AI model paths sometimes differ\n     from the Gemini API's own names for the same model (e.g. `models/gemini-2.5-flash`\n     vs `gemini-2.5-flash`) — check both columns. Ollama takes a user-supplied\n     model name at runtime, so there's nothing to add there; just confirm the\n     generic pass-through in `llm.py` still works.\n   - **`docs.aws.amazon.com` returns HTTP 403 to `WebFetch`** (it blocks the\n     fetcher's user agent — this is not an egress-policy denial). Use `WebSearch`\n     for Bedrock model IDs instead; it reads the same doc pages server-side and\n     surfaces the model-card and inference-profile-support pages. The per-model\n     card pages (`.../model-card-anthropic-claude-<model>.html`) list the exact\n     `modelId` and profile IDs.\n2. **Classify each gap:**\n   - *New model, generally available* → add it.\n   - *New model, preview/experimental* → use judgment; this repo has taken preview\n     models before (e.g. `gemini-3-pro-preview`) when there's no GA alternative yet,\n     but prefer GA when one exists.\n   - *Existing model deprecated or sunset by the provider* → remove it, unless it's\n     the only model left for that provider (flag that case instead of leaving the\n     catalog empty).\n   - *Existing model merely superseded but still served* → leave it unless the\n     provider's docs explicitly say to migrate off it.\n3. **Match existing naming conventions** when adding a member:\n   - Enum member names are `SCREAMING_SNAKE_CASE`, usually family+size, e.g.\n     `SONNET_45`, `GEMINI_25_PRO`, `LLAMA_33_70B`. Version numbers drop dots\n     (`4.5` → `45`).\n   - Enum values are the exact string the provider's API expects\n     (`claude-sonnet-4-5`, `gemini-2.5-pro`, `gpt-5.1`) — copy verbatim from the\n     provider docs, don't guess.\n   - Values must be unique across *all* the enums in `AllModelEnum` —\n     `tests/schema/test_models.py` enforces this. Where two providers serve the\n     same upstream model one side needs a distinguishing form: `VertexAIModelName`\n     uses the `models/` prefix, so keep it on new Vertex entries.\n   - Keep provider families grouped and roughly ordered by size/generation within\n     a class, matching how they read today.\n4. **Apply changes across every coupled location** — do not edit only the enum:\n   - `src/schema/models.py`: add/remove the `StrEnum` member. Keep the docstring\n     URL current (see step 1).\n   - `src/core/settings.py` `model_post_init`: if the removed model was a\n     `DEFAULT_MODEL` fallback, repoint it at a remaining (ideally cheap/fast) model\n     for that provider — match the intent of the current defaults (e.g.\n     Haiku/Flash/Nano-tier, not the flagship).\n   - `src/core/llm.py`: nothing to do for a plain rename/add (the `_MODEL_TABLE`\n     and `if model_name in ...Name` dispatch are enum-driven), but check for any\n     provider-specific special-casing (see the Groq safeguard-model branch) that\n     might need to apply to the new model too.\n   - `tests/core/test_llm.py`, `tests/core/test_settings.py`: update any test that\n     hardcodes a model value being removed; add a case for a notable new model if\n     it has special handling (temperature override, tool-binding quirk, etc).\n   - `.env.example`, other docs: update only if they name a specific model that\n     changed (most don't).\n5. **Do not touch provider credentials or add live network calls during the\n   survey step.** This step is pure research + code edit, no API keys needed.\n6. **Live-test what you changed** (needs API keys — see \"Live testing\" below).\n7. **Summarize** what was added/removed/repointed and why, citing the provider\n   doc for each change. Flag any model rename explicitly — it's a breaking change\n   for existing deployments pinning the old enum value in `DEFAULT_MODEL`/\n   `AVAILABLE_MODELS` env config, not something to swap silently. Commit and push\n   per the repo's normal git workflow; open a PR only if asked.\n\n## Providers you can't live-verify (Bedrock, Azure, Vertex service-account, DeepSeek, OpenRouter)\n\nNot having a key for a provider is **not** a reason to skip it — a stale or\nbroken catalog entry is worse than a doc-sourced one. Update these from docs\njust like the rest, and in the PR mark them explicitly as **doc-only /\nunverified**, citing the provider page and calling out any caveat below so the\nnext person with a key knows exactly what to spot-check. Only leave a provider\nuntouched when the docs themselves are ambiguous *and* the change would be a\nproduct decision (e.g. adding a whole new pricing tier), not a freshness update.\n\n- **AWS Bedrock** — the enum *value* is passed straight to `ChatBedrock(model_id=...)`,\n  so it must be a real Bedrock ID, not a friendly label. Two gotchas:\n  1. The latest Claude models are **not invocable on-demand by their base model\n     ID** — a bare `anthropic.claude-...` call 400s with \"on-demand throughput\n     isn't supported.\" They must go through a cross-region **inference profile**:\n     the base ID prefixed with a geo (`us.`/`eu.`/`apac.`) or `global.`. Prefer\n     `global.` (routes dynamically, region-agnostic — the best fit for a catalog\n     value with no region context) and note in the PR that single-region\n     deployments not enrolled in Global CRIS should swap the prefix for their geo.\n  2. Bedrock inherits the same **sampling-parameter restrictions** as the direct\n     Anthropic API — e.g. a Sonnet-5-class model rejects `temperature`. If you\n     point a Bedrock entry at such a model, mirror the no-`temperature` branch\n     that already exists for it in `llm.py`'s Anthropic and Bedrock dispatch.\n  If AWS credentials happen to be present, `boto3.client(\"bedrock\").list_foundation_models()`\n  is the fastest way to confirm real IDs — but note that Bedrock access is a\n  separate enablement from plain AWS creds, so this can fail with an auth error\n  even when other AWS calls would work.\n- **Azure OpenAI** is the one genuinely heavier lift, because it's deployment-based\n  and the catalog is coupled in more places than the enum:\n  - `settings.py` `model_post_init` hardcodes a `required_models` set (currently\n    `{\"gpt-4o\", \"gpt-4o-mini\"}`) that it validates the `AZURE_OPENAI_DEPLOYMENT_MAP`\n    against — bumping the enum means bumping that set and the `.env.example`\n    deployment-map sample and the ~7 Azure cases in `tests/core/test_settings.py`.\n  - `llm.py` hardcodes `temperature=0.5` for the Azure path, but Azure's GPT-5-era\n    **reasoning** variants reject `temperature` (400). If you move Azure onto one,\n    add a no-`temperature` branch like the Anthropic/Bedrock Sonnet-5 handling.\n  - Changing an Azure enum value is a **breaking change** to every user's\n    deployment map (they name deployments after these keys). Treat an Azure\n    generation bump as its own reviewed change, and flag the deployment-map break\n    loudly — don't fold it silently into a routine refresh.\n\n## Live testing\n\n`scripts/check_live_models.py` sends a trivial one-word prompt to every model of\nevery provider that has credentials configured in the environment, and reports\nPASS/FAIL/SKIP per model. It is deliberately outside the pytest suite (real\nnetwork calls, tiny real cost) — run it by hand or from a scheduled trigger with\nkeys populated:\n\n```sh\nPYTHONPATH=src uv run python scripts/check_live_models.py                    # all configured providers\nPYTHONPATH=src uv run python scripts/check_live_models.py --provider anthropic google\n```\n\nA `SKIP` line means no credentials were present for that provider — that's\nexpected and not a failure. Only treat `FAIL` rows as build-blocking. Cost is\nnegligible (a handful of few-token completions per provider) but it's real spend\nagainst real keys — don't wire it into CI or run it on every commit.\n\nIf no provider credentials are configured in the current environment, skip this\nstep entirely rather than failing — the survey/edit step is still fully useful on\nits own.\n","frontmatter":{"name":"model-refresh","description":"Periodically audit the LLM model catalog in src/schema/models.py against what each provider currently ships: add newly released models, remove/flag ones the provider has deprecated or discourages, and re-point DEFAULT_MODEL fallbacks at a current model. Use when asked to \"check for new models\", \"update the model list\", \"refresh the model catalog\", or on the scheduled model-refresh trigger."},"isInternal":false,"tokens":2553,"sizeBytes":10584},{"name":"SKILL.md","path":".claude/skills/smoke-test/SKILL.md","rawUrl":"https://raw.githubusercontent.com/JoshuaC215/agent-service-toolkit/HEAD/.claude/skills/smoke-test/SKILL.md","title":"smoke-test","category":"anthropic-skill","format":"markdown","content":"---\nname: smoke-test\ndescription: >-\n  Run and interpret scripts/smoke_test.sh, the on-demand checks for optional\n  dependencies that CI and the default docker compose don't cover: the Postgres\n  and MongoDB checkpointers, the AG-UI endpoint, and LangFuse tracing. Use when\n  you've touched the memory/checkpointer layer, service startup/health, the\n  AG-UI adapter, LangFuse tracing, or bumped one of those dependencies — and want\n  to confirm the real integration still works without waiting for a full CI run.\n  Also use when deciding WHETHER such a check is even warranted for a change.\n---\n\n# Optional-dependency smoke tests\n\n`scripts/smoke_test.sh` exercises the parts of this project that aren't in the fast\nunit suite or the default CI path because they need real infrastructure: the\n**Postgres** and **MongoDB** checkpointers, the **AG-UI** endpoint, and **LangFuse**\ntracing. It spins up the real dependency in Docker, runs the service against it, and\nverifies the integration end-to-end, then tears everything down.\n\n\"Smoke test\" here means a quick *is-this-obviously-broken* confidence check, not\nexhaustive integration coverage. It's deliberately cheap to run one target and skip the\nrest.\n\n## First: should you run this at all?\n\nMost changes do **not** need it. It pulls images, starts containers, and takes minutes —\nthat time and token cost isn't worth it for work that doesn't touch these paths. Skip it\nfor: adding or editing an agent graph, prompts, or tools; docs; client/Streamlit-only\nchanges; anything already covered by `uv run pytest` and normal CI.\n\n**Run the relevant target when your change touches its path:**\n\n| You changed… | Run |\n| --- | --- |\n| `src/memory/*`, `initialize_database`, a saver/store class, checkpointer wiring | `postgres` and/or `mongo` |\n| `langgraph-checkpoint-postgres` / `-mongodb`, `pymongo`, `psycopg` versions | `postgres` / `mongo` |\n| `src/service/agui.py`, the AG-UI adapter, `ag-ui-*` deps | `agui` |\n| LangFuse tracing (the `CallbackHandler` wiring, `/health` auth check), `langfuse` dep | `langfuse` |\n| Service startup / lifespan / `/health` broadly | the most relevant one or two |\n| Nothing specific — periodic pre-release confidence pass | `all` |\n\nPrefer the **narrowest** target that covers your change. Run `all` (which includes the\nheavy LangFuse stack) only for a deliberate full pass — not as a reflex.\n\n## Running it\n\n```sh\n./scripts/smoke_test.sh                 # default: postgres, mongo, agui\n./scripts/smoke_test.sh mongo           # a single target\n./scripts/smoke_test.sh postgres agui   # a subset\n./scripts/smoke_test.sh langfuse        # heavy: ~5GB stack, run on its own\n./scripts/smoke_test.sh all             # everything, incl. langfuse\n```\n\n`langfuse` is excluded from the default run because it starts LangFuse's full 6-service\nself-host stack (~5GB of images). A green run ends with `--- All smoke tests passed ---`;\nexit code is non-zero on any failure or an unknown target.\n\n## Interpreting the result — and why a green ≠ \"it works\" trap is real\n\nThe whole design point is that **a passing API test does not prove the intended\ndependency was used.** The persistence test just checks that history survives across two\ninvokes, which is true for *any* working checkpointer — including SQLite. So if\n`DATABASE_TYPE=mongo` silently didn't take effect (dropped env, a fallback), the pytest\nstep would still pass while nothing touched Mongo.\n\nThat's why each target does a **second, independent check** that the specific dependency\nwas really exercised, and that's the line you should actually trust:\n\n- `✓ verified: N postgres checkpoint rows for this run's thread`\n- `✓ verified: N mongo checkpoint documents for this run's thread`\n- `✓ verified: N LangFuse traces recorded for this run`\n- `✓ verified: AG-UI streamed a complete run with the expected response`\n\nEach uses a **unique per-run thread id** (`SMOKE_THREAD_ID`), so the count reflects *this*\nrun's data even against a non-empty volume. A `✗ FAIL: … was NOT exercised as intended`\nmeans the dependency wasn't actually hit — treat that as a real failure, not flakiness.\n\nOther guards worth knowing when you read a failure:\n- **Port already in use:** `start_service` refuses to run if something is already on\n  `:8080`, so the health check can't accidentally pass against an unrelated process.\n- **Service died on startup:** it fails fast and dumps the service log instead of waiting\n  out the full timeout.\n- **LangFuse ingestion is async:** traces flow through a worker → ClickHouse pipeline, so\n  the script polls the traces API for a bit (usually a few seconds) before deciding.\n\n## Cloud-environment quirks (Claude Code on the web / sandboxes)\n\nThis script is meant to be runnable by an agent in a sandboxed cloud environment. A few\nthings bite there specifically:\n\n- **Docker daemon may not be running** (and can stop between steps). If `docker` errors\n  with \"Cannot connect to the daemon,\" start it: `(dockerd > /tmp/dockerd.log 2>&1 &)`\n  then wait a few seconds.\n- **Don't build the service image in-sandbox.** Container builds can't reach package\n  registries through the egress proxy (TLS/CA), so `docker compose build` fails. This is\n  exactly why the script runs the service on the **host** via `uv` and only puts the\n  *dependencies* in Docker. Don't \"fix\" it by baking the proxy CA into the committed\n  Dockerfile — that's sandbox-specific.\n- **Registry allowlist for LangFuse.** The LangFuse stack's `minio` image comes from\n  `cgr.dev` (Chainguard). In a restricted-egress environment add **`cgr.dev`** to the\n  network allowlist, or the pull 403s. (If a pull still 403s afterward on a *different*\n  host, add whatever host it names — some registries serve blobs from a separate CDN.)\n- **`pkill -f run_service.py` will kill your own shell.** `-f` matches full command lines,\n  and your command text contains that string, so it self-matches. To free port 8080 use\n  `fuser -k 8080/tcp` instead.\n- **Long foreground `sleep` loops can be killed** by the sandbox. When running the script\n  (or waiting on it), launch it in the background (`nohup … &`) and poll its log file\n  rather than blocking in the foreground.\n- **`docker compose exec` needs `.env`; `docker exec` doesn't.** The host-based flow\n  deliberately omits `.env`, and `docker compose exec` re-parses the full config (which\n  references `env_file: .env`) and fails without it. The script queries containers via\n  `docker exec <id>` (id from `docker compose ps -q`) to sidestep this.\n\n## Extending it\n\nThe script is built around one principle: **every target proves the real dependency was\nused, not just that the API returned something.** Keep that when adding a target.\n\nTo add a new optional-dependency target:\n1. Add a `smoke_<name>()` function following the existing shape: start the dependency\n   (Docker), `start_service` on the host with the right env, run an API-level check, then\n   a **dependency-identity check** (query the dependency directly for this run's data via\n   `assert_positive_count`, or assert on captured output like the AG-UI target).\n2. Register it in the `case` in the target loop and, if it should be part of a full pass,\n   in the `all` expansion. Decide whether it's light enough for the default set (DB-style)\n   or heavy enough to be opt-in (LangFuse-style).\n3. If it needs an add-on compose file, follow the `docker/compose.<dep>.yaml` pattern; if\n   it's a large external stack, prefer fetching upstream's compose pinned to a tag (as the\n   `langfuse` target does via `LANGFUSE_REF`) rather than vendoring it.\n\nKeep these out of the CI docker job: they live in `tests/smoke/` (not `tests/integration/`,\nwhich CI's `test-docker` job scopes to) and are marked `@pytest.mark.docker` so they're\nskipped unless `--run-docker` is passed.\n","frontmatter":{"name":"smoke-test","description":"Run and interpret scripts/smoke_test.sh, the on-demand checks for optional dependencies that CI and the default docker compose don't cover: the Postgres and MongoDB checkpointers, the AG-UI endpoint, and LangFuse tracing. Use when you've touched the memory/checkpointer layer, service startup/health, the AG-UI adapter, LangFuse tracing, or bumped one of those dependencies — and want to confirm the real integration still works without waiting for a full CI run. Also use when deciding WHETHER such a check is even warranted for a change."},"isInternal":false,"tokens":1927,"sizeBytes":7830},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/JoshuaC215/agent-service-toolkit/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\n## Comment style\n\n- Default to no comments in code you add or edit.\n- Only add a comment when the *why* is non-obvious: a hidden constraint, a subtle invariant, or a workaround for a specific bug. Never explain *what* the code does — clear naming should do that.\n- Never write multi-paragraph comment blocks or over-explain inline. Match the terseness of the surrounding code.\n- Exception: brand-new files may start with a short, genuinely useful module/file-level docstring. This does not license verbose inline comments throughout the rest of the file.\n- When editing an existing file, match its existing comment density and style rather than introducing a heavier style than what's already there.\n\n## Maintainer scaffolding vs. template content\n\nThis repo is a GitHub template. `.github/workflows/template-cleanup.yml` strips\nmaintainer-only scaffolding from downstream clones on first instantiation, driven by\nconventions rather than a hardcoded list. Put new scaffolding where the cleanup already\ncovers it:\n\n- Maintainer automation (skills, hooks, scheduled runs) goes in `.claude/` or\n  `docs/maintenance/` — whole-directory buckets, removed entirely, auto-covered.\n- A maintainer-only *workflow* must carry the owner-guard\n  `if: github.repository == 'JoshuaC215/agent-service-toolkit'` line; the cleanup\n  removes any guarded workflow, so this is auto-covered too.\n- ONLY when a maintainer-only file must live in a shared dir (`scripts/`, etc.) and\n  can't carry the guard marker do you add it to the explicit list in\n  `template-cleanup.yml`. Adding such a file without updating the cleanup is the one\n  case that leaks scaffolding into clones.\n","isInternal":false,"tokens":371,"sizeBytes":1672}],"systemPromptSnippet":"<yakaai_skills repo=\"JoshuaC215/agent-service-toolkit\">\n<!-- File: .claude/skills/dependency-refresh/references/coupling-constraints.md (Tokens: ~937 | Category: anthropic-skill) -->\n# Coupling constraints & gotchas\n\nProject-specific constraints hit during real refreshes — check here before\nfighting a resolver conflict or a CI surprise, and add new ones in the same PR\nthat hits them. Key points only; verify current specifics against the resolver\nand release notes.\n\n## uv & the cooldown mechanism\n\n- **Relative `exclude-newer` needs a recent uv.** Older uv warns\n  (`Failed to parse pyproject.toml during settings discovery`), ignores the\n  whole `[tool.uv]` table, and a re-lock with it silently strips the cooldown\n  metadata from `uv.lock`. Always lock with the repo's pinned uv version.\n- **A cooldown conflict looks like an unsatisfiable pin** — `uv lock` fails\n  with an `exclude-newer` hint. That's enforcement, not breakage: hold the\n  bump, or (security fixes only) add a commented temporary\n  `[tool.uv.exclude-newer-package]` override per SKILL.md.\n- **Re-locks refetch index metadata**, so unrelated wheel entries can drift out\n  of `uv.lock` with no version change. Harmless for supported platforms.\n\n## GitHub Actions\n\n- **Node runtime deprecations show up as CI warnings, not lockfile conflicts**,\n  and pinned `@vN` refs don't auto-upgrade. Check each action's `runs.using`\n  — including the nested `uses:` lines inside composite actions — and bump to\n  a major that ships the current runtime. Watch for behavior changes riding\n  along (e.g. `docker/build-push-action` turning on provenance attestations\n  by default).\n- **Some actions stop publishing floating major tags** for supply-chain\n  hardening (`astral-sh/setup-uv` as of v8) and must be pinned to an exact\n  release.\n\n## LangChain / LangGraph ecosystem\n\n- **`langchain` ⇄ `langgraph` move in lockstep** — the meta-package pins a\n  narrow `langgraph` range, so neither can be bumped past the other.\n- **`langgraph` ⇄ `langgraph-checkpoint` ⇄ the checkpointer packages** upgrade\n  as one coupled unit, and checkpointer majors can change stored-checkpoint\n  compatibility or drop APIs (the mongodb saver dropped its async class;\n  `src/memory/mongodb.py` bridges it). The test suite mocks the real backends,\n  so verify old checkpoints still read under new savers manually.\n- **LLM SDK bumps chain**: `langchain-<provider>` pulls a newer provider SDK\n  which drags its own floors (e.g. `openai` → `jiter`). Expect to move the\n  whole chain together.\n- **Provider adapters can cap shared libs** (e.g. `langchain-google-vertexai`\n  capping `pyarrow`) — a floor that won't move is usually one of these.\n- **Adapter majors can change real-API behavior** (transports, structured\n  output defaults) that the fake-model e2e can't exercise — check those paths\n  with a real provider key before trusting the bump in production.\n- **`langfuse` SDK usage here is tiny** (`CallbackHandler` +\n  `auth_check()`), so SDK majors are usually version-bumps; the heavyweight\n  self-host infra requirements belong to the Langfuse *server*, which this\n  repo doesn't bundle.\n\n## Tooling & misc\n\n- **`ruff` formatter output can change on minor bumps** — expect one-time\n  `ruff format` diffs, not just new lint rules.\n- **Uncapped dev-tool majors flood in via `uv lock --upgrade`.** Cap\n  temporarily to keep a disruptive major out of a routine round, and lift the\n  cap in a focused pass.\n- **`numpy` leads Python-version drops** — its floor is usually the first hard\n  constraint when deciding when to drop an old Python minor.\n- **Don't write resolver picks down as requirements.** Pin-reconciliation once\n  produced a `>=` floor on a transitive-only package that nothing required,\n  which later blocked an unrelated upgrade. Transitive-only packages don't\n  belong in `[project] dependencies` at all.\n\n## Python interpreter\n\n- **Validate new Python minors on a stable patch release, not an rc** — rc\n  builds have hit already-fixed interpreter/pydantic bugs that don't reproduce\n  on stable.\n\n<!-- File: .claude/skills/dependency-refresh/references/live-e2e.md (Tokens: ~909 | Category: anthropic-skill) -->\n# Live end-to-end verification (no API key needed)\n\nUnit tests mock the LLM and transport, so a running-service check is the only\nway to catch integration regressions from bumps to FastAPI/uvicorn/langgraph/\nthe checkpointers, and a real browser pass is the only thing that catches\nStreamlit-stack UI breakage.\n\n## Fake-model HTTP ladder\n\nThe service ships a fake model (`USE_FAKE_MODEL=true`) that satisfies the \"at\nleast one LLM key\" startup check and returns a canned reply — so you can drive\nthe real HTTP API on the upgraded stack without provider credentials. Leaving\n`AUTH_SECRET` unset keeps the endpoints unauthenticated (no bearer token).\n\nRun it natively (fastest — exercises the same `run_service.py`/uvicorn\nentrypoint and the full dependency stack, just not containerized):\n\n```sh\nPYTHONPATH=src USE_FAKE_MODEL=true HOST=127.0.0.1 PORT=8080 uv run python src/run_service.py &\n# then, once /health is up:\ncurl -s localhost:8080/health                       # {\"status\":\"ok\"}  -> app + lifespan booted\ncurl -s localhost:8080/info                          # agents/models listed\ncurl -s -XPOST localhost:8080/invoke   -H 'content-type: application/json' \\\n     -d '{\"message\":\"hi\",\"agent_id\":\"chatbot\",\"model\":\"fake\"}'         # -> completion + run_id\ncurl -s -N -XPOST localhost:8080/stream -H 'content-type: application/json' \\\n     -d '{\"message\":\"hi\",\"agent_id\":\"chatbot\",\"model\":\"fake\",\"stream_tokens\":true}'  # SSE tokens\n# persistence: invoke twice with the same \"thread_id\", then POST /history for that thread\n# and confirm the checkpointer returns the prior turns (validates the checkpointer packages).\n```\n\nWhat each check validates: `/health` + `/info` = FastAPI/uvicorn/pydantic boot\nand agent wiring; `/invoke` = a full graph run (langgraph + langchain +\nlangsmith `run_id`); `/stream` = the SSE `StreamingResponse` path; `/history`\non a reused `thread_id` = the checkpointer\n(langgraph-checkpoint-sqlite/aiosqlite by default).\n\n## Streamlit UI e2e (browser)\n\nCI never drives the actual Streamlit interface — pytest mocks the transport and\nthe docker CI job only checks health endpoints — so a real browser pass is the\nonly thing that catches Streamlit/pandas/pyarrow-level UI breakage from a bump.\nWith the fake-model service from above still running:\n\n```sh\nuv run streamlit run src/streamlit_app.py --server.headless true --server.port 8501 &\n# quick single-message smoke:\nuv run --with playwright python scripts/smoke_live_app.py http://localhost:8501\n# wider coverage (multi-turn resume, settings selectors, feedback widget, streaming toggle, ...):\nuv run --with playwright python scripts/e2e_ui_tests.py http://localhost:8501\n```\n\n`smoke_live_app.py` sends one chat message and verifies a streamed response\nrenders and settles. `e2e_ui_tests.py` is a small suite of key user journeys\nbuilt on the same idea — run `--list` to see them, or pass names to run a\nsubset. Both exit 0 on pass and save a diagnostic screenshot on failure, and\nboth are URL-parameterized so the same commands also run against the deployed\napp (omit the URL, or set `LIVE_APP_URL`). Run at least the smoke before\nopening any PR that bumps `streamlit`, its rendering stack (pandas, pyarrow,\npillow, altair), or the client/schema code the UI consumes; run the fuller\nsuite when a bump could plausibly touch chat history, settings, or the\nfeedback/streaming paths.\n\n## Containerized test\n\nTo validate the image itself (base image + in-image `uv sync`), run\n`docker compose up --build` and hit the same endpoints on the mapped ports —\nthis mirrors the `test-docker` CI job. It requires pulling the Python slim\nbase image, so it needs outbound Docker Hub access (not available in every\nsandbox; CI handles it via docker-in-docker).\n\n<!-- File: .claude/skills/dependency-refresh/SKILL.md (Tokens: ~2653 | Category: anthropic-skill) -->\n---\nname: dependency-refresh\ndescription: >-\n  Run a dependency & version refresh for this repo: Python libraries in\n  pyproject.toml/uv.lock, Docker base images, GitHub Actions pins, the uv CLI\n  version (CI + README + Dockerfiles), the supported Python range, and the\n  infra images (postgres/mongo/LangFuse) used by compose and the smoke tests.\n  Use when asked to \"update dependencies\", \"bump versions\", \"do a dependency\n  refresh\", or on the scheduled monthly dependency-refresh run. Self-contained\n  playbook; prior state (deferred majors, cooldown dates) lives in the previous\n  refresh PR, which Step 0 tells you how to find.\n---\n\n# Dependency & Version Refresh\n\nThis skill is the complete playbook. Running state — what moved each round and\nwhich majors are deferred — lives in **refresh PR descriptions**, not in a file.\n\nSupporting references in this skill's directory — read them at the step that\nneeds them, not up front:\n\n- `references/coupling-constraints.md` — project-specific coupling constraints\n  and gotchas learned in past refreshes (langchain⇄langgraph lockstep,\n  checkpointer majors, Actions runtime deprecations, uv quirks, …). Skim the\n  headings before triage; read fully when a resolver conflict or CI surprise hits.\n- `references/live-e2e.md` — the live end-to-end verification recipe\n  (fake-model HTTP ladder + Streamlit browser tests).\n\n## Conventions: PRs are the state record\n\nEvery refresh produces exactly one PR that both delivers the changes and\nrecords the state the *next* refresh starts from:\n\n- **Branch:** `claude/dependency-refresh-YYYY-MM-DD`\n- **Title:** `chore(deps): dependency refresh YYYY-MM-DD`\n- **Body sections** (the template):\n\n```markdown\n## What moved\n<pin raises, lock-only movement, Actions/uv CLI, infra images, base images —\ngrouped, with one-line release-note checks for anything 0.x or major>\n\n## Deferred majors & cooldowns\n<carry this table forward from the previous refresh PR: drop rows that landed\n(note them under \"What moved\"), keep rows still deferred, add newly discovered\nmajors. Every key-dependency major gets a row the round it is first seen.>\n\n| Upgrade | From → To | Major released | Eligible from | Notes / ROI |\n| --- | --- | --- | --- | --- |\n\n## Verification\n<checks run and their results, including which smoke targets were re-run and\nwhich were deliberately skipped and why>\n```\n\nNever stack extra state elsewhere: no journal files, no backlog docs. If a\nlearning is *reusable knowledge* (a coupling constraint, a gotcha), record it in\n`references/coupling-constraints.md` in the same PR — that file is knowledge,\nnot state.\n\n## Step 0 — Recover state from the last refresh PR\n\nBefore touching any pin, find the most recent refresh PR and read its body:\n\n1. Search PRs (state: all, newest first) with query\n   `\"dependency refresh\" in:title` scoped to this repo.\n2. Fallback: search by head-branch prefix `head:claude/dependency-refresh`.\n3. Last resort: `git log` on `main` for commits touching `uv.lock`, then look up\n   their PRs.\n\nFrom the latest one (or two, if the latest was narrow), extract: the deferred\nmajors table, anything \"deliberately held back\", and verification caveats\n(e.g. \"MongoDB untested\"). That table seeds this round's triage; carry it\nforward per the template above.\n\n## Cooldown policy\n\nTwo layers, one mechanism where possible:\n\n**1. Global 14-day resolver cooldown (enforced by uv).** `pyproject.toml` has:\n\n```toml\n[tool.uv]\nexclude-newer = \"14 days\"\n```\n\nEvery resolution (`uv lock`, `uv lock --upgrade`, `uv sync` re-locks) only\nconsiders releases at least 14 days old, so a compromised or broken\nfresh release can't enter the lock during its highest-risk window. This\nautomatically limits pin raises too: raising a `~=` pin to a version younger\nthan 14 days makes the resolve fail with an `exclude-newer` hint — that is the\npolicy working, not an error to route around. Hold that bump for the next\nround instead of overriding.\n\n**Security exception (the only sanctioned override):** if a release fixes a\nvulnerability that affects this repo (Dependabot PR, advisory), take it\nimmediately regardless of age — add a temporary per-package override with a\ncomment, and remove it in the next refresh once the release is older than the\nglobal cooldown:\n\n```toml\n[tool.uv.exclude-newer-package]\nsomepkg = \"0 days\"  # security fix CVE-XXXX; remove once >14 days old\n```\n\nCooldown mechanics to know: the span is recorded in `uv.lock`\n(`exclude-newer-span`), re-locks are stable, and it needs the repo's pinned uv\nversion — see the uv entries in `references/coupling-constraints.md`.\n\n**2. Three-month major cooldown for key dependencies (enforced by process).**\nKey dependencies are the ones where a bad major is expensive or sticky:\n\n- **Infra images:** `postgres`, `mongo` (compose/smoke tests) — majors can\n  change on-disk formats.\n- **App platform:** `streamlit`, `fastapi`, `pydantic`.\n- **Core agent stack:** `langgraph` + `langchain` and the coupled\n  `langgraph-checkpoint-*` packages.\n\nA new major of a key dependency is not adopted until **at least 3 months after\nits X.0.0 release date**, and then only in its own dedicated PR (never inside a\nsafe-bumps round). The round that first sees the new major adds a row to the\ndeferred table with the major's release date and the computed \"eligible from\"\ndate; subsequent rounds carry the row until it lands or is rejected. Eligible ≠\nautomatic: when the date arrives, do the normal major triage (below) before\nlanding it. Non-key majors need no 3-month wait — just ordinary triage.\n\n## Where versions live\n\n| What | File |\n| --- | --- |\n| Runtime deps + version pins | `pyproject.toml` → `[project] dependencies` |\n| Dev tooling (ruff, pyrefly, pytest, …) | `pyproject.toml` → `[dependency-groups] dev` |\n| Minimal client/Streamlit deps (a subset, **kept in sync** with the main list) | `pyproject.toml` → `[dependency-groups] client` |\n| Fully resolved versions (source of truth for what installs) | `uv.lock` |\n| Resolver cooldown | `pyproject.toml` → `[tool.uv] exclude-newer` |\n| Lint/format target Python | `pyproject.toml` → `[tool.ruff] target-version` |\n| CI test matrix | `.github/workflows/test.yml` (`python-version`) |\n| Container base image | `docker/Dockerfile.app`, `docker/Dockerfile.service` |\n| Supported Python range + classifiers | `pyproject.toml` → `requires-python`, `classifiers` |\n| GitHub Actions versions (`actions/checkout`, `setup-python`, `setup-uv`, `docker/*`, `codecov-action`, …) | `.github/workflows/*.yml` (`uses:`) |\n| `uv` CLI version — **four places, keep in sync** | `.github/workflows/test.yml` (both `setup-uv` steps' `version:`), `docker/Dockerfile.app` + `docker/Dockerfile.service` (`pip install uv==`), `README.md` quickstart (`curl .../uv/<version>/install.sh`) |\n| Infra images used by compose + smoke tests | `compose.yaml` (`postgres:` tag), `docker/compose.mongo.yaml` (`mongo:` tag), `scripts/smoke_test.sh` (`LANGFUSE_REF`) |\n\n## The workflow\n\n1. **Recover state** (Step 0 above).\n2. **Survey.** Compare resolved versions in `uv.lock` against the latest on\n   PyPI (`https://pypi.org/pypi/<package>/json` → `info.version`,\n   `info.requires_dist`, `info.requires_python`; each release's file\n   `upload_time_iso_8601` gives the age for cooldown checks). Also survey the\n   non-Python surfaces from the table above: Actions pins, uv CLI, Docker base\n   images, infra image tags, `LANGFUSE_REF`.\n3. **Triage** each candidate (principles below), honoring both cooldowns.\n   New key-dependency majors get a deferred-table row, not a bump.\n4. **Apply safe bumps** by editing pins in `pyproject.toml`. Preserve the\n   existing style: `~=` for app deps, `>=` floors for loosely-pinned libs.\n5. **Re-resolve:** `uv lock --upgrade`. Read conflicts carefully — they name\n   the exact transitive constraint (or cooldown) blocking a bump; check\n   `references/coupling-constraints.md` for known ones.\n6. **Reconcile pins to the lock.** Where the resolver picked higher than the\n   written pin (common for `>=` floors), raise the written pin to match — after\n   which `uv lock` must report no changes. **Sanity-check each reconciled floor\n   against what actual dependents require** before writing it down (grep the\n   package's block in `uv.lock` for reverse-dep `specifier`s) — otherwise a\n   floor nothing needs can block a future upgrade.\n7. **Sync + static verify:** `uv sync --frozen`, then `uv run ruff check .`,\n   `uv run ruff format --check .`, `uv run pyrefly check`, `uv run pytest`.\n8. **Live e2e:** follow `references/live-e2e.md` (fake-model HTTP ladder;\n   Streamlit browser smoke when the UI stack moved). If a bump touches\n   checkpointers, AG-UI, or LangFuse, also run the relevant\n   `scripts/smoke_test.sh` target (see the smoke-test skill; needs the Docker\n   daemon — `sudo dockerd &` if it isn't running).\n9. **PR:** branch, title, and body per the conventions above. Never push to\n   main. Record new reusable gotchas in the coupling reference in the same PR.\n\n## Triage principles\n\n- **Minor/patch bumps** within a major are generally safe — batch them.\n- **Major bumps** (and pre-1.0 `0.x` minors, which don't promise SemVer\n  stability): assess the actual code/behavior change against how the repo\n  *uses* the package, weigh ROI, and hold non-trivial ones for their own PR.\n  Grep for the import first — often the repo touches only a tiny, stable part\n  of the API.\n- **Key-dependency majors** additionally wait out the 3-month cooldown.\n- **Version-coupled packages** move in lockstep and a bump in one drags others\n  — the known couplings are in `references/coupling-constraints.md`; the\n  resolver will find the rest.\n- **Transitive-only deps** (nothing in the repo imports them) are lower risk\n  for our code; their risk lives in whatever consumes them (e.g.\n  Streamlit ↔ pandas/pyarrow). Don't add them to `[project] dependencies`\n  just to pin them.\n\n## Python version policy\n\nFollow CPython's cycle: a new minor each October, ~5 years of support (~18\nmonths bugfix, then security-only). Keep the declared range\n(`requires-python`), classifiers, CI matrix, ruff `target-version`, and Docker\nbase images mutually consistent — all listed in the table above. Adopt a new\nminor once the dependency stack resolves and passes on it; drop a minor when\nit nears security-EOL or a needed dependency drops it first (numpy is usually\nthe leading indicator). Verify support claims on a real interpreter of that\nversion (`uv python install 3.X`, then the full check ladder) — and use a\nstable patch release, not an `rc` (see the coupling reference).\n\n<!-- File: .claude/skills/maintainer-response/SKILL.md (Tokens: ~5328 | Category: anthropic-skill) -->\n---\nname: maintainer-response\ndescription: >-\n  Draft replies to GitHub issues and pull requests on JoshuaC215/agent-service-toolkit\n  in the authentic voice of the maintainer (JoshuaC215). Use when triaging, responding\n  to, or reviewing issues/PRs on this repo — e.g. \"respond to issue 290\", \"review the\n  open PRs\", \"draft a reply to this bug report\", or any batch maintainer triage. Produces\n  human-in-the-loop drafts for review; it NEVER posts to GitHub on its own.\n---\n\n# Maintainer Response (JoshuaC215)\n\nDraft issue/PR replies that sound like Joshua and reflect how he actually runs this\nproject. Output is always a **draft for human review** plus a short rationale. You do\nnot post comments, merge, close, or label unless the human explicitly tells you to in a\nseparate, follow-up instruction.\n\n## Operating rules (read first)\n\n1. **Never post or mutate GitHub state by default.** Draft only. Posting, closing,\n   merging, and labeling require an explicit go-ahead naming the specific item.\n2. **Research before drafting.** Read the full issue/PR body, every existing comment,\n   and the actual code/diff being discussed. Confirm whether Joshua has already replied —\n   if he has, you are continuing a thread, not opening one. **Also pull the item's\n   cross-references (linked issues/PRs, siblings tackling the same thing) and read\n   Joshua's prior comments across all of them before drafting — see \"Relate items before\n   drafting\" below.** An item is never triaged in isolation.\n3. **Never fabricate technical claims.** If you assert a file behaves a certain way,\n   verify it in the repo first (cite `path:line`). If you can't verify or can't\n   reproduce, say so and ask for a repro — that is what Joshua does. **For fork PRs whose\n   diff you can't open locally, cite `main` line numbers for context but phrase the fix\n   as \"from the description\" and don't assert what the patch actually does.**\n4. **Flag, don't bluff.** When a decision is genuinely the maintainer's call (accept vs\n   decline a feature, breaking changes, roadmap), present it as a decision for the human\n   with a recommendation, not a fait accompli.\n5. **Match scope to effort.** A vague one-line issue gets a short reply; a substantial,\n   well-tested PR gets real technical review.\n6. **Be concise — don't reiterate, don't lecture (this is the #1 AI tell).** When you're\n   simply agreeing with a contributor, *agree and state the asks* — do not restate the issue,\n   re-explain the mechanism, or quote `file:line` back at someone who clearly already knows\n   their own code. \"Looks good. Two asks before merge: …\" beats a paragraph re-deriving the\n   bug. Reserve file:line citations and root-cause explanations for when they add real\n   information (correcting a misunderstanding, justifying a decline, substantiating a concern\n   the contributor hasn't raised) — not as a demonstration that you read the diff. Shorter and\n   plainer almost always reads as more human. **Never explain back a caveat the contributor\n   already raised in their own PR/issue** (if their PR body already says \"this is\n   defense-in-depth, the real fix is JWTs,\" don't restate that at them — a 3-word\n   acknowledgment plus the actual ask is the whole comment). If a draft for a simple accept or\n   change-request runs more than a few sentences, cut it.\n\n## Relate items before drafting (clusters, not silos)\n\nIssues and PRs on this repo cluster: one feature request usually spawns one or more PRs,\nand multiple contributors often attack the same problem from different angles. **Treating\neach number as an isolated ticket is the single most common failure here** — it produces\ndrafts that suggest closing an issue whose linked PR moved last week, or reply to a PR in\na way that ignores or contradicts feedback Joshua already left on the parent issue. In a\nbatch, do the mapping *first*, before you draft a single reply.\n\n**Build the cluster map first.** For every item in scope, pull cross-references both\ndirections:\n- **PR → issue:** `Fixes/Closes/Resolves #NNN`, plus any `#NNN` referenced in the body or\n  clearly implied by what the diff does.\n- **Issue → PRs:** PRs that reference it, *and* PRs touching the same feature/files even\n  with no explicit link (e.g. two different chat-history PRs against one chat-history issue).\n- **Sibling PRs:** two or more PRs implementing the same request are a cluster even when\n  they never reference each other.\n\n**Then treat each cluster as one thing:**\n1. An issue plus its PRs and their siblings is **one cluster with one coherent position** —\n   not N independent drafts that can contradict each other or the maintainer's earlier call.\n2. **Read Joshua's prior comments across the whole cluster before drafting any member.** A\n   design preference or decision he already stated on the issue (or on a sibling PR) is\n   authoritative: every draft in the cluster must be **consistent with it and build on it** —\n   don't re-open a question he already answered, and don't reply to a PR as if the issue's\n   discussion didn't happen. If a draft would contradict his earlier stance, you misread the\n   thread — re-read it. When two contributors are mid-flight on the same feature, say so and\n   steer them to converge (\"you and @other are both on this — let's settle on #NNN's approach\")\n   rather than reviewing each PR as if it were the only one.\n3. **Cluster-aware staleness.** Nothing is stale in isolation. Before you suggest closing an\n   issue for inactivity, check whether a linked PR has recent activity (and vice-versa): an\n   issue whose parent PR moved last week is *live*. Judge activity across the cluster, not the\n   single number. If the item's own metadata looks stale but a linked item is active, that's a\n   flag-for-the-maintainer, not a close.\n\nDraft per cluster, then split into per-item comments only where each contributor needs a\ndifferent concrete ask — and cross-link those drafts so the relationship is visible.\n\n## Voice & tone\n\n- Warm, casual-professional, first-person singular (\"I think\", \"I'm not inclined\",\n  \"I'll take a look\"). He owns the project as an individual — rarely \"we\".\n- Short, conversational sentences. Contractions throughout.\n- Opens casually: \"Hey\", \"Hi @user\", \"Hmm,\", or just thanks. Often leads with genuine\n  appreciation for contributions.\n- Light, tasteful emoji — at most one or two: 🙏 🫡 😃 :) and `!!` for real enthusiasm.\n  Don't overdo it; **many comments have none — default to zero unless there's genuine\n  enthusiasm**, vary your openers, and don't reuse the same emoji (🙏/🫡) on every draft\n  across a batch.\n- Direct but softened feedback: \"Nit:\", \"I think\", \"my bad\", \"maybe I missed something\".\n  Admits uncertainty openly (\"I'm not able to test this\", \"I'm not sure either\").\n- Closes with a clear next step (\"Should be good to merge after that\", \"open a dedicated\n  issue\", \"let me know if this works\").\n- Uses markdown naturally: ```sh blocks for commands, ```py for runnable examples,\n  bullet lists when enumerating asks, and deep links to `file.py#Lxx` and to LangGraph /\n  LangChain docs. He pastes working code rather than only describing it.\n\n### Stock phrases (reuse, don't copy robotically)\n- Thanks: \"Thanks for contributing, awesome!\" · \"This is cool, thanks @user!\" · \"This is\n  really excellent work 🫡\" · \"This is great 🙏\" · plain \"Thanks!\"\n- Accepting: \"Sounds good, I would welcome a PR for this.\" · \"Open to this idea.\" ·\n  \"I'd welcome a pull request if it wasn't *too* complex and had good tests.\"\n- Requesting changes: \"Can you run the linting and type checking and push the fixes?\" ·\n  \"Nit: ...\" · \"Rest of the changes look great!!\"\n- Declining/deferring: \"I'm not too inclined to take it on.\" · \"It's probably not\n  something I'll have time to develop myself.\" · \"I'd rather use integrations for\n  dedicated tools than build and maintain them in the project directly.\"\n- Scoping: \"maybe open a dedicated issue and discuss it in more detail first before\n  coding — happy to provide feedback early so you don't spend too much time on something\n  that's ultimately rejected.\"\n- Triage: \"Can you post the full repro steps...? Very difficult to debug without more\n  information. Thanks!\"\n- Availability: \"I'm traveling the next few days but will take a look next week, thanks!\"\n\n## The lint/CI ask (use verbatim when a PR needs cleanup)\n\nCI runs ruff format check, ruff check, pyrefly, and pytest (+docker) on every PR to `main`\n(`.github/workflows/test.yml`). When a contributor's PR is failing or unformatted:\n\n```sh\nuv run ruff format\nuv run ruff check --output-format github\nuv run pyrefly check\n```\n\nAsk them to run these and push the fixes. For test expectations, point to `tests/` and\nnote tests are run locally without Docker (`uv sync --frozen` → `pytest`).\n\n**Lead with the substantive ask.** Only attach this lint block when formatting/types are\nactually in question; keep it secondary. When the real blocker is design or behavior\n(e.g. a breaking default), don't let boilerplate lint instructions bury the point.\n\n**Don't frame it as routine.** Most contributors are first-timers who have no reason to know\nthese checks are \"normal\" here — so never say \"the usual lint pass\" / \"as always\" / \"the\nstandard checks.\" That reads as aggressive or cliquey. Just offer the commands plainly and\nhelpfully (\"if you run these and push, that'll get CI green\"). For a simple, agreed change,\na short \"run a lint/type pass\" with the block is plenty — no preamble about CI policy.\n\n## Decision heuristics (what Joshua actually accepts)\n\n> **Current stance (2026 — read this first).** The repo is more dormant than in its early\n> days and Joshua has less time to maintain it, so he is **more conservative about taking on\n> contributions** than the older history suggests. Default toward **decline / defer / close**\n> unless the contribution is **REALLY compelling or REALLY well done *and* tightly scoped**.\n> The bar for \"I'd welcome a PR\" enthusiasm is higher now — reserve it for changes that are\n> genuinely low-maintenance and clearly worth it. When unsure, lean conservative and let\n> demand prove itself (see \"gauge demand\" below) rather than greenlighting eagerly.\n\n**Lean ACCEPT** when the change is:\n- Optional and config-driven (off by default; gated behind a setting), low maintenance\n  burden, aligned with LangGraph/LangChain primitives, and has good tests.\n- A focused bug fix with a clear repro.\n- Requested/upvoted by multiple people (he cites upvotes as signal).\n\n**Lean DECLINE / DEFER** when the change:\n- Adds ongoing maintenance burden or heavy new dependencies.\n- Duplicates an existing integration (LangFuse, LangSmith) — he prefers pointing to\n  dedicated tools over rebuilding them in-repo.\n- Relies on immature/uncertain external protocols, or is large/vague/speculative.\n- Is a big aggregate PR — he steers contributors to **split into focused PRs/issues** and\n  discuss design in an issue *before* writing lots of code.\n\n**Conditional / \"I'd welcome a PR\"**: green-light offers to contribute *selectively* — gate\non tight scope + tests, and ask for a design sketch or docs link first so the contributor\ndoesn't waste effort. Given the current conservative stance, prefer this over an eager yes,\nand only extend a warm \"I'd welcome a PR\" when the idea is genuinely compelling and low-cost\nto maintain.\n\n**Gauge demand instead of committing.** For a reasonable-but-not-compelling feature, the\npreferred move now is to **politely decline as a core feature** while **inviting upvotes /\nconcrete use cases** (\"if others are hitting this, give it a 👍 or chime in — that helps me\nprioritize\"). This keeps the door open without taking on work, and lets real demand surface.\n\n**Close stale / low-activity issues.** Long-open issues with little traction are fine to\n**close politely** to keep the backlog focused — acknowledge any valid point, explain you're\nclosing for low activity, and leave the door open to revisit if demand grows or a better\n(ideally LangGraph-native) option appears. Warm, not curt.\n\n**Apologize for slow responses.** Many threads are months (or ~a year) old. Open with a brief,\ngenuine apology for the delay; when relevant it's fine to give light context (e.g. reduced\nbandwidth / a job change pulled focus away). Keep it short — one clause, then move on.\n\n**Things he won't merge as-is**: secrets/.env checked in, breaking changes flipped on by\ndefault, features he can't test with no owner for future support (he'll ask \"are you OK\nif I point future feedback/errors on this to you?\").\n\n### Be skeptical of automated / AI-generated PRs\nThis repo attracts bulk AI-generated PRs (often with a \"Generated with Claude Code\"\nfooter, \"fix/find-00X\" branch names, or spec docs). Don't rubber-stamp them. Check that:\nthe claimed bug is real and reproducible in this codebase; the fix doesn't introduce a\nbreaking default; the PR has real tests and passing CI (note when CI hasn't run because\nit's from a fork); and the change matches project conventions. It's fine — and on-brand —\nto politely ask the author to confirm the real-world repro, justify a default, or narrow\nscope. Security reports get a courteous, non-defensive reply that asks precisely where\nthe issue surfaces if it isn't already demonstrated.\n\n## Response patterns by category\n\n- **Usage question** → answer directly with the relevant file/line + a short runnable\n  snippet; correct wrong assumptions gently (\"I think the example you copied is the wrong\n  one — you want ...\"). Close the loop (\"let me know if that works\").\n- **Bug report (clear repro)** → confirm, point at the root cause in code, suggest or\n  endorse the fix; invite a PR or note you'll patch it.\n- **Bug report (no repro)** → thank them, ask for full repro steps / versions / model;\n  don't speculate at length.\n- **Feature request** → apply the accept/decline/defer heuristics (default conservative);\n  give the reasoning out loud; offer the path forward — welcome a PR with tests *only if\n  compelling + tightly scoped* / open a design issue first / suggest an existing integration\n  / **decline as a core feature but invite upvotes to gauge demand** / **close if stale with\n  a warm, door-open note**.\n- **PR — good** → brief, genuine agreement, any nits/asks as a short list, the lint ask only\n  if needed, a clear merge gate. Don't re-summarize the PR back to its author. For a simple\n  change you're accepting, \"Looks good. Two asks before merge: …\" is the whole comment. Offer\n  to help only when there's something to help with.\n- **PR — needs design discussion** → appreciate the effort, explain the concern, propose\n  splitting or redesigning, keep the door open.\n- **Contributor is blocked / asked a question** → **answer the actual question first.** Before\n  re-engaging a stale PR with \"add a test / run lint,\" check the PR *and its linked issues* for\n  a direct question the contributor asked (often a \"how do I wire X?\" that went unanswered).\n  Answer it concretely — a short runnable snippet is warranted and on-brand for a genuine \"how\n  do I\" — *then* get to the polish asks. Jumping to test/lint while ignoring their blocker reads\n  as dismissive and is why the PR stalled.\n- **Declining** → keep it to ~4 short sentences of rationale, then the door-open close.\n  Don't over-explain; warmth + a clear reason beats a long justification.\n- **Spam / empty / unintelligible / non-English one-liner** → brief, kind, ask for\n  clarification in English or note what's missing. Don't write paragraphs. For\n  **duplicates**, reference both issue numbers and ask to consolidate into one.\n- **Security report** → courteous, take it seriously, ask precisely where the\n  vuln surfaces if not demonstrated; don't get defensive, don't overpromise.\n\n## Workflow\n\n1. **Identify** the item(s) and pull full context (body, all comments, code/diff, CI\n   status, whether it's a fork, whether Joshua already replied).\n2. **Relate** — build the cluster map (see \"Relate items before drafting\"): link each\n   item to its issues/PRs/siblings and read Joshua's prior comments across the whole\n   cluster. Do this for the entire batch before drafting anything.\n3. **Classify** category + accept/decline/defer disposition — per cluster, so members\n   don't contradict each other.\n4. **Draft** the reply in Joshua's voice, grounded in verified facts and consistent with\n   his earlier stance anywhere in the cluster.\n5. **Report** for each item: a 1-2 line summary of the request, your research assessment\n   (is the claim true? does it reproduce? CI state?), the decision the human needs to make\n   with a clear recommendation, and the draft reply in a quoted block ready to paste.\n   Group items in the same cluster together under a shared header so the relationship is\n   obvious at a glance.\n6. **Wait** for the human's decision. Only post if explicitly told to, for the named item.\n\n## Output format for a batch\n\n**Always hyperlink issue/PR numbers in the report** (summary tables, section titles, and\ninline cross-references) so they're one click to open and compare — never leave them as\nbare `#NNN`. Use the full URL: issues →\n`https://github.com/JoshuaC215/agent-service-toolkit/issues/<NNN>`, PRs →\n`https://github.com/JoshuaC215/agent-service-toolkit/pull/<NNN>`. Markdown form\n`[#NNN](url)`. (This applies to the report you show the human — not to the draft reply\ntext itself, where GitHub auto-links `#NNN` natively.)\n\n**Fencing:** wrap each draft reply in a fenced block. If the draft itself contains a code\nblock (e.g. the ` ```sh ` lint ask), fence the *outer* draft block with **four backticks**\n` ```` ` so the inner triple-backtick block doesn't close it early — otherwise every section\nafter it renders broken. Verify fences are balanced before delivering the report.\n\n**Group clustered items together.** When an issue and its PRs (or sibling PRs) form a\ncluster, present them under one short cluster header that states the shared decision and\nJoshua's governing prior stance, then list the per-item drafts beneath it — so the human\nsees one holistic position, not scattered contradictory drafts. Standalone items just get\ntheir own entry.\n\nFor each item produce:\n\n> **[#NNN](url) — <title>** (`issue`/`PR`, author, age) — category · disposition\n> **Request:** …\n> **Assessment:** … (verified facts, repro, CI, fork status)\n> **Decision needed:** … **Recommendation:** …\n> **Draft reply:**\n> ```\n> <the comment, in Joshua's voice>\n> ```\n\nKeep drafts tight. When in doubt, err toward the shorter, friendlier version Joshua would\nactually type.\n\n## Posting locally (clean attribution)\n\nOnly post when Joshua has explicitly cleared the specific item (see the `Status` column in\n`DRAFT_RESPONSES_REPORT.md`: post ☑️/✅ items; never post ⏳ \"Needs decision\" items, and\nre-confirm 📝 \"Revised — confirm\" items first).\n\n**How the comment is attributed depends entirely on which credential posts it:**\n\n- **Cloud / Claude Code on the web** posts through the Claude GitHub App and a cloud routine\n  layer that *unconditionally* adds a \"with Claude\" badge and a \"Generated by Claude Code\"\n  footer to the comment body. This is **not** suppressible by any setting (the `attribution`\n  key in `settings.json` only affects commit trailers and PR *bodies*, not comments). See\n  anthropics/claude-code#62791.\n- **Locally on Joshua's laptop**, post via the **`gh` CLI authenticated as Joshua**\n  (`gh auth login` / a PAT) — *not* through a GitHub-App-backed MCP server. Posted this way\n  the comment is authored directly by `JoshuaC215` with **no app badge and no footer**,\n  because (a) it's his own token, not an on-behalf-of app, and (b) the body is exactly what\n  you write.\n\n**Local posting rules:**\n1. Use `gh issue comment <n> --body-file <file>` / `gh pr comment <n> --body-file <file>`\n   (body-file avoids shell-escaping issues with multi-line/markdown bodies). Write each body\n   to its own file first and post from the file — never inline the body with `--body \"...\"`.\n   The body-file path can live anywhere (a scratch/temp dir is fine); it does **not** need to\n   be inside the repo.\n2. **Always pass `-R JoshuaC215/agent-service-toolkit` on every `gh` write.** `gh issue\n   comment` / `gh pr comment` infer the repo from the current directory's git remote, so they\n   fail with `fatal: not a git repository` whenever you run from a scratch/temp dir (e.g. where\n   the body files live) or any path outside a clone. The explicit `-R owner/repo` flag makes\n   the command location-independent — use it always rather than relying on `cd`'ing into the\n   clone (and avoid `cd`, which can trigger a permission prompt).\n3. **Know whether each number is an issue or a PR before posting** — `gh issue comment` and\n   `gh pr comment` are **not** interchangeable (using the wrong one errors). When the draft\n   report doesn't already label it, check first:\n   ```sh\n   gh api repos/JoshuaC215/agent-service-toolkit/issues/<n> \\\n     --jq 'if .pull_request then \"PR\" else \"ISSUE\" end'\n   ```\n   (The `issues/<n>` endpoint works for both; the `.pull_request` field is present only on PRs.)\n4. **Do not append any attribution, footer, \"Generated by\", or co-author line to comment\n   bodies.** GitHub auto-links bare `#NNN` and `@user` natively, so keep those un-linked in\n   the comment text. A body that itself contains a fenced code block posts fine from a\n   body-file with no extra escaping.\n5. Confirm identity first with `gh api user --jq .login` → must be `JoshuaC215`. If it isn't\n   (or if writes would route through a Claude-App MCP server), stop and tell Joshua rather\n   than posting with the wrong attribution.\n6. To **comment then close** an issue, chain the close after the comment succeeds:\n   `gh issue comment <n> -R JoshuaC215/agent-service-toolkit --body-file <file> && gh issue close <n> -R JoshuaC215/agent-service-toolkit`.\n7. After posting, record the comment URL back into the report's `Status` (☑️ Posted).\n\n<!-- File: .claude/skills/model-refresh/SKILL.md (Tokens: ~2553 | Category: anthropic-skill) -->\n---\nname: model-refresh\ndescription: >-\n  Periodically audit the LLM model catalog in src/schema/models.py against what\n  each provider currently ships: add newly released models, remove/flag ones the\n  provider has deprecated or discourages, and re-point DEFAULT_MODEL fallbacks at\n  a current model. Use when asked to \"check for new models\", \"update the model\n  list\", \"refresh the model catalog\", or on the scheduled model-refresh trigger.\n---\n\n# Model Catalog Refresh\n\nKeep `src/schema/models.py` (the `AllModelEnum` catalog) current: add models a\nprovider shipped since the last refresh, remove ones the provider has deprecated\nor is actively steering users away from, and make sure `src/core/settings.py`'s\nper-provider `DEFAULT_MODEL` fallbacks point at a model that still exists.\n\n## Where model config lives\n\n| What | File |\n|---|---|\n| The enum of every supported model, per provider, with the provider's docs URL in each class docstring | `src/schema/models.py` (`AllModelEnum`) |\n| Per-provider default model + which models are \"available\" when a key is set | `src/core/settings.py` (`Settings.model_post_init`) |\n| Enum → API model-string mapping, and provider-specific construction quirks (temperature, streaming, tool-binding) | `src/core/llm.py` (`_MODEL_TABLE`, `get_model`) |\n| Unit tests asserting `get_model` builds the right LangChain class per model | `tests/core/test_llm.py` |\n| Settings/default-model tests | `tests/core/test_settings.py` |\n| Live smoke test (real API calls) | `scripts/check_live_models.py` |\n\n## Workflow\n\n1. **Survey.** For each provider block in `src/schema/models.py`, fetch the docs\n   URL already in that class's docstring and diff its current model list against\n   the enum values present. These docstring URLs are the **single canonical\n   source** for where to look — don't hardcode a second copy of them elsewhere,\n   since provider doc URLs shift over time and duplicated links silently rot.\n   - **If a docstring URL 404s, redirects to a generic landing page, or otherwise\n     no longer points at a model listing:** find the current canonical URL for\n     that provider's model docs and update the docstring to it as part of this\n     same change, then continue the survey from the corrected page. Don't skip\n     the provider just because the old link broke.\n   - A few providers need a second check beyond the docstring URL: Azure OpenAI\n     lags OpenAI's own releases (it's deployment-based — check what base models\n     Azure currently supports), Anthropic has a separate deprecations page worth\n     checking for retirement dates, and Vertex AI model paths sometimes differ\n     from the Gemini API's own names for the same model (e.g. `models/gemini-2.5-flash`\n     vs `gemini-2.5-flash`) — check both columns. Ollama takes a user-supplied\n     model name at runtime, so there's nothing to add there; just confirm the\n     generic pass-through in `llm.py` still works.\n   - **`docs.aws.amazon.com` returns HTTP 403 to `WebFetch`** (it blocks the\n     fetcher's user agent — this is not an egress-policy denial). Use `WebSearch`\n     for Bedrock model IDs instead; it reads the same doc pages server-side and\n     surfaces the model-card and inference-profile-support pages. The per-model\n     card pages (`.../model-card-anthropic-claude-<model>.html`) list the exact\n     `modelId` and profile IDs.\n2. **Classify each gap:**\n   - *New model, generally available* → add it.\n   - *New model, preview/experimental* → use judgment; this repo has taken preview\n     models before (e.g. `gemini-3-pro-preview`) when there's no GA alternative yet,\n     but prefer GA when one exists.\n   - *Existing model deprecated or sunset by the provider* → remove it, unless it's\n     the only model left for that provider (flag that case instead of leaving the\n     catalog empty).\n   - *Existing model merely superseded but still served* → leave it unless the\n     provider's docs explicitly say to migrate off it.\n3. **Match existing naming conventions** when adding a member:\n   - Enum member names are `SCREAMING_SNAKE_CASE`, usually family+size, e.g.\n     `SONNET_45`, `GEMINI_25_PRO`, `LLAMA_33_70B`. Version numbers drop dots\n     (`4.5` → `45`).\n   - Enum values are the exact string the provider's API expects\n     (`claude-sonnet-4-5`, `gemini-2.5-pro`, `gpt-5.1`) — copy verbatim from the\n     provider docs, don't guess.\n   - Values must be unique across *all* the enums in `AllModelEnum` —\n     `tests/schema/test_models.py` enforces this. Where two providers serve the\n     same upstream model one side needs a distinguishing form: `VertexAIModelName`\n     uses the `models/` prefix, so keep it on new Vertex entries.\n   - Keep provider families grouped and roughly ordered by size/generation within\n     a class, matching how they read today.\n4. **Apply changes across every coupled location** — do not edit only the enum:\n   - `src/schema/models.py`: add/remove the `StrEnum` member. Keep the docstring\n     URL current (see step 1).\n   - `src/core/settings.py` `model_post_init`: if the removed model was a\n     `DEFAULT_MODEL` fallback, repoint it at a remaining (ideally cheap/fast) model\n     for that provider — match the intent of the current defaults (e.g.\n     Haiku/Flash/Nano-tier, not the flagship).\n   - `src/core/llm.py`: nothing to do for a plain rename/add (the `_MODEL_TABLE`\n     and `if model_name in ...Name` dispatch are enum-driven), but check for any\n     provider-specific special-casing (see the Groq safeguard-model branch) that\n     might need to apply to the new model too.\n   - `tests/core/test_llm.py`, `tests/core/test_settings.py`: update any test that\n     hardcodes a model value being removed; add a case for a notable new model if\n     it has special handling (temperature override, tool-binding quirk, etc).\n   - `.env.example`, other docs: update only if they name a specific model that\n     changed (most don't).\n5. **Do not touch provider credentials or add live network calls during the\n   survey step.** This step is pure research + code edit, no API keys needed.\n6. **Live-test what you changed** (needs API keys — see \"Live testing\" below).\n7. **Summarize** what was added/removed/repointed and why, citing the provider\n   doc for each change. Flag any model rename explicitly — it's a breaking change\n   for existing deployments pinning the old enum value in `DEFAULT_MODEL`/\n   `AVAILABLE_MODELS` env config, not something to swap silently. Commit and push\n   per the repo's normal git workflow; open a PR only if asked.\n\n## Providers you can't live-verify (Bedrock, Azure, Vertex service-account, DeepSeek, OpenRouter)\n\nNot having a key for a provider is **not** a reason to skip it — a stale or\nbroken catalog entry is worse than a doc-sourced one. Update these from docs\njust like the rest, and in the PR mark them explicitly as **doc-only /\nunverified**, citing the provider page and calling out any caveat below so the\nnext person with a key knows exactly what to spot-check. Only leave a provider\nuntouched when the docs themselves are ambiguous *and* the change would be a\nproduct decision (e.g. adding a whole new pricing tier), not a freshness update.\n\n- **AWS Bedrock** — the enum *value* is passed straight to `ChatBedrock(model_id=...)`,\n  so it must be a real Bedrock ID, not a friendly label. Two gotchas:\n  1. The latest Claude models are **not invocable on-demand by their base model\n     ID** — a bare `anthropic.claude-...` call 400s with \"on-demand throughput\n     isn't supported.\" They must go through a cross-region **inference profile**:\n     the base ID prefixed with a geo (`us.`/`eu.`/`apac.`) or `global.`. Prefer\n     `global.` (routes dynamically, region-agnostic — the best fit for a catalog\n     value with no region context) and note in the PR that single-region\n     deployments not enrolled in Global CRIS should swap the prefix for their geo.\n  2. Bedrock inherits the same **sampling-parameter restrictions** as the direct\n     Anthropic API — e.g. a Sonnet-5-class model rejects `temperature`. If you\n     point a Bedrock entry at such a model, mirror the no-`temperature` branch\n     that already exists for it in `llm.py`'s Anthropic and Bedrock dispatch.\n  If AWS credentials happen to be present, `boto3.client(\"bedrock\").list_foundation_models()`\n  is the fastest way to confirm real IDs — but note that Bedrock access is a\n  separate enablement from plain AWS creds, so this can fail with an auth error\n  even when other AWS calls would work.\n- **Azure OpenAI** is the one genuinely heavier lift, because it's deployment-based\n  and the catalog is coupled in more places than the enum:\n  - `settings.py` `model_post_init` hardcodes a `required_models` set (currently\n    `{\"gpt-4o\", \"gpt-4o-mini\"}`) that it validates the `AZURE_OPENAI_DEPLOYMENT_MAP`\n    against — bumping the enum means bumping that set and the `.env.example`\n    deployment-map sample and the ~7 Azure cases in `tests/core/test_settings.py`.\n  - `llm.py` hardcodes `temperature=0.5` for the Azure path, but Azure's GPT-5-era\n    **reasoning** variants reject `temperature` (400). If you move Azure onto one,\n    add a no-`temperature` branch like the Anthropic/Bedrock Sonnet-5 handling.\n  - Changing an Azure enum value is a **breaking change** to every user's\n    deployment map (they name deployments after these keys). Treat an Azure\n    generation bump as its own reviewed change, and flag the deployment-map break\n    loudly — don't fold it silently into a routine refresh.\n\n## Live testing\n\n`scripts/check_live_models.py` sends a trivial one-word prompt to every model of\nevery provider that has credentials configured in the environment, and reports\nPASS/FAIL/SKIP per model. It is deliberately outside the pytest suite (real\nnetwork calls, tiny real cost) — run it by hand or from a scheduled trigger with\nkeys populated:\n\n```sh\nPYTHONPATH=src uv run python scripts/check_live_models.py                    # all configured providers\nPYTHONPATH=src uv run python scripts/check_live_models.py --provider anthropic google\n```\n\nA `SKIP` line means no credentials were present for that provider — that's\nexpected and not a failure. Only treat `FAIL` rows as build-blocking. Cost is\nnegligible (a handful of few-token completions per provider) but it's real spend\nagainst real keys — don't wire it into CI or run it on every commit.\n\nIf no provider credentials are configured in the current environment, skip this\nstep entirely rather than failing — the survey/edit step is still fully useful on\nits own.\n\n<!-- File: .claude/skills/smoke-test/SKILL.md (Tokens: ~1927 | Category: anthropic-skill) -->\n---\nname: smoke-test\ndescription: >-\n  Run and interpret scripts/smoke_test.sh, the on-demand checks for optional\n  dependencies that CI and the default docker compose don't cover: the Postgres\n  and MongoDB checkpointers, the AG-UI endpoint, and LangFuse tracing. Use when\n  you've touched the memory/checkpointer layer, service startup/health, the\n  AG-UI adapter, LangFuse tracing, or bumped one of those dependencies — and want\n  to confirm the real integration still works without waiting for a full CI run.\n  Also use when deciding WHETHER such a check is even warranted for a change.\n---\n\n# Optional-dependency smoke tests\n\n`scripts/smoke_test.sh` exercises the parts of this project that aren't in the fast\nunit suite or the default CI path because they need real infrastructure: the\n**Postgres** and **MongoDB** checkpointers, the **AG-UI** endpoint, and **LangFuse**\ntracing. It spins up the real dependency in Docker, runs the service against it, and\nverifies the integration end-to-end, then tears everything down.\n\n\"Smoke test\" here means a quick *is-this-obviously-broken* confidence check, not\nexhaustive integration coverage. It's deliberately cheap to run one target and skip the\nrest.\n\n## First: should you run this at all?\n\nMost changes do **not** need it. It pulls images, starts containers, and takes minutes —\nthat time and token cost isn't worth it for work that doesn't touch these paths. Skip it\nfor: adding or editing an agent graph, prompts, or tools; docs; client/Streamlit-only\nchanges; anything already covered by `uv run pytest` and normal CI.\n\n**Run the relevant target when your change touches its path:**\n\n| You changed… | Run |\n| --- | --- |\n| `src/memory/*`, `initialize_database`, a saver/store class, checkpointer wiring | `postgres` and/or `mongo` |\n| `langgraph-checkpoint-postgres` / `-mongodb`, `pymongo`, `psycopg` versions | `postgres` / `mongo` |\n| `src/service/agui.py`, the AG-UI adapter, `ag-ui-*` deps | `agui` |\n| LangFuse tracing (the `CallbackHandler` wiring, `/health` auth check), `langfuse` dep | `langfuse` |\n| Service startup / lifespan / `/health` broadly | the most relevant one or two |\n| Nothing specific — periodic pre-release confidence pass | `all` |\n\nPrefer the **narrowest** target that covers your change. Run `all` (which includes the\nheavy LangFuse stack) only for a deliberate full pass — not as a reflex.\n\n## Running it\n\n```sh\n./scripts/smoke_test.sh                 # default: postgres, mongo, agui\n./scripts/smoke_test.sh mongo           # a single target\n./scripts/smoke_test.sh postgres agui   # a subset\n./scripts/smoke_test.sh langfuse        # heavy: ~5GB stack, run on its own\n./scripts/smoke_test.sh all             # everything, incl. langfuse\n```\n\n`langfuse` is excluded from the default run because it starts LangFuse's full 6-service\nself-host stack (~5GB of images). A green run ends with `--- All smoke tests passed ---`;\nexit code is non-zero on any failure or an unknown target.\n\n## Interpreting the result — and why a green ≠ \"it works\" trap is real\n\nThe whole design point is that **a passing API test does not prove the intended\ndependency was used.** The persistence test just checks that history survives across two\ninvokes, which is true for *any* working checkpointer — including SQLite. So if\n`DATABASE_TYPE=mongo` silently didn't take effect (dropped env, a fallback), the pytest\nstep would still pass while nothing touched Mongo.\n\nThat's why each target does a **second, independent check** that the specific dependency\nwas really exercised, and that's the line you should actually trust:\n\n- `✓ verified: N postgres checkpoint rows for this run's thread`\n- `✓ verified: N mongo checkpoint documents for this run's thread`\n- `✓ verified: N LangFuse traces recorded for this run`\n- `✓ verified: AG-UI streamed a complete run with the expected response`\n\nEach uses a **unique per-run thread id** (`SMOKE_THREAD_ID`), so the count reflects *this*\nrun's data even against a non-empty volume. A `✗ FAIL: … was NOT exercised as intended`\nmeans the dependency wasn't actually hit — treat that as a real failure, not flakiness.\n\nOther guards worth knowing when you read a failure:\n- **Port already in use:** `start_service` refuses to run if something is already on\n  `:8080`, so the health check can't accidentally pass against an unrelated process.\n- **Service died on startup:** it fails fast and dumps the service log instead of waiting\n  out the full timeout.\n- **LangFuse ingestion is async:** traces flow through a worker → ClickHouse pipeline, so\n  the script polls the traces API for a bit (usually a few seconds) before deciding.\n\n## Cloud-environment quirks (Claude Code on the web / sandboxes)\n\nThis script is meant to be runnable by an agent in a sandboxed cloud environment. A few\nthings bite there specifically:\n\n- **Docker daemon may not be running** (and can stop between steps). If `docker` errors\n  with \"Cannot connect to the daemon,\" start it: `(dockerd > /tmp/dockerd.log 2>&1 &)`\n  then wait a few seconds.\n- **Don't build the service image in-sandbox.** Container builds can't reach package\n  registries through the egress proxy (TLS/CA), so `docker compose build` fails. This is\n  exactly why the script runs the service on the **host** via `uv` and only puts the\n  *dependencies* in Docker. Don't \"fix\" it by baking the proxy CA into the committed\n  Dockerfile — that's sandbox-specific.\n- **Registry allowlist for LangFuse.** The LangFuse stack's `minio` image comes from\n  `cgr.dev` (Chainguard). In a restricted-egress environment add **`cgr.dev`** to the\n  network allowlist, or the pull 403s. (If a pull still 403s afterward on a *different*\n  host, add whatever host it names — some registries serve blobs from a separate CDN.)\n- **`pkill -f run_service.py` will kill your own shell.** `-f` matches full command lines,\n  and your command text contains that string, so it self-matches. To free port 8080 use\n  `fuser -k 8080/tcp` instead.\n- **Long foreground `sleep` loops can be killed** by the sandbox. When running the script\n  (or waiting on it), launch it in the background (`nohup … &`) and poll its log file\n  rather than blocking in the foreground.\n- **`docker compose exec` needs `.env`; `docker exec` doesn't.** The host-based flow\n  deliberately omits `.env`, and `docker compose exec` re-parses the full config (which\n  references `env_file: .env`) and fails without it. The script queries containers via\n  `docker exec <id>` (id from `docker compose ps -q`) to sidestep this.\n\n## Extending it\n\nThe script is built around one principle: **every target proves the real dependency was\nused, not just that the API returned something.** Keep that when adding a target.\n\nTo add a new optional-dependency target:\n1. Add a `smoke_<name>()` function following the existing shape: start the dependency\n   (Docker), `start_service` on the host with the right env, run an API-level check, then\n   a **dependency-identity check** (query the dependency directly for this run's data via\n   `assert_positive_count`, or assert on captured output like the AG-UI target).\n2. Register it in the `case` in the target loop and, if it should be part of a full pass,\n   in the `all` expansion. Decide whether it's light enough for the default set (DB-style)\n   or heavy enough to be opt-in (LangFuse-style).\n3. If it needs an add-on compose file, follow the `docker/compose.<dep>.yaml` pattern; if\n   it's a large external stack, prefer fetching upstream's compose pinned to a tag (as the\n   `langfuse` target does via `LANGFUSE_REF`) rather than vendoring it.\n\nKeep these out of the CI docker job: they live in `tests/smoke/` (not `tests/integration/`,\nwhich CI's `test-docker` job scopes to) and are marked `@pytest.mark.docker` so they're\nskipped unless `--run-docker` is passed.\n\n<!-- File: CLAUDE.md (Tokens: ~371 | Category: claude-rule) -->\n# CLAUDE.md\n\n## Comment style\n\n- Default to no comments in code you add or edit.\n- Only add a comment when the *why* is non-obvious: a hidden constraint, a subtle invariant, or a workaround for a specific bug. Never explain *what* the code does — clear naming should do that.\n- Never write multi-paragraph comment blocks or over-explain inline. Match the terseness of the surrounding code.\n- Exception: brand-new files may start with a short, genuinely useful module/file-level docstring. This does not license verbose inline comments throughout the rest of the file.\n- When editing an existing file, match its existing comment density and style rather than introducing a heavier style than what's already there.\n\n## Maintainer scaffolding vs. template content\n\nThis repo is a GitHub template. `.github/workflows/template-cleanup.yml` strips\nmaintainer-only scaffolding from downstream clones on first instantiation, driven by\nconventions rather than a hardcoded list. Put new scaffolding where the cleanup already\ncovers it:\n\n- Maintainer automation (skills, hooks, scheduled runs) goes in `.claude/` or\n  `docs/maintenance/` — whole-directory buckets, removed entirely, auto-covered.\n- A maintainer-only *workflow* must carry the owner-guard\n  `if: github.repository == 'JoshuaC215/agent-service-toolkit'` line; the cleanup\n  removes any guarded workflow, so this is auto-covered too.\n- ONLY when a maintainer-only file must live in a shared dir (`scripts/`, etc.) and\n  can't carry the guard marker do you add it to the explicit list in\n  `template-cleanup.yml`. Adding such a file without updating the cleanup is the one\n  case that leaks scaffolding into clones.\n</yakaai_skills>"}