Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,61 @@ All notable changes to this project are documented in this file. Format follows

For narrative release notes written for operators and product owners, see [RELEASE_NOTES.md](RELEASE_NOTES.md).

## [1.22.0] - 2026-09-14

The agent can stop guessing. **Clarifying questions** ship end to end: when a request is genuinely ambiguous the agent pauses the turn, the SPA renders a multiple-choice picker in the transcript, and the answer resumes that same tool call — surviving a page refresh. The tool worked from PR-2 but the model reached for it 4 times in 24 ambiguous requests; a measured system-prompt clause takes that to 24/24 while leaving clear requests at 0/18. On the admin side, the **cost drill-down** closes the gap between "top users by cost" and the per-session anatomy: an admin walks user → conversations → session profile with 15 diagnosis rules, a context trajectory chart and a copyable diagnostic JSON — all **content-free by construction**, enforced by a denylist test and a moto test that seeds content and proves none returns. Two silent data bugs are fixed: deleting a knowledge-base document mid-upload **permanently leaked its byte reservation**, and born-managed provisioning **mistook an established legacy agent for a new one** and stranded its corpus. And an `@`-mention now **binds the conversation** instead of borrowing one turn — measured on prod, 247 of 247 mentions started the conversation, so the borrow was paying an invisible tool-loss failure for a case that has never occurred. **No CDK deploy required.** One operator step: enable the Clarifying Questions tool in each existing environment's catalog — the seed skips a tool row that already exists.

### 🚀 Added

