Commit Graph

14820 Commits

Author SHA1 Message Date
Ran Shem Tov 8f74bcb738 feat(showcase): add CrewAI state and multimodal flows 2026-08-05 18:22:36 +03:00
Markus Ecker da3dba5704 fix(channels-slack): clear the status when a tool call follows the first reply
An agent that streams text, calls a tool, then streams more text leaves Slack's
native "is thinking…" indicator running indefinitely after the answer has
already been posted.

`postedReply` latches on the first posted reply and makes `clearStatus` a
one-shot. But `onToolCallStartEvent` and `onToolCallEndEvent` both write the
status again *after* that latch closes, and from then on nothing clears it:
`onFirstReply` early-returns, and the backstops in `finalizeTurnStream` and
`finish` are skipped precisely because a reply was posted. Slack eventually
expires the stale status, which is why it presents as a slow hang rather than
a bug.

The latch really tracks "the status is already cleared for what is on screen",
not "a reply has been posted", so `setStatus` now resets it whenever a
non-empty status is written. Both existing backstops then fire exactly when
they should, and the normal streamed-text path still skips the redundant clear.

Reproduces on any agent that narrates before a tool call — emitted stream is
text → TOOL_CALL_* → text — and is independent of `showToolStatus`: with it off
both tool events set the generic status, with it on START sets
"is using `tool`…". Either way the write lands after the latch.

Covered by a regression test driving that exact sequence, verified failing
without the change ("expected 'is thinking…' to be ''").
2026-08-05 17:22:01 +02:00
Ran Shem Tov 02c0daf28a fix(showcase): settle CrewAI interrupt resumes 2026-08-05 18:16:51 +03:00
Ran Shem Tov 1265509f37 feat(showcase): add native CrewAI Flow interrupts 2026-08-05 17:51:43 +03:00
Ran Shem Tov 36f05f17b9 fix(showcase): render Mastra MCP Apps diagrams reliably on live
The MCP Apps cell intermittently rendered an empty iframe (an empty box or a
thin band) on a live endpoint while passing under aimock.

Root cause is the model, not the renderer. Excalidraw's `create_view` declares
`elements` as `type: "string"` holding a JSON array, so the model must emit
double-encoded, hand-escaped JSON. gpt-4o-mini frequently appends a stray `}`
just past the closing `]`. The MCP server then rejects the call with
"Invalid JSON in elements: Unexpected non-whitespace character after JSON",
returns isError, and there is no diagram to draw, so the iframe paints empty.
aimock never catches this because it replays a recorded, valid payload.

Measured against the real Excalidraw MCP server using this agent's exact
system prompt and the real tool schema:

  gpt-4o-mini   create_view OK 3, isError 5
  gpt-4.1       create_view OK 8, isError 0

And on JSON validity alone (n=12 unless noted):

  gpt-4o-mini                 8 invalid
  gpt-4o-mini + hardened prompt 7 invalid  (prompting does not fix it)
  gpt-4.1-mini                3 invalid, and it bloats output
  gpt-4.1                     1 invalid of 24, output stays compact

gpt-4.1 is already used by five other agents in this file, so this keeps the
integration consistent. The cell's aimock fixture does not key on the model, so
d6 replay is unaffected.
2026-08-05 17:43:45 +03:00
Ran Shem Tov 33fca30cad fix(showcase/mastra): return gold-shaped search_flights result so flight rows render
On a live endpoint the tool-rendering flight card rendered every row blank
("United ? -> ? --") while the model's narration below it carried the real
times and prices. The result was delivered in full; the card just never
matched it.

search_flights emitted Mastra-flavored keys (flightNumber / departureTime /
arrivalTime / price) but FlightListCard - in both this integration and gold
langgraph-python, byte-identical - reads { airline, flight, depart, arrive,
price_usd }, which is exactly what gold's tool_rendering_agent.py returns.
Only `airline` overlapped, so the rest fell back to placeholders.

Return the gold result shape directly. The legacy caller-supplied `flights`
passthrough is untouched, and the only consumers of this tool are the three
tool-rendering-style agents, all of which drive gold-shaped cards.

Verified on a live real-LLM endpoint (no aimock): reproduced the blank rows
before the change, then confirmed all three rows render
"United UA231 08:15 -> 16:45 $348" (plus Delta and JetBlue) after it, across
tool-rendering, tool-rendering-custom-catchall and
tool-rendering-reasoning-chain, and across a two-turn weather + flights
conversation.

The existing e2e only asserted origin/destination and a row count, so blank
rows passed. It now asserts the result's depart/arrive/price and rejects the
"? -> ?" placeholder.

Fixes PNI-121
2026-08-05 17:35:36 +03:00
Ran Shem Tov 7e85d964d5 feat(showcase): use native CrewAI reasoning and inputs 2026-08-05 17:35:35 +03:00
github-actions[bot] 8df39dd9bb style: auto-fix formatting 2026-08-05 14:33:00 +00:00
Ran Shem Tov d9bf3efcf1 fix(mastra-showcase): render beautiful-chat flights as fixed-schema A2UI (langgraph-python parity)
The "Search Flights (A2UI Fixed Schema)" pill narrated flight results as plain
text instead of rendering FlightCards. Root cause: mastra's beautiful-chat
reused the shared `searchFlightsTool` (returns plain `{ flights }`, which the
tool-rendering cells render via their own frontend FlightListCard), so nothing
produced an A2UI surface and the model just described the data.

langgraph-python's beautiful_chat.py wires a DEDICATED fixed-schema
`search_flights` whose tool RESULT is a complete `a2ui_operations` envelope (a
flat Row of literal FlightCards on `app-dashboard-catalog`, surface
`flight-search-results`). Mirror it:

- Add `searchFlightsA2uiTool` returning that envelope (buildFlightCardComponents
  mirrors `_build_flight_components` — inline values, no structural-template
  children).
- Add a dedicated `beautifulChatAgent` (query_data, todos, generate_a2ui, the
  fixed search_flights, + the flight/dashboard steering prompt) and point the
  beautiful-chat route at it, so the fixed-schema flights and steering don't
  leak into the shared weatherAgent / tool-rendering cells.

Verified live (real LLM, dedicated agent): the flights pill paints two
FlightCard surfaces (catalogId app-dashboard-catalog, surface
flight-search-results), and the Sales Dashboard dynamic surface still renders in
full (metrics + pie + bar) via the grounded generate_a2ui.
2026-08-05 17:30:55 +03:00
Ran Shem Tov 33aa7fed82 chore(showcase): validate CrewAI alpha stack 2026-08-05 17:26:38 +03:00
Ran Shemtov b30527ca8a fix(showcase-mastra): render interactive open-gen-ui on a live endpoint (#6389)
## Problem (PNI-119)

`open-gen-ui-advanced` (and its minimal sibling) did **not** render
working generative UI on a **live** endpoint, though both passed under
aimock.

Both cells reused the shared `weatherAgent` (`instructions: "You are a
helpful assistant."`, gpt-4o). On a live LLM that emitted **static HTML
with no JS wiring** — the calculator's buttons rendered but did nothing
(staging repro: `7 + 8 =` left the display on `0`; captured SSE
`generateSandboxedUi` args contained only
`initialHeight/placeholderMessages/css/html`, no
`<script>`/`jsFunctions`/`jsExpressions`).

aimock hid the divergence: its fixtures match on
`userMessage`/`hasToolResult` and replay a fully-wired UI regardless of
the agent.

## Root cause

Two factors, one dominant:

1. **No dedicated agent.** Gold `langgraph-python` uses purpose-built
`open_gen_ui_agent.py` / `open_gen_ui_advanced_agent.py` whose system
prompt mandates a single interactive `generateSandboxedUi` call (iframe
restrictions, wire host functions via `Websandbox.connection.remote.*`,
visible result element). Mastra reused a generic agent.
2. **Context is a read-channel on Mastra.** The provider delivers the
design-skill + sandbox-function descriptors via `copilotkit.addContext`
→ `RunAgentInput.context`. The `@ag-ui/mastra` bridge maps that to
`requestContext.get("ag-ui").context` (a tool read-channel) and does
**not** inject it into the LLM prompt (unlike langgraph's
`CopilotKitMiddleware`). So even the tool description alone was not
enough for gpt-4o.

## Fix (surgical, showcase-only)

- `src/mastra/agents/index.ts` — add dedicated `openGenUiAgent` /
`openGenUiAdvancedAgent` porting gold's system prompts, with **dynamic
instructions** that read `requestContext.get("ag-ui").context` and fold
the design-skill + sandbox descriptors into the prompt (surfacing the
read-channel the Mastra-idiomatic way, matching what gold's agent sees).
- `src/mastra/index.ts` — register both.
- `src/app/api/copilotkit-ogui/route.ts` — point `getLocalAgent` at the
new agents.

No published-package or bridge change.

## Verification (live)

Local `next dev` against **real OpenAI** (aimock bypassed). All three
advanced pills render interactive, host-wired UI; host round-trips
confirmed in console:

- **Calculator** → `evaluateExpression 7*8 = 56`
- **Ping the host** → `notifyHost: Ping!` (+ "Host notified
successfully." in-iframe)
- **Inline expression evaluator** → `evaluateExpression 5 + 3 * 2 = 11`

**aimock unaffected** — matcher keys
(`userMessage`/`hasToolResult`/`context`) don't touch the prompt or
agent, so D6 replay still passes. Changed files typecheck clean.

## Follow-up (out of scope)

Live surfaced a benign `@copilotkit/runtime` OGUI-middleware warning: an
empty/null `jsFunctions` yields a JSON-patch `add /jsFunctions` with no
`value` (`OPERATION_VALUE_REQUIRED`, patch dropped). Tracked separately
(needs a runtime release).
2026-08-05 16:00:28 +02:00
Guido Vizoso 9c3c0c0148 feat(reskinnable-demo): improve general layout and UI (#6394)
## What does this PR do?

Reworks the reskinnable demo's layout from four flush, edge-to-edge
columns into a
shadcn-inset-style frame: the assistant column and the skin's app each
float as a
rounded card, separated by a resizable gutter.

**Layout**
- Two panels with simple bounds — assistant `min 250px / default 600px /
max 50%`,
app takes the remainder. Capping the assistant as a *share* means the
app needs no
  floor of its own and the mobile breakpoint stays a genuine 768px.
- Drag to resize, swap which side the assistant docks on, or hide it
entirely. Side
and open state persist, shell-global, so switching skins never
rearranges the
  workspace.
- Card radius is a fixed 12px in px, deliberately *not* `--radius` — the
frame is
shell chrome and must read identically in every skin. Card colours stay
themed, so
  a reskin still restyles it.

**Chat**
- `CopilotSidebar` → inline `CopilotChat`, which is what lets the
cluster live in a
  panel. That deletes a whole geometry contract (`--nw-chat-width`,
`--nw-rail-offset`, `data-nw-chat-open`) plus ~110 lines of CSS that
existed only
  to keep a floating selector off a fixed panel.
- The thread rail is a fixed-width element, not a nested panel — it sits
beside the
conversation and hides via a container query when the card is dragged
narrow.

**Skin selector**
- Now a dropdown, so its footprint stays flat as skins are added (the
pill row took
three rows at four skins). Shell controls — switcher, swap sides, hide —
are
  grouped in the card; the chat header keeps only conversation actions.

**Type**
- Base scale set to 15px at the root, so it reaches all four skins with
no per-skin
  edits.

## Notes for reviewers

- `react-resizable-panels` is pinned to **4.x**, whose API is renamed
from the 2.x/3.x
used by six sibling examples
(`Group`/`orientation`/`Separator`/`useDefaultLayout`).
  Only 4.x accepts pixel size constraints. See the header comment in
  `src/components/ui/resizable.tsx` before touching it.
- The chat CSS re-key landed as a deliberate bracket — duplicate onto
`.nw-chat`,
make the structural change, delete the originals — because its failure
mode is
silent: a lint-clean, test-clean build with subtly wrong chat
typography. A guard
  test asserts first parity, then absence.
- Adds `e2e/inset-layout.spec.ts` (9 tests). Weighted toward what
actually broke
during development — every bug here was a sizing behaviour invisible to
jsdom.
- The reskin skill taught two now-retired patterns (publishing
`--nw-nav-inset-*`,
rooting layouts at `h-screen`); both are corrected, so authoring a new
skin no
  longer produces dead code and a broken root height.

**Known limitation:** the sub-768px overlay branch is not verified.
Mobile was
already broken before this work and is explicitly out of scope; the e2e
suite runs at
1280px.

## Checklist

- [x] I have read the Contribution Guide
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked
2026-08-05 10:55:12 -03:00
Ran Shem Tov 4700acc4a5 docs(showcase): plan CrewAI full D6 parity 2026-08-05 16:48:25 +03:00
Ran Shem Tov 5d3671b245 fix(runtime): skip value-less activity patch for null open-gen-ui params
When the LLM emits jsFunctions/css as null (frequent on open-gen-ui-advanced),
setParam coerces the value to undefined and emitParamDelta produced a JSON
Patch `{op:"add", path}` with no `value` property. fast-json-patch rejects
that client-side with OPERATION_VALUE_REQUIRED and drops the whole activity
patch (console warning, benign but noisy and fragile).

Guard emitParamDelta to skip emitting when the value is undefined. Empty
arrays and completion markers still emit. Adds a regression test feeding a
null jsFunctions and asserting no value-less patch is produced.
2026-08-05 16:46:34 +03:00
Guido Vizoso 1c53a8c60b Merge branch 'main' into feat/reskinnable-demo-ui 2026-08-05 09:34:49 -03:00
Guido Vizoso 7aab1bead2 docs(reskinnable-demo): document the inset frame and retire the old chrome
The reskin skill was the urgent part: it is the sanctioned path for adding a skin,
and it still taught two patterns the frame retired. It told authors to publish
--nw-nav-inset-left/right from a useEffect so the floating selector could dodge their
nav -- both the variables and that selector are gone -- and to root the layout at
h-screen overflow-hidden, which overflows the app card by the frame's padding.
Following it produced a skin with dead code and a broken root height. The nav-inset
step is removed with a short "Retired" note so it is not reintroduced by copying an
older skin, and the root guidance now explains that the CARD is the bound.

Adds a "The inset frame" section to CLAUDE.md covering what a reader needs before
touching src/shell/layout: the two-panel model and its bounds, why the thread rail is
a fixed element rather than a nested panel, the v4 API rename and the
id-becomes-data-testid behaviour, where the shell controls live, the fixed 12px card
radius, and the h-full requirement for skin layouts.

Also corrects the composition chain, the shell's ownership list, and the
four-skins-not-two count across CLAUDE.md, README.md and DESIGN.md, plus the skill's
verification step, which told authors to look for their skin in a bottom-left
floating pill.
2026-08-05 09:29:46 -03:00
Guido Vizoso c70db12a43 test(reskinnable-demo): cover the inset layout end to end
Nine new tests over what only a real browser can see: panel order per docked side,
resize bounds in both directions, persistence across a reload and across a skin
switch, the rail collapse/reopen cycle, the hide/launcher cycle, that the selector
renders options only while open, that every skin's chrome fits its card without
scrolling the document, and that the chat header shows the active skin's assistant
name.

Weighted toward what actually broke. Every bug this layout shipped was a sizing or
resize behaviour invisible to jsdom, which has no layout engine: a rail collapsed to
zero width, a rail that could not be reopened, an assistant column that could be
widened but never narrowed, and a header showing the framework default. All four
passed a green unit suite. Each assertion was mutation-checked rather than assumed --
widening the assistant cap to 95% fails only the resize test, hardcoding the header
title fails only the header test.

Navigation goes through a gotoSkin helper that waits for the frame; the shell is
client-rendered, so reading geometry straight after goto returns an empty panel set.

Updates smoke.spec.ts for the dropdown: switching opens the menu first, and the
"no other skin's chrome leaked" guards get STRICTER -- from exactly-one selector pill
to zero occurrences, since a closed menu renders no other brand.

Retires the CopilotSidebar launcher from the fixme'd specs. a2ui-canvas clicked
copilot-chat-toggle, which no longer exists -- inert only because the spec never
runs, so it would have failed the moment anyone re-enabled it. memory-learning keeps
its guarded open-chat block deliberately: the count check makes it a no-op today, and
that spec needs the docker memory stack so it cannot be run here to verify a
behavioural edit.
2026-08-05 09:29:33 -03:00
Guido Vizoso 0c8d8db275 feat(reskinnable-demo): mount the frame, free the skins, retire the floating selector
Composes ShellFrame in the per-skin layout, passing the skin's Layout as its app
slot and the shared ChatPanel as its chat slot, replacing the sibling
Layout + ChatPanel + FloatingSelector trio.

Each skin's chrome drops its viewport height for h-full: it now fills the app card,
which the frame has already inset by its own padding, so a viewport-height root
overflowed the card by exactly that much. Logistics keeps overflow-hidden -- its nav
stays pinned only while the container is bounded, which is now the card. Banking
keeps its second, unrelated canvas-clearing effect and therefore its useEffect
import.

The four --nw-nav-inset-* publishing effects go with the floating selector they fed.
Each skin published the width of its own edge-nav so a floating pill could compute a
safe band and dodge it; the selector now occupies a slot in the assistant column and
overlaps nothing, so the whole mechanism is unnecessary and its component is deleted.

This is one commit because the halves are not independently correct: without the skin
edits the cards overflow, and deleting the selector before its import is removed
would not build.
2026-08-05 09:29:18 -03:00
Guido Vizoso 58efc192e4 feat(reskinnable-demo): render the chat inline inside a resizable card
Replaces CopilotSidebar with an inline CopilotChat so the cluster can live in a
panel. CopilotSidebar was a fixed <aside> that pushed document.body's margin and
faked two columns by being handed the width of both, insetting its own contents past
the rail while the rail painted into the freed strip as a separately-fixed sibling.

That deletes an entire geometry contract: --nw-chat-width, --nw-rail-offset and
data-nw-chat-open existed only to describe a fixed panel's footprint to the rest of
the page. Also gone: the force-open-on-mount ref dance, the header slot cast
(CopilotChat has no header slot -- that is a modal concern, so the header is now an
ordinary sibling), and the rail's own fixed positioning and translate animation.

The thread rail is a FIXED-WIDTH element, not a panel. v4's collapse API fought that
three ways: a collapsed 0 written to storage was restored forever, expand() restores
the "most recent size" which after collapsing to 0 is 0, and resize() is ignored
while collapsed. Conditionally rendering it makes isInboxOpen the single source of
truth with no imperative sync.

The header reads the SKIN for its title, not the chat configuration. As
CopilotSidebar's header slot it rendered inside the chat's own provider so our labels
reached it; as a sibling it reads the wrapper's, whose default "CopilotKit Chat" is
non-null and won the ?? chain -- every skin's header showed that instead of its
assistant name. The header is skin chrome now, and holds only conversation actions.
2026-08-05 09:29:05 -03:00
Guido Vizoso 06f973823e feat(reskinnable-demo): add the inset shell frame
src/shell/layout/ owns the app's outer geometry: a padded region holding the
assistant column (selector card above chat card) and the skin's app card, separated
by a resizable gutter.

The model is deliberately just "one bounded panel, one that takes the remainder" --
assistant min 250px / default 600px / max 50%, app gets what is left. An earlier
version nested the thread rail as a resizable panel INSIDE the assistant column,
which made its floor a compound of rail + conversation and forced a derived
breakpoint, a switching collapsed floor and an app floor to compensate. Capping the
assistant as a SHARE rather than a pixel count is what removes the need for an app
floor and lets the mobile breakpoint stay a genuine 768px instead of being derived
from panel arithmetic.

- selector-card: the skin switcher as a dropdown, so its footprint stays flat as
  skins are added, plus the shell controls -- swap sides and hide. All three are
  shell concerns, which is why they are here and not in the chat header.
  useSkinThemeReconcile stays on the card root: it reads the computed
  --nw-dark-capable from inside the skin's theme root, and losing it would let a
  light-only skin render dark chat chrome.
- layout-preferences: side and open state, shell-global so switching skins never
  rearranges the workspace. Read through useSyncExternalStore because this repo
  treats react-hooks/set-state-in-effect as an error, with write-through
  persistence so only a deliberate choice is stored. The hook returns inert
  defaults outside its provider rather than throwing.
- use-is-desktop: matchMedia through the same external-store pattern.
- panel-sizes: the three numbers, in one place.
2026-08-05 09:28:49 -03:00
Guido Vizoso a36ab28019 refactor(reskinnable-demo): re-scope the chat CSS and set the base type scale
Three changes to the shell stylesheet.

Re-scopes chat typography from the SDK's [data-copilot-sidebar] attribute to a
.nw-chat wrapper. That attribute came from CopilotSidebar, which the inset frame
replaces with an inline chat, so ~23 rule blocks styling assistant markdown would
have silently stopped matching -- a lint-clean, test-clean build with subtly wrong
chat text. The migration ran as a bracket: duplicate onto .nw-chat, land the
structural change, then delete the originals, with a guard test asserting first the
parity and then the absence. Typography is provably unchanged -- a probe mounting
markdown-shaped content with the library's prose class reported 140 identical
computed values across 14 selectors before and after.

Adds the frame's own rules: .nw-panel-card (a FIXED 12px radius, deliberately not
reading --radius, because the frame is shell chrome and must read identically in
every skin) and the chat cluster's fixed-width rail with the container query that
hides it when the card is dragged narrow.

Sets the root font size to 15px. The root, not body: every rem in the shell and all
four skins resolves against it, so this is the one lever that rescales type
everywhere without per-skin edits. Spacing scales with it by design; anything that
must not move is written in px. The chat prose moves 0.9375rem -> 1rem, since that
value existed to hit 15px under a 16px root and would otherwise sit below the new
default.

Deletes the ~120-line floating-selector dock and the unused .brand-text-gradient.
2026-08-05 09:28:35 -03:00
Guido Vizoso 6c7e7b7df4 feat(reskinnable-demo): add the react-resizable-panels v4 wrapper
Pins ^4.12.2 and wraps its Group/Panel/Separator API behind two styled handles: an
8px gutter for the gap between cards and a 1px hairline. 4.x rather than the 2.x/3.x
used by six sibling examples because only 4.x accepts PIXEL size constraints, and
the layout's bounds are pixels.

Three v4 behaviours the consumers depend on, each with a test:
- The API is RENAMED from 2.x/3.x: PanelGroup->Group, direction->orientation,
  PanelResizeHandle->Separator, autoSaveId->useDefaultLayout. Most material online,
  including shadcn's Resizable block, targets the old names and will not compile.
- data-testid is DERIVED from a Panel's id and overwrites any passed in, so a
  panel's id is its query handle. Getting this wrong makes every
  [data-testid$='-panel'] query silently match nothing.
- Separator refuses flex-grow/flex-shrink overrides, so shrink-0 would be dead.

Adds a jsdom ResizeObserver stub, without which every test rendering a Group throws.

Note for anyone adding a dependency here: declare it in package.json and run
'pnpm install --lockfile-only' from the REPO ROOT. This example pins pnpm 10.10.0
while the root pins 10.33.4, so 'pnpm add' from this directory makes corepack switch
versions and the older pnpm re-resolves the entire workspace lockfile.
2026-08-05 09:28:05 -03:00
Ran Shem Tov 1aaca6142b docs(showcase): define CrewAI full D6 design 2026-08-05 15:09:49 +03:00
Ran Shem Tov ced993447f fix(showcase): copy patches/ into harness build context
Root package.json pins patchedDependencies (eventsource@3.0.7 ->
patches/eventsource@3.0.7.patch, added in #6334). The harness Dockerfile
selectively copies root manifests but omitted patches/, so the frozen
install hashes the patch file, ENOENTs, and exits 254. Broke the
showcase-harness build-check for any PR rebased onto post-#6334 main.
Copy patches/ before the install. Verified: image builds clean locally.
2026-08-05 14:29:58 +03:00
Ran Shem Tov aa15946c2a fix(mastra-showcase): ground beautiful-chat A2UI dynamic render from request context
The dynamic `generate_a2ui` tool grounded its inner `render_a2ui` subagent from
the tool's `contextEntries` arg, which the outer model always sends empty
(captured live: `{"messages":[…],"contextEntries":[]}`). On a live LLM the inner
render then ran with an EMPTY system prompt: ungrounded, it emitted
invalid/misnamed components (or none), so the surface never resolved against the
catalog. Result: the Beautiful Chat A2UI dynamic surface (Sales Dashboard,
flights) rendered no UI, with a render error that varied run to run. aimock hid
it: the recorded fixture returns a valid envelope regardless of the empty
context.

Read the catalog schema + A2UI generation guidelines the `@ag-ui/mastra` bridge
already forwards onto Mastra's request context
(`requestContext.get("ag-ui").context`) and ground the render there instead of
trusting the model-supplied arg. Mirrors `readAgUiContext` in `@ag-ui/mastra`'s
`getA2UITools`. Preserves per-demo catalogId (the grounded model emits it, and
`buildA2uiOperationsFromToolCall` uses `args.catalogId`), so the same shared
weatherAgent serves both beautiful-chat and declarative-gen-ui. Falls back to
the arg when no request context is present.
2026-08-05 14:20:44 +03:00
Ran Shem Tov 528ebfa351 fix(showcase): un-quarantine MAF Python multimodal (parity with langgraph)
multimodal was wrongly quarantined based on a local-only failure: the
sample.png/sample.pdf demo assets are Git LFS pointers, and without
git-lfs on PATH the attachment send fails before the run starts
(runStartCount=0). langgraph-python multimodal fails locally for the
exact same reason, yet declares the feature supported.

Verified the MAF agent actually works: with the real assets in place
the D6 cell is green (2 turns, assertions passed). Both production
deploys serve the real 10KB PNG (LFS pulled), so multimodal is green
in prod for MAF just like langgraph. not_supported_features now matches
langgraph exactly: [gen-ui-interrupt, interrupt-headless].
2026-08-05 14:12:16 +03:00
Ran Shem Tov 47c56c8f9c fix(showcase): MAF Python manifest + exact pins for clean validate
- Remove multimodal from features (already in not_supported_features);
  the duplicate made generate-registry reject the manifest, blocking D6
  from starting.
- Pin agent-framework-{ag-ui,openai,core} to exact published latest
  (1.0.1 / 1.12.0 / 1.13.0) so validate-pins classifies them as exact
  framework deps; drop unused langchain-openai/langchain-core.
- Ratchet validate-pins baseline down 31 -> 27 (the 4 non-exact framework
  fails are now exact); ms-agent-python's only remaining FAIL is the
  shared-frontend openai ^5.9.0, identical to every other integration.
2026-08-05 14:12:16 +03:00
Ran Shem Tov a71768d3c4 chore(showcase): MAF Python deps floor to explicit agent-framework 1.0+ latest
Replace main's ugly beta floors (ag-ui 1.0.0b251117 / openai 1.0.0rc6) with
clean 1.0+ ranges. Resolves to latest (ag-ui 1.0.1, core 1.13.0, openai 1.12.0);
D6 36/36 green on that.
2026-08-05 14:12:16 +03:00
Ran Shem Tov 02f079180d fix(showcase): MAF Python frontend-tools-async + gen-ui-open green; quarantine multimodal
- frontend-tools-async: drop the stray broad 'project planning' fixture entry that
  substring-shadowed 'Find my notes about project planning' -> run-loop. Green.
- gen-ui-open + gen-ui-open-advanced: remove 6 gen-ui-open-owned strays from
  gen-ui-tool-based.json ('3D axis visualization', 'Inline expression evaluator',
  'render an open gen-ui element', 'continue the advanced gen-ui flow') that
  collided (same context, different responses) with gen-ui-open.json. Green;
  gen-ui-tool-based unaffected.
- multimodal: quarantine. On main + latest deps the browser gets 200 from the
  runtime but never starts a run (runStartCount=0) — a shared multimodal
  frontend/runtime issue (identical frontend to langgraph; agent works via direct
  POST; my #5985 fix doesn't change it). Genuine gap, needs shared-layer work.

Full D6 now 36/36 green on official latest (ag-ui 1.0.1, core 1.13.0, openai 1.12.0).
2026-08-05 14:12:16 +03:00
Ran Shem Tov b56bb182d2 feat(showcase): MAF Python shared-state + reasoning-chain green on main (latest deps)
- shared-state-streaming: replace main's stale 'counter' fixture (drifted from
  langgraph canonical) with the write_document document demo (6-entry poem/email/
  quantum + chunkSize) + SharedStateStreamingFrameworkAgent seed subclass; frontend
  already matches langgraph. Un-quarantine + add to features. Green (3/3 turns).
- tool-rendering-reasoning-chain: un-quarantine + add to features. Green on core
  1.13.0 / openai 1.12.0 (latest) via aimock encrypted_content (CopilotKit/aimock#342,
  which fixes the Responses reasoning multi-tool regression) + the store:False agent.
  CI-green depends on aimock#342 releasing.
- Drop reasoning-default-render / agentic-chat-reasoning from not_supported (no D6
  probe featureType — not real cells).
2026-08-05 14:12:16 +03:00
Ran Shem Tov 9de40f4e10 fix(showcase): MAF Python custom-catchall green on main base
Port the #5985 fix onto main: ported langgraph's full 18-entry custom-catchall
fixture (main's 4-entry set left SF/flights/d20/chain pills leaking to the
default-catchall fixture) + _ToolRenderingFrameworkAgent dropping the divergent
end-of-run MESSAGES_SNAPSHOT so the narration (not the tool card) is the terminal
bubble. Verified green via per-cell --direct. First cell of the #5985->main
re-integration.
2026-08-05 14:12:16 +03:00
Ran Shem Tov c2cb1c9e60 fix(showcase-mastra): render interactive open-gen-ui on a live endpoint
Both open-gen-ui cells reused the shared weatherAgent
("You are a helpful assistant.", gpt-4o). On a live LLM that produced
static HTML with no JS wiring - buttons rendered but were dead. aimock
hid it: fixtures match on userMessage/hasToolResult and replay a
fully-wired UI regardless of the agent.

Gold langgraph-python uses dedicated agents whose system prompt mandates
a single interactive generateSandboxedUi call and reads the design-skill
+ sandbox-function descriptors from copilotkit context. On Mastra,
RunAgentInput.context is a read-channel (requestContext.get("ag-ui")),
not injected into the prompt, so that guidance never reached the model.

Add dedicated openGenUiAgent / openGenUiAdvancedAgent porting gold's
prompts, with dynamic instructions that fold the ag-ui context into the
system prompt, and point the ogui route at them.

Verified live (real OpenAI, aimock bypassed) - all three advanced pills
render interactive, host-wired UI with confirmed round-trips:
evaluateExpression 7*8=56, notifyHost, evaluateExpression 5+3*2=11.
aimock matcher keys are untouched, so replay is unaffected.
2026-08-05 13:29:30 +03:00
Sean 12a55f3c46 Merge branch 'main' into fix/react-core-activity-contentkey-memo 2026-08-05 14:22:25 +08:00
Maxim 2f24038c0d chore(skills): sync in-memory runner reference skills and mirror
Updates the runtime agent-runner skill references to match the bounded
in-memory runner: correct the InMemoryAgentRunner store as a process-global
singleton, document its bounds and onConcurrentRun concurrency handling, note
that dedup weakens past the run cap, fix rotted in-memory.ts citations onto
stable symbols, and correct the multi-instance SqliteAgentRunner scaling
guidance. The generated skills/ mirror is regenerated in lockstep so source and
mirror stay in sync.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 03:06:24 +02:00
Maxim c91376452a docs(runtime): document in-memory runner bounds, concurrency, and durability
Documents the now-bounded in-memory runner for users: the maxThreads /
maxRunsPerThread / maxBytes limits and their defaults, the precise eviction
model (LRU threads, per-thread run-cap, cross-thread byte ceiling enforced at
run completion), and the onConcurrentRun throw/supersede option. Clarifies that
the store is a process-global singleton shared by every runner, that dedup
weakens past the run cap, and points at the first-party SqliteAgentRunner for
durable or multi-instance deployments. Adds a troubleshooting entry for the
in-memory eviction warning.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 03:06:15 +02:00
Maxim 8f56cc9457 test(runtime): cover bounded store, eviction, and run teardown isolation
Adds a dedicated bounded-thread-store suite and extends the in-memory runner
suite to lock in the behaviors introduced with the bounded, teardown-isolated
runner:

- LRU thread eviction, per-thread run-cap FIFO trimming, and byte-ceiling
  eviction (including that a live/running or stop-requested thread is never
  evicted, and that a just-appended thread pushes OTHER threads out rather than
  self-evicting).
- InMemoryLimits validation/normalization: invalid bounds clamp to defaults
  instead of crashing enforceRunCap, and the 0/Infinity disable sentinels are
  preserved.
- Thread-level createdAt and message-snapshot decoupling survive run-cap
  eviction and interleaved empty-snapshot runs.
- stop() guards and the supersede path: an aborted run finalizes as a clean
  RUN_FINISHED against its own captured intent, a superseded run cannot clobber
  its replacement's state or history, and an immediate abort-throw that emitted
  nothing creates no phantom historic run.

Also restores the shared store's default limits after tests that reconfigure
the process-global store so suites stay isolated, and updates a handle-run
comment for the GLOBAL_STORE -> shared store rename.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 03:06:07 +02:00
Maxim e93119ae20 fix(runtime): isolate a superseded or stopped in-memory run's teardown
A run's async teardown used to read the shared, mutable store.stopRequested to
decide whether to finalize as a clean RUN_FINISHED or a synthetic RUN_ERROR.
Under supersede (or a stop() immediately followed by a new run()), a later run
resets that shared flag, so the earlier run's fire-and-forget finalization could
be mislabelled — an intentionally stopped run finalized as an error, or vice
versa — and could push history under, or clobber the state of, the newer run
that now owns the thread.

Fix by capturing a per-run RunFinalizeControl when the run starts. stop() and a
superseding run() flip THAT run's captured control (not just the store flag),
and the run's teardown reads its own captured intent, so a later run resetting
store state can never change how an earlier run finalizes.

The teardown itself is unified into a single finalizeRun helper shared by the
success and error paths (they were near-identical and must stay symmetric) and
made ownership-aware:

- It only pushes history / resets shared store state when this run still owns
  the thread (store.currentRunId still equals this run's id), so a superseded
  run cannot corrupt the successor's history or state.
- The error path additionally requires at least one real (pre-finalize) event,
  reviving a guard that had gone dead: an immediate throw that emitted nothing
  must not create a phantom historic run holding only the synthetic terminal.
- On completion it releases the run's infinite ReplaySubject buffer via an
  identity guard (store.subject === nextSubject), reclaiming the duplicate
  buffer on the owning path while leaving a live successor's subject untouched.

The concurrency branch now also triggers on store.stopRequested, not just
isRunning: stop() flips isRunning off the instant it aborts but the run keeps
finalizing, and a run() slipping through that window went entirely unhandled.
The previous-subject bridge is removed: forwarding a dying superseded run's
subject would replay its RUN_STARTED and push its terminal into the live run's
stream, an invalid AG-UI sequence — a superseded run must stay isolated to its
own subscribers.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 03:05:35 +02:00
Maxim 05fbd70938 fix(runtime): delegate InMemoryAgentRunner storage to the bounded store
Route every storage access in the runner through the shared ɵBoundedThreadStore
instead of the old unbounded GLOBAL_STORE Map: run() acquires threads via
getOrCreate (which applies LRU eviction), and connect/isRunning/stop/
listThreads/getThreadMessages/getThreadEvents/clearThreads read through the
store's touch-aware accessors so reads keep LRU order honest.

The constructor now accepts InMemoryLimits inline alongside onConcurrentRun.
Note the scope difference, called out in the JSDoc: onConcurrentRun is
per-runner, but the limits reconfigure the PROCESS-GLOBAL store shared by every
runner. A partial limits update coalesces each unspecified field against the
store's current effective bounds (not the hardcoded defaults), so tuning one
bound never silently resets its siblings; a genuine clobber of an
already-customized store warns once.

getThreadMessages now returns the thread-level snapshot (a shallow array-level
copy) rather than the last run's snapshot, so run-cap eviction and interleaved
empty-snapshot runs can never lose it. getThreadState is hardened to reject
arrays (which pass `typeof === "object"`) and to return a defensive shallow
copy so callers cannot mutate stored snapshot state.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 03:05:14 +02:00
Maxim 4bd4fff6a3 fix(runtime): add bounded in-memory thread store with limits validation
The in-memory runner previously kept every thread and run forever in an
unbounded process-global Map, so a long-lived process leaked memory without
limit. Introduce ɵBoundedThreadStore as the single backing store, enforcing
three independent bounds resolved from InMemoryLimits (defaults in
ɵINMEMORY_DEFAULTS):

- maxThreads: LRU eviction of whole threads.
- maxRunsPerThread: FIFO run-cap per thread.
- maxBytes: approximate cross-thread byte ceiling (via ɵestimateBytes),
  enforced at run completion by evicting other LRU non-running threads.

Limit values are validated and normalized once (ɵnormalizeLimits /
ɵisValidLimit): only a non-negative integer or +Infinity is well-formed.
Invalid values (negatives, -Infinity, NaN, fractional caps) would otherwise
turn the `count > limit` enforcement guards into infinite loops or a shift()
of undefined; they are instead clamped to the documented default with a single
warning. Clamp-and-warn rather than throw matches this file's best-effort
posture (ɵestimateBytes swallows serialization failures), because constructing
a non-durable convenience runner must never abort — or later surface an
unhandled rejection — on a typo'd bound.

Thread creation time and the latest non-empty message snapshot are held at the
THREAD level (InMemoryEventStore.createdAt / messagesSnapshot), decoupled from
historicRuns so run-cap FIFO eviction can neither drift the reported creation
time forward nor drop the message history. Eviction — whole-thread LRU and
per-thread run-cap trimming alike — is logged once per store (warn-once latch)
so bounded history loss is visible rather than silent.

Also defines the per-run RunFinalizeControl shape and the store's
activeFinalize holder that the run-teardown isolation builds on.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 03:04:55 +02:00
Maxim 25817605cc fix(reskinnable-demo): put real data behind the banking report, and fix the six defects it was hiding (#6378)
Fixes the six defects a 12-agent review found in the banking skin, by
fixing the thing that caused most of them: the skin had **three
disagreeing answers to "what did we spend"**.

| Source | Total | Drove |
|---|---|---|
| `charges-data.ts` fixture (45 rows, Apr–Jun) | $632,806 | the Charges
page |
| `seed.json` ledger (4 rows, **Apr–May only**) | $30,089 | the report's
**charts** |
| static `policies[].spent` | $137,000 | the report's **KPI** |

Every one of those numbers appeared in the product, and they were never
reconciled.

## The headline defect

`SpendingTrendChart` substituted a hard-coded `[3200, 4100, 3600, 5200,
4800, 6400]` / Jan–Jun series whenever fewer than three distinct months
were present. Intended as an empty state — but the ledger spanned
exactly **two** months, so the fallback was the **default path**. The
report's "Spend over time" showed six invented figures, roughly 20×
smaller than the total printed directly above them, under a card whose
own docstring reads:

> Every number is computed here from the live ledger … so a report can
never quote a figure the app disagrees with.

Reproduced in the running app before fixing (`POST
/api/banking/v1/reports`, no agent needed), and there's a nasty
interaction worth knowing: attaching an invoice dates a synthetic
transaction *today*, supplying a third month, so **attaching an invoice
masked the bug**. A test written casually would sit in the masked state
and pass.

## The fix: one ledger

The 45 charges now live in `seed.json` as real transactions across
**Apr/May/Jun**, and the Charges page reads them over REST like every
other surface.

**Team and policy are now different axes.** A charge belongs to one of
seven org teams; a policy is one of three budget envelopes (Technology /
Go-to-Market / G&A) and several teams share one. They were a single
`ExpenseRole` enum — which is exactly why covering every team meant
choosing between a seven-slice donut and discarding real charges.
`ExpenseRole` still types a member's own team; `PolicyType` types the
envelopes, joined by `policyForTeam`.

**`policies[].spent` is derived from approved charges on every read.**
It could no longer disagree with the charts — and it also now *moves*:
approving a charge previously left `spent` untouched, so the budget
never reflected the approval and the over-limit gate kept comparing
against a stale figure. Verified live: approving a \$960 charge moved
`spent` by exactly \$960.

**Over-limit is derived-only.** A charge no longer *stores*
`over-limit`; the Charges badge resolves through `withOverLimit`, the
same rule the report uses.

## The six review findings

| | Defect | Fix |
|---|---|---|
| a1 | report charted fabricated spend | charts real months; explicit
empty state at zero |
| a2 | donut its own docs said would collapse | real \$533k base — the
\$900k invoice that hit **89%** now reaches **73%**; 89% would need
~\$2.8M |
| a3 | `report` matched inside "quarterly report" | anchored both sides,
whole words |
| a4 | `as Transaction` laundered a nullable `policyId` | cast removed;
compiler checks it |
| a5 | comment claimed additions "have no policyId" | corrected — three
lines from the code that sets it |
| a6 | `?sort=banana` lit the "active" tint | params validated; unknown
reads as unset |

All six existed **identically in `examples/showcases/banking/`** — they
came from the upstream PRs this skin replayed, not from the port. Scoped
to the skin per review; banking still carries them.

## The scripted demo is unchanged by construction

- the four demo-load-bearing transactions survive byte-identical
- over-limit is still **exactly 3 charges / \$30,000**
- AWS \$15,000 still derives over-limit for the teach-mode pill
- Delta Airlines is still the only Delta charge (the fixture's
near-duplicate "Delta Air Lines" became United Airlines)
- all four status badges still appear (`Amazon Business` is kept
pending, under its policy's headroom, so a plain **Pending** chip
survives)

The donut goes 44/40/16 → **48/27/24** and is relabelled "Spend by
policy", which is what it reads.

## Verification

```
nx build react-core,a2ui-renderer,core,runtime,shared   exit 0
tsc --noEmit                                            0 errors
vitest                                                  239 passed (was 215)
eslint                                                  0 problems
```

Plus live checks against a running server: 49 transactions over 3
months, derived `spent` tracking an approval, over-limit holding at
3/\$30,000.

The 24 new tests are confirmed **red** against the old code —
`parseSort("banana")` returned `"banana"`, `parseTop("-5")` returned
`-5`, the seed spanned two months, no row carried a team, and `spent`
was stored.

## Not in this PR

A "tool replay-guard sweep" (4 items, `navigateToPageAndPerform` and
three approval tools missing the resolved-state guard `showCharges` has)
and ~13 subject-neutral items are captured as follow-ups.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-05 02:07:14 +02:00
github-actions[bot] dde029bb75 style: auto-fix formatting 2026-08-05 00:06:48 +00:00
Mark 2b67cb43bf fix(showcase): gate url-source content and correct the v1-parity claim
Adversarial review of the two prior commits found the multimodal gate was
only half fixed, plus an overclaim.

Gate: _NATIVE_CONTENT still short-circuited ImageUrl and DocumentUrl. v2's
load_messages builds those from unvetted client input, so url-source
attachments bypassed the gate entirely where v1 had routed them through it:

- an image/heic or image/svg+xml url reached the provider as input_image,
  which the Responses API rejects, failing the turn
- an audio/mpeg document url reached it as input_file
- a blank-mime inline PDF was handed to the image sniffer and degraded,
  because load_messages collapses ImageInputContent and DocumentInputContent
  to the same bare BinaryContent and erases the modality v1 defaulted on

Native content is now gated BEFORE the fixpoint check rather than instead of
it: _classify_native_content returns a tuple only when the gate must act, and
None means provider-safe, which falls through to the fixpoint and preserves
the object's identity. ImageUrl is gated rather than rerouted, so a
provider-safe one keeps its identity and any explicit _media_type.

_url_media_type reads FileUrl.media_type, which INFERS from the url and
raises when it cannot; the explicit _media_type is None unless the client set
one, so reading only the latter would have missed a.heic entirely.

Parity claim: the mount_agent docstring said routing and behaviour were both
unchanged. Routing is; model input is not. v2 defaults
manage_system_prompt='server', so each agent's system_prompt now reaches the
model. On v1 it never did — _agent_graph emitted system parts only if not
messages, and the AG-UI bridge always supplied history — so 18 of 19 agents
had silently dead prompts. Verified by A/B on both versions with the same
agent and request: v1 sends 0 system-prompt parts, v2 sends 1. The new
behaviour is correct and is kept; the docstring now says so instead of
claiming parity.

52/52 python tests pass. Gate verified end-to-end through
load_messages -> flatten -> _map_user_prompt for heic/svg/png urls, inline
heic/png, audio-mime document urls, blank-mime PDFs, and un-inferable urls;
f(f(x)) == f(x) confirmed for ImageUrl, DocumentUrl and BinaryContent.
2026-08-05 00:05:10 +00:00
Maxim 8829633e29 Merge branch 'main' into fix/banking-skin-real-data 2026-08-05 02:00:42 +02:00
Mark 4917c67c99 fix(showcase): apply the multimodal provider gate to v2 native content
PydanticAI v2's AGUIAdapter.load_messages converts AG-UI attachments to
native content types before the model boundary; v1 delivered the raw AG-UI
part dicts. _NATIVE_CONTENT listed BinaryContent as a flatten fixpoint, so
under v2 inline attachments were waved straight through and the whole
provider gate was skipped:

- inline PDFs were no longer text-extracted, so raw bytes went to OpenAI
- unsupported image subtypes (HEIC/SVG/TIFF) were no longer degraded and
  reached the provider as images, which fails the turn
- missing-mime magic-byte sniffing never ran
- AudioUrl/VideoUrl were neither fixpoints nor classifiable, so they hit
  the fail-loud raise

BinaryContent is no longer treated as a fixpoint. _classify_native_content
maps native content onto the same (kind, scheme, mime, value) tuple the
AG-UI classifier produces, so every existing gate — PDF extraction, image
sniffing, the supported-subtype allow list, the binary choke point —
applies unchanged. audio/* and video/* mimes are named explicitly because
_kind_for routes them to "other", and a missing mime defaults to "image"
so the sniffer runs.

Five test assertions checked that state-backing content was still AG-UI
InputContent, which encoded v1's bridging. They now assert the flatten's
output (ImageUrl) never appears in state, which is the guarantee they were
written to protect. Behaviour is otherwise unchanged: a supported inline
image still flattens to an ImageUrl data URI exactly as on v1.

52/52 python tests pass, up from 42/52. Verified against pydantic-ai
2.22.0.
2026-08-04 23:24:43 +00:00
Maxim addbe2888d test(reskinnable-demo): lock in the single-ledger invariants
Two new suites, each written so it would have failed against the previous code
rather than merely describing the new behaviour:

- derived-spend: policies[].spent equals the sum of approved charges, excludes
  pending/flagged (so the over-limit check cannot double-count the charge it is
  gating), and agrees between policies() and findPolicy(). Plus the properties
  the demo depends on — at least three distinct months (the condition whose
  absence made the trend chart fabricate), AWS $15,000 still deriving
  over-limit for the teach-mode pill, exactly one Delta charge, and a team and
  category on every ledger row.
- charges-data: parseSort and parseTop reject the values that previously slipped
  through as valid, and toChargeRow's over-limit projection only applies to
  pending charges.

Confirmed red against the old code: parseSort("banana") returned "banana",
parseTop("-5") returned -5, the seed spanned two months, no row carried a team,
and spent was a stored field.

The four existing fixtures move from ExpenseRole to PolicyType for policy
`type`, following the team/policy split.
2026-08-05 00:40:55 +02:00
Maxim d7e7c8eaf4 fix(reskinnable-demo): stop the alert marker firing on ordinary notes
The regex that decides whether a transaction note gets a red-alert prefix was
anchored on the left only, so `report` matched inside "reporter" and "quarterly
report" and `disput` inside "disputation". A note reading "attached to the
quarterly report" was served a fraud marker.

Anchored on both sides and switched from stems to whole words, verified against
the four phrasings that previously false-positived.
2026-08-05 00:40:40 +02:00
Maxim 05390dc625 fix(reskinnable-demo): stop the banking report showing invented figures
Four defects in the report's charts, all reported by review and all
reproduced in the running app before fixing.

- SpendingTrendChart substituted a hard-coded [3200, 4100, 3600, 5200, 4800,
  6400] Jan-Jun series whenever fewer than three months were present. Intended
  as an empty state, it was the DEFAULT path: the seeded ledger spanned two
  months, so the report's "Spend over time" always showed six invented numbers
  — roughly 20x smaller than the total printed directly above them — under a
  card whose own contract says every number comes from the live ledger. It now
  charts whatever months exist, with a real empty state at zero.
- SpendBreakdownChart's docstring said the report must use SpendByTeamBars
  instead, "because an attached invoice can push one team to ~96% and a donut
  cannot survive that", while the report rendered the donut anyway. The warning
  was real but its cause was the thin ledger, not the chart: against the old
  $137,000 base a $900,000 invoice took one slice to 89%. Against the real
  ~$533,000 base the same invoice reaches 73%, and 89% would need ~$2.8M.
  Robust because the data is real, not because a floor was added to the arc.
- augmentForReport built its synthetic transactions behind an `as Transaction`
  cast that was hiding a real hole: `policyId` came from an `?.id` lookup, so it
  could be undefined where the field is a required string. The cast is gone and
  the compiler checks it. Additions also now resolve their model-authored team
  to a policy envelope through `policyForTeam` rather than comparing a team name
  to a policy name, with one "Unattributed" segment for unmappable names.
- A comment inside TopChargesChart claimed document-sourced charges "have no
  policyId" — three lines above the code that gives them one.

The donut column is relabelled "Spend by policy", which is what it reads.
2026-08-05 00:40:40 +02:00
Maxim 274d46ffcb feat(reskinnable-demo): put one real ledger behind the banking skin
The skin held three disagreeing answers to "what did we spend": a 45-row
Charges fixture ($632,806), a 4-row seeded ledger ($30,089 across two
months), and static policy totals ($137,000). Each surface read a different
one, so they drifted silently — and because the ledger spanned only two
months, the report's trend chart fell back to a hard-coded series and showed
invented figures under a card that promises live numbers.

Now there is one ledger. The 45 charges live in seed.json as real
transactions across Apr/May/Jun, and the Charges page reads them over REST
like every other surface.

- Splits team from policy. A charge belongs to one of seven org teams; a
  policy is one of three budget envelopes (Technology / Go-to-Market / G&A)
  and several teams share one. These were a single `ExpenseRole` enum, which
  is why the two axes read as one thing and why covering every team meant
  either a seven-slice donut or discarding real charges. `ExpenseRole` still
  types a member's own team; `PolicyType` types the envelopes, joined by
  `policyForTeam`.
- Derives `policies[].spent` from approved charges on every read, so it can
  no longer disagree with the charts. It also now MOVES: approving a charge
  previously left `spent` untouched, so the budget never reflected the
  approval and the over-limit gate kept comparing against a stale figure.
- Makes over-limit derived-only. A charge no longer stores "over-limit"; the
  Charges table resolves the badge through `withOverLimit`, the same rule the
  report uses, so the two cannot disagree.
- Validates the `?sort=` and `?top=` params. `?sort=banana` used to be cast
  straight to a SortKey and lit the control's "active" tint while the table
  silently sorted by the default; `?top=-5` reached `slice(0, -5)` and dropped
  the LAST five rows, inverting top-N.

The scripted demo is unchanged by construction: the four demo-load-bearing
transactions survive byte-identical, over-limit is still exactly three charges
totalling $30,000, AWS $15,000 still derives over-limit for the teach-mode
pill, and Delta Airlines is still the only Delta charge (the fixture's near
-duplicate "Delta Air Lines" became United Airlines).
2026-08-05 00:40:22 +02:00
Mark febd24954c chore(showcase): port pydantic-ai integration to Pydantic AI v2
Server, agents and pins moved to v2. The /multimodal cell is NOT yet
ported — see below.

- requirements.txt: pydantic-ai-slim 2.22.0, ag-ui-protocol 0.1.19. Drops
  the opentelemetry-api<1.44 ceiling; v2 works with otel 1.44.0.
- StateDeps imports move from pydantic_ai.ag_ui to pydantic_ai.ui across
  the 8 agent modules and one test.
- agent_server.py: to_ag_ui() was removed in v2, so a mount_agent() helper
  builds the equivalent Starlette sub-app (single POST / route, matching
  what AGUIApp produced) and mounts it. All 19 paths are byte-identical,
  trailing slashes included, so no TS route changes. deps are constructed
  per request because v2's adapter assigns client state onto deps.state
  instead of replacing the deps object as v1 did.
- test_multimodal_content_mapping.py: importorskip pointed at the removed
  pydantic_ai.ag_ui, which would have skipped all 43 tests green under v2.
  Now targets pydantic_ai.ui.ag_ui and uses the public
  AGUIAdapter.load_messages in place of the v1 private helper.

Verified against pydantic-ai 2.22.0 by driving the real app with
TestClient: 16/19 mounts return 200 SSE with RUN_STARTED..RUN_FINISHED and
no RUN_ERROR. The other 3 (/a2ui_dynamic, /beautiful_chat, /) reach tool
execution and then fail on a raw OpenAI() client constructed inside a tool,
which the harness cannot intercept and aimock handles in CI. Per-request
deps isolation confirmed on /shared_state_read_write: state sent by one
request does not appear in the next.

Known incomplete: multimodal_agent.py does not handle v2's content types.
v2's load_messages emits BinaryContent where v1 emitted ImageUrl, so
_flatten_messages_for_model no longer flattens it, VideoUrl reaches the
unrecognised-part raise, and one binary path raises RuntimeError. 10 of
the 43 multimodal tests fail as a result.
2026-08-04 22:39:42 +00:00
Tyler Slaton c236e33978 chore: release monorepo v1.66.2 (#6376)
## Release monorepo v1.66.2

**Scope:** `monorepo` | **Bump:** `patch`

---

### How this release process works

1. **This PR was created automatically** by the "release / create-pr"
workflow.
   It bumped the `monorepo` packages to `1.66.2`
   and generated AI-enhanced release notes.

2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
   must pass before merging. This is the review gate.

3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.

4. **When this PR is merged**, the `release / publish` workflow
automatically:
   - Builds all packages
   - Publishes the `monorepo` packages to npm at version `1.66.2`
   - Creates git tag `monorepo/v1.66.2`
   - Creates a GitHub Release with the final release notes

### Before merging

- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)

---

> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
v1.66.2
2026-08-04 14:58:22 -07:00