- **Clarifying questions (`ask_user_question`)** — the agent pauses a turn to ask structured multiple-choice questions. The interrupt is raised by the tool itself via `ToolContext` rather than a `BeforeToolCall` hook, so Strands' `_stop_for_interrupts` carries the `PausedTurnSnapshot`, the resume route and the `PendingInterrupt` breadcrumb with no special case. New `user_question_required` SSE event; gated by `ASK_USER_QUESTION_ENABLED` (default on with a kill switch) (#1100)
- **Clarifying-questions picker in the chat transcript** — renders the questions inline, always offers Other + Skip (so the model must not supply them; model-supplied duplicates are stripped server-side), and resumes the same turn by POSTing an `interrupt_responses` entry whose `response` is always an object — a null would re-raise the interrupt forever (#1102, #1105)
- **The picker survives a refresh** — pending prompts rehydrate from the `user_question` `PendingInterrupt` breadcrumb on `GET /messages`, so a reload lands back on the question instead of a dead turn (#1103)
- **System-prompt guidance that makes the tool actually fire** — appended only when `ask_user_question` is in the turn's **post-filter** effective tool list (the request's `enabled_tools` and the registered set diverge), and applied to the prompt handed to the agent, never to `self.system_prompt`, which is snapshotted for resume and hashed into the agent cache key. ~63 tokens, constant per configuration. Catalog seed flips to `enabledByDefault: True` (#1106)
- **Admin cost drill-down** — `GET /admin/costs/users/{id}/sessions` and `GET /admin/costs/sessions/{id}/profile` (scope `admin.costs`), plus `apis/shared/observability/content_policy.py`: a denylist of every content-bearing attribute on the session/cost/upload row families with three aliased allowlist projections. Unrecorded cost renders `costKnown=false`, never `$0`. No new table, no GSI operation, no feature flag (#1093)
- **15 cost diagnosis rules** — `admin/costs/diagnoses.py` encodes the classifications prior quota investigations reached by hand (prefix spiral, partial-miss heavy, over-threshold, summary over budget, prompt/`toolConfig` mutation, agent-cache bypass, attachment-heavy…), each with numeric evidence and the fix (#1093)
- **Conversations section on `/admin/users/:userId`** — period/sort controls, per-row model, tools on, context bar against the window, cost with share of the user's month, cache waste, and a severity dot for the diagnoses that fired; row links to the anatomy. Top-users table now shows email / tier / quota %, replacing a hard-coded `None` (#1094)
- **Session profile band + context trajectory chart** on `/admin/costs/sessions/:id` — messages, model calls and mix, tool calls, attachments, compactions, peak context of window, write:read ratio (each "not tracked" wherever a counter predates the session), an expandable Diagnoses list, and "Copy diagnostic JSON" meant to be handed to a model for a second opinion (#1094)
- **Content-free tool census and compaction counter** — `ToolCensusHook` tallies tool name → `{calls, errors}` per model call and the stream coordinator attaches each tally to that call's `C#` cost row as `toolCalls`; `toolCallCount`/`toolErrorCount` ride the existing session-aggregate `UpdateItem` and a monotonic `compactionCount` rides the compaction-state update (the persisted `compaction` map is last-write-wins and cannot count occurrences). Additive attributes only — no table, no index, no backfill. Gated by `COST_DIAGNOSTICS_ENABLED` (default on with a kill switch) (#1095)
- **`GET /files/{uploadId}/download`** — cookie-authed, owner-scoped, 302 to a freshly minted presigned URL with `Cache-Control: no-store`. A link to it keeps working for as long as the file does (#1101)
- **Knowledge-base storage usage bar** — new `KbUsage` (`engine`, `storedBytes`, `reservedBytes`, `cap`, `elevated`) on `DocumentsListResponse`. Managed KBs show "X of Y used" against the binding's effective cap (min of owner tier and per-KB ceiling), green/yellow/red at <75 / 75–90 / ≥90%. Best-effort: a record-read failure never breaks the documents list (#1108)

### ⚠️ Changed

- **An `@`-mention now binds the conversation instead of running one turn.** Two outcomes, no third: mentioning into an **empty** thread binds the Agent to it, exactly like launching from its card; mentioning into a thread that **has messages** opens a **new** conversation with that Agent, and the SPA says so. This reverses decision D11 on new evidence — of **247 prod mentions, 247 started the conversation** (dev: 60 of 61), so the borrow bought nothing and cost an invisible failure. The SPA now carries the binding in the `assistantId` query param and stops sending `agent_mention`; the backend still honours that flag for older clients, and `binds_conversation` gains `thread_is_empty` so a stale tab lands where a current one does. Retires the ~$0.12-per-mention prefix re-write and the history fork (#1115)
- **Send button is an up arrow**, not a paper airplane (#1114)

### 🐛 Fixed

- **A mentioned Agent silently lost its tools after the first turn.** The thread still looked like the Agent's while its tools, skills and model were gone, and nothing surfaced it — not the UI, and not the model, which cannot know its own toolset shrank. Asked for a tool it had used a moment earlier it returned `Unknown tool: create_rubric` and told the user to toggle a setting that was already correct (#1115)
- **"Continue" after a `max_tokens` truncation dropped the Agent entirely.** The SPA was already resending `rag_assistant_id` — only a `not is_continuation` guard discarded it — so a properly launched Agent finished its reply with none of its tools, skills, model or instructions. The block now runs for a continuation, with binding validation, persistence and RAG skipped (#1115)
- **Generated-document download links were dead on arrival and dead on reload.** The tool result handed the model a ~1,400-character presigned S3 URL and the model re-emitted it in prose truncated at the `?` — signature gone, `AccessDenied` (observed twice in one prod session). The card's own button was on a clock too: the signature expired an hour after the message was written. The office tools and `workspace_write` now put `upload_id` in the card payload and tell the model the card is already on screen, dropping the tool result from ~1,500 to ~330 characters **in the cacheable prefix, for the life of the session**. The SPA resolves through the new download route, recovers the upload id from a legacy `download_url`'s S3 key so persisted cards heal on render, and the global `marked` link renderer rewrites raw user-files S3 hrefs — fixing links already sitting in shipped conversations (#1101)
- **Deleting a document mid-upload leaked its byte reservation, permanently.** The request-time reservation is released on every abandon path except deletion, so each cancelled upload shaved bytes off that assistant's allowance forever — surfacing months later as "uploads stopped working", with no failure anywhere near the deletes that caused it. `soft_delete_document` now releases through `release_reservation_if_managed`, whose `settle_once` stamp makes it exactly-once against the other three paths (#1059)
- **The same delete popped five "Not found" dialogs.** The poll tolerates five consecutive 404s and the component handled `DOCUMENT_NOT_FOUND` cleanly, but the global `errorInterceptor` pops a dialog for every failed request *before* any caller's catch runs. The poll's reads now set `SUPPRESS_ERROR_TOAST`, and the loop is finally stoppable — `deleteDocument` already dropped the id from `pollingDocuments`, but that signal was display-only and the running loop never read it (#1059)
- **Born-managed provisioned over an established legacy agent.** Legacy KBs share one S3-Vectors index and never write a `KB_Record`, so an established legacy agent looked identical to a new one — its *next* upload was mistaken for a first upload, flipping retrieval to an empty managed KB and stranding the existing corpus. The record-is-`None` branch is now guarded on a cheap existing-documents probe (`Limit=1` COUNT), and fails toward legacy on any probe error (#1109)
- **The storage usage bar showed for legacy (Classic) KBs**, which are uncapped and have no denominator to show (#1110)
- **19 of 51 chat greetings wrapped to a second line** in the 616px text column, and because the greeting types out a character at a time the wrap happened in full view and pushed the composer down mid-animation. Twenty offenders rewritten shorter; a new `greeting-line-length.spec.ts` sums per-character advance widths captured from the real InterVariable woff2 (jsdom has no font metrics), tracking browser layout to within ±7px across 455 name/greeting combinations. Two of the offenders were `DEFAULT_GREETING_TEMPLATES` entries a golden spec pinned verbatim — the pin was preserving the bug (#1116)
- **Cost diagnostics crashed the scheduled-runs image** — `feature_flags` was not shipped in `Dockerfile.scheduled-runs` (#1095)

### 🔒 Security

- **Remaining log-injection sinks sanitized** — user-controlled values reaching `logger` calls in admin role pins, model icons, fine-tuning, sessions, skills (routes, service, user service), tool discovery and the inference-api chat routes now pass through `scrub_log()` (#1098)
- **Nightly workflow ref allowlist guarded by a test** — `tests/supply_chain/test_nightly_ref_allowlist.py` pins which refs the nightly build may check out (#1098)

### 📦 Dependencies

- Backend (dev): `pytest-xdist` 3.6.1 added — the suite is xdist-safe (moto mocks and hypothesis are per-worker; no test mutates shared on-disk state)

### 🔧 CI/CD

- **Backend pytest runs in parallel** — `pytest -n auto` on the PR gate fans ~3k tests across all runner cores instead of running single-threaded. `-v` dropped from `pytest.ini` (thousands of `PASSED` lines with no diagnostic value); the nightly coverage run stays serial on purpose (#1111)
- **Infra jest is transpile-only** — `isolatedModules` stops each jest worker re-type-checking the whole project, which was the dominant cost of the infra suite once backend went parallel. Type safety is preserved by a single `tsc --noEmit` step added to the infra CI job, where previously only ts-jest enforced it on PRs. Workers stay at 2 — the `--maxWorkers` bump regressed 2.5× in #1112 (#1113)

### 📚 Docs

- **Authenticated web assessment via browser takeover** — a 468-line spec for the browser-takeover surface (#1107)
- **Managed-KB specs closed out**, with `MANAGED_KB_NEW_DEFAULT` armed in dev recorded (#1104)
- **Backfill instructions corrected across all six `backfill_*.py` scripts** — give the backend venv's interpreter (a bare `python` fails on `boto3` before doing anything), and stop naming `describe-table` `ItemCount` as the verification: DynamoDB refreshes those counts roughly every six hours, so a correct backfill reads as a failure (#1098)

## [1.21.0] - 2026-09-13

Global preferences get a home. **Customize** (`/customize/{tools,skills,connectors}`) replaces the composer's settings drawer, which had been presenting durable, account-wide state as "settings for this conversation" — a user who enabled a tool to get through one question had changed the `toolConfig` of every future turn, and nothing said so. Tools and skills gain full detail pages, an MCP server's sub-tools can be switched one at a time, and `/skill-name` in the composer invokes a skill for a single message the way `@agent` already did. Chat itself stops guessing: a four-tool answer now renders as **one** card instead of five, the loading indicator states what the agent is actually doing (`Running browse_web · 4s`) from a new `agent_status` event, and each finished tool batch gets a model-written summary line off a Nova Micro side-channel that never touches the cacheable prefix. On the cost side, the tool catalog moves off a full-table Scan onto a new `EntityTypeIndex` (95 items read to return 24, before), four tenant-global catalogs gain a TTL + single-flight cache, and the per-request user-profile upsert is throttled — together roughly 24 DynamoDB writes and four uncached scans removed from every SPA first load. **Requires a CDK deploy, and a backfill must be run — see the deployment notes.** ⚠️ **This release removes the only way to select a Conversation Mode**; prod uses one (Guided Learning, ~60 sessions in the first 12 days of September).
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
**An open-source, production-ready Generative AI platform for institutions**
*Built by Boise State University, designed for everyone.*

[![Release](https://img.shields.io/badge/Release-v1.21.0-6366f1?style=flat&logo=github&logoColor=white)](RELEASE_NOTES.md)
[![Release](https://img.shields.io/badge/Release-v1.22.0-6366f1?style=flat&logo=github&logoColor=white)](RELEASE_NOTES.md)
[![Nightly](https://github.com/Boise-State-Development/agentcore-public-stack/actions/workflows/nightly.yml/badge.svg)](https://github.com/Boise-State-Development/agentcore-public-stack/actions/workflows/nightly.yml)

![Python](https://img.shields.io/badge/Python-3.13+-3776AB?style=flat&logo=python&logoColor=white)
Expand Down Expand Up @@ -296,7 +296,7 @@ agentcore-public-stack/

See [RELEASE_NOTES.md](RELEASE_NOTES.md) for the full changelog, including new features, bug fixes, platform upgrades, and deployment notes for each release.

**Current release:** v1.21.0
**Current release:** v1.22.0

---

Expand Down
Loading