Commit Graph

1456 Commits

Author SHA1 Message Date
Tyler Slaton 88fc3da497 fix(next): guard root bodies against extension hydration races 2026-08-05 11:25:13 -07:00
Maxim b93b0951bc docs(reskinnable-demo): teach the reskin skill what the demo must prove
The skill documented how to wire the `Skin` contract and nothing about what
the resulting demo has to demonstrate. Tested by asking a fresh agent to plan
a new BI skin with only the old skill: it produced a technically sound plan
that missed most of the banking demo's beats — no rich-thread step, zero
`useAgentContext` readables, no stored-procedure or teach-mode arc — and
substituted its own thesis (RBAC governance) for the demo's. Contract
documentation says what is available; only choreography says what is required.

Adds demo-beats.md: the nine beats framed by what the audience must conclude,
each with banking's implementation cited, plus the presentation requirements
(a pill per beat so the presenter never types, a visible affordance on every
mutation, pretty prose, Reset, the chat-placement framing), the domain
shortlist and the quality bar. The beats are a strong default an explicit
instruction can override, expressed as a beat map with a row per beat that
records deliberate skips — omission failures respond to a slot you must fill,
not to exhortation. Re-running the same planning task with the reworked skill
covers all nine.

Also folds in what tracing banking turned up, all of it silent-failure class:
renders must key off the tool `result` and not `status` or they go blank on
thread replay, exactly when "reload and the chart is still there" is being
shown; "what's on my screen?" needs a route readable plus per-page on-screen
readables, which is why that beat is impossible in the three other skins; and
long-term memory is a seeded file, not emergent, so seed-memories.ts gets a
template — including why beat 6's procedure must never be seeded and why a
seeded procedure must run without a confirmation gate.

Corrects CLAUDE.md, which claimed four skins and then documented two. Now
covers all four across both substrates with a beat-coverage matrix, fixes the
`identifyUser` attribution (logistics and keel contribute one too), drops an
unsupported claim that a Panel's `id` becomes its `data-testid`, notes keel's
parameterized routes, and acknowledges the shell-mounted inspector that
replaced the glass engine.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:16:44 +02: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
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 cdd605c672 docs(pydantic-ai): port integration docs and demos to Pydantic AI v2 (#6367)
This pull request was posted by Claude Code using claude-opus-5 on
behalf of David. David has not reviewed this diff line by line.

Closes https://github.com/CopilotKit/CopilotKit/issues/6363

`Agent.to_ag_ui()`, `AGUIApp` and the whole `pydantic_ai.ag_ui` module
were removed in Pydantic AI v2. The docs installed pydantic-ai unpinned,
so following the quickstart today gets 2.22.0 and fails twice: first at
resolution (`starlette==0.45.3` conflicts with the `>=0.46.2` the
`ag-ui` extra requires), then at `AttributeError`.

## What changed

**8 doc pages** under
`showcase/shell-docs/src/content/docs/integrations/pydantic-ai/`
(`quickstart.mdx`, `quickstart/pydantic-ai.mdx`,
`human-in-the-loop.mdx`, `human-in-the-loop/agent.mdx`,
`generative-ui/tool-rendering.mdx`, and the three `shared-state/`
pages):

- the agent is served from a Starlette route via
`AGUIAdapter.dispatch_request(request, agent=agent)`
- `StateDeps` imports move from `pydantic_ai.ag_ui` to `pydantic_ai.ui`
- install commands exact-pin `pydantic-ai-slim[ag-ui,openai]==2.22.0`
and `ag-ui-protocol==0.1.19`, matching the starter fleet, plus
`starlette>=0.46.2` since the snippets import Starlette directly

**Per-request deps.** Every stateful snippet builds `StateDeps` inside
the request handler:

```python
async def run_agent(request: Request) -> Response:
    return await AGUIAdapter.dispatch_request(
        request, agent=agent, deps=StateDeps(AgentState())
    )
```

`dispatch_request` validates the client's state into `deps.state`
(`pydantic_ai/ui/_adapter.py`, `run_stream_native`), so a module-level
instance shared across requests lets concurrent runs clobber each other.
The old `to_ag_ui(deps=...)` snippets all did this.

**`examples/canvas/pydantic-ai`** — `requirements.txt` pinned,
`agent/agent.py` ported, README corrected.

**`examples/showcases/pydantic-ai-todos`** — `pyproject.toml` pinned and
`uv.lock` regenerated (it was still resolving 1.0.10), `agent/main.py`
ported, `src/agent.py` and `src/tools.py` imports moved, README and
`src/app/api/copilotkit/route.ts` comments corrected.

**`skills/copilotkit-integrations`** — beyond the issue's file list:
`SKILL.md`, `sources.md` and `references/integrations/pydantic-ai.md`
also taught `to_ag_ui()`. Same rot, same fix.

## Verified by execution

The reason these docs rotted is that nothing runs them, so everything
below was actually run, not read.

- Both install commands were run verbatim in throwaway environments. `uv
add 'pydantic-ai-slim[ag-ui,openai]==2.22.0' 'ag-ui-protocol==0.1.19'
'starlette>=0.46.2' uvicorn` and the `pip install` equivalent both
resolve, landing pydantic-ai-slim 2.22.0, ag-ui-protocol 0.1.19,
starlette 1.3.1.
- Every ```python fence on the 8 doc pages was extracted, `exec`'d, and
driven with a real `RunAgentInput` POST through
`starlette.testclient.TestClient` with the model overridden to
`TestModel`. All 8 return 200 `text/event-stream` with a `RUN_STARTED`
... `RUN_FINISHED` sequence and no `RUN_ERROR`.
- The canvas agent was installed from its `requirements.txt` and driven
the same way: 200, SSE, `RUN_STARTED` ... `TOOL_CALL_*` ...
`STATE_SNAPSHOT` ... `RUN_FINISHED`.
- The todos agent was installed with `uv sync --frozen` from the
regenerated lock and driven the same way. Two sequential requests, one
seeding a todo and one sending empty state, each saw only their own
state, confirming the per-request deps actually isolate.

Not executed: the Next.js frontends and the docs site build (no
`node_modules` in this checkout). The TypeScript edits are comment-only.

## Deliberately out of scope

`showcase/integrations/pydantic-ai` is left on its v1 fleet pin. It is
418 files, 19 mounts and 190 e2e specs, and CopilotKit said they will
take it as https://github.com/CopilotKit/CopilotKit/issues/6364. The
dojo and the docs therefore diverge until that lands.

The CI guard from the issue's last acceptance criterion is not built
here. A proposal for it is posted on
https://github.com/CopilotKit/CopilotKit/issues/6363 for the team to
own.

Two pre-existing malformed code fences were fixed in passing, because
leaving them meant the ported snippets still would not run:
`quickstart/pydantic-ai.mdx` and
`shared-state/predictive-state-updates.mdx` each had TypeScript embedded
inside an unterminated ```python fence. The TypeScript now sits in its
own fence.

Overlaps with https://github.com/CopilotKit/CopilotKit/pull/6355, which
ports `examples/integrations/pydantic-ai`. No file overlap.
2026-08-04 12:58:07 -07:00
David Sanchez 22108c0948 docs(pydantic-ai): constrain the direct dep, not the transitive one
Follows the maintainer's Correction #2 on issue 6363. An exact version in a
docs install command is the same rot as the starlette==0.45.3 pin it replaced:
it goes stale silently and nobody re-resolves prose. The 2.22.0 the docs shipped
was already a version behind current the day it was written.

- docs install lines use pydantic-ai-slim[ag-ui,openai]>=2,<3, which constrains
  the dep the pages actually care about and fails loudly at the v3 boundary
- ag-ui-protocol drops out of the docs lines entirely; no doc snippet imports
  ag_ui, so naming it there was the transitive-dep noise the correction is about
- starlette>=0.46.2 stays, because the v2 snippets import Starlette directly.
  A floor with no ceiling cannot force a downgrade, so it does not recreate the
  silent backtrack
- examples/showcases/pydantic-ai-todos moves to a range in pyproject.toml and
  relocks; the uv.lock is what reproduces
- examples/canvas/pydantic-ai keeps exact pins: it has no lockfile, so
  requirements.txt is its only reproducibility artifact

Smoke-tested the open question from the issue: starlette 1.x works on
pydantic-ai v2. All 8 doc pages pass on 2.23.0 + starlette 1.3.1 and on
2.23.0 + starlette 0.52.1, so Jordan's <1.0 guard can be dropped rather
than raised.
2026-08-04 12:08:20 -05:00
Mark 14a294f2d8 Port the Pydantic AI example to Pydantic AI v2 (#6355)
This pull request was posted by Claude Code using claude-opus-5 on
behalf of David. David has not reviewed this diff.

`examples/integrations/pydantic-ai` only runs on Pydantic AI **v1**.
`Agent.to_ag_ui()`, `AGUIApp`, and the `pydantic_ai.ag_ui` shim were all
removed in v2 (pydantic/pydantic-ai#5464, announced in
pydantic/pydantic-ai#5345). The example pins `pydantic-ai-slim==1.0.18`,
so anyone installing it against current Pydantic AI (2.22.0) fails at
import.

This ports it to v2.

## Changes

1. `agent/pyproject.toml` — `pydantic-ai-slim[ag-ui,openai]>=2.0.0`,
`ag-ui-protocol>=0.1.19`
2. `agent/src/agent.py` — `StateDeps` moved from `pydantic_ai.ag_ui` to
`pydantic_ai.ui`
3. `agent/src/main.py` — serve via `AGUIAdapter.dispatch_request` on a
Starlette route
4. `agent/uv.lock` — relocked (resolves `pydantic-ai-slim` 2.22.0,
`ag-ui-protocol` 0.1.19)

## One fix beyond the mechanical port

The old wiring built the app once around a single shared `StateDeps`
instance. `dispatch_request` mutates `deps.state` with the state the
client sends, so one shared instance lets state leak between threads,
channels and users — which matters more for Channels than it did for a
single browser tab. Each request now gets its own `replace(deps)` copy,
matching the pattern in [Pydantic AI's own AG-UI
examples](https://github.com/pydantic/pydantic-ai/blob/main/examples/pydantic_ai_examples/ag_ui/api/shared_state.py).

<details><summary>Verified end to end</summary>

`uv sync` + a request through the actual ASGI app (model overridden with
`TestModel` so no API call), with a Channels-shaped payload (`threadId`,
`state`, `forwardedProps`):

```
health: 200 {'status': 'ok'}
POST / -> 200 text/event-stream; charset=utf-8
event types: ['RUN_STARTED', 'TOOL_CALL_START', 'TOOL_CALL_END', 'TOOL_CALL_START', 'TOOL_CALL_ARGS',
 'TOOL_CALL_END', ..., 'TOOL_CALL_RESULT', 'STATE_SNAPSHOT', 'TOOL_CALL_RESULT', 'STATE_SNAPSHOT',
 'TEXT_MESSAGE_START', 'TEXT_MESSAGE_CONTENT', ..., 'TEXT_MESSAGE_END', 'RUN_FINISHED']
threadId echoed: slack-C123-thread-1
tool call: get_proverbs
tool call: add_proverbs
tool call: set_proverbs
tool call: get_weather
```

The emitted event set is exactly what `channels-slack`'s `RunRenderer`
subscribes to (`RUN_*`, `TEXT_MESSAGE_*`, `TOOL_CALL_*`), plus
`STATE_SNAPSHOT` which it ignores.

</details>

<details><summary>Note on <code>ag-ui-protocol</code>: 0.1.19, not
0.1.18</summary>

Not required by this port, but worth pinning forward: typed multimodal
input content (`ImageInputContent` &c.) landed in 0.1.15 and the
interrupt lifecycle in 0.1.19. We found that a Pydantic AI install below
0.1.15 rejects an inbound image attachment with a 422 rather than
skipping it — so a Channels gateway forwarding a Slack image needs the
newer floor. We're tracking that on our side.

</details>
2026-08-04 09:02:10 -07:00
David Sanchez d19bd0f81c docs(pydantic-ai): port integration docs and demos to Pydantic AI v2
Agent.to_ag_ui(), AGUIApp and the pydantic_ai.ag_ui module were removed in
Pydantic AI v2. The docs installed pydantic-ai unpinned, so anyone following
the quickstart got 2.22.0 and failed first at dependency resolution
(starlette==0.45.3 conflicts with the >=0.46.2 the ag-ui extra needs) and then
at AttributeError.

- 8 doc pages under showcase/shell-docs .../integrations/pydantic-ai serve the
  agent from a Starlette route via AGUIAdapter.dispatch_request
- StateDeps moves from pydantic_ai.ag_ui to pydantic_ai.ui
- stateful snippets build StateDeps per request; dispatch_request writes the
  client's state into deps.state, so a shared instance leaks state between users
- install commands exact-pin pydantic-ai-slim==2.22.0 and ag-ui-protocol==0.1.19
- examples/canvas/pydantic-ai and examples/showcases/pydantic-ai-todos ported
  and pinned, todos relocked
- skills/copilotkit-integrations reference updated to the same shape

showcase/integrations/pydantic-ai is deliberately untouched; it is tracked
separately.
2026-08-04 10:51:17 -05:00
David Sanchez 4be25161b1 fix(examples): pin pydantic-ai starter deps and build deps per request
Address review on CopilotKit/CopilotKit#6355:

- Pin `pydantic-ai-slim[ag-ui,openai]==2.22.0` and `ag-ui-protocol==0.1.19`,
  matching the starter fleet standard. Open-ended floors would pull a
  breaking major on release, and `docker/Dockerfile.agent:16` runs a bare
  `uv sync` that would silently re-resolve forward.
- Mirror the specifiers in `uv.lock`'s `requires-dist` so `uv sync --frozen`
  at `Dockerfile:42` keeps working. Resolved versions and hashes are
  unchanged; `uv lock` is a no-op.
- Construct `StateDeps(ProverbsState())` per request instead of
  `dataclasses.replace()`. `replace()` is a shallow copy, so the new deps
  point at the same state object; it is only safe today because `StateDeps`
  has one field that the adapter rebinds before every run. Any deps class
  with a second mutable field would silently share it.
2026-08-04 10:31:07 -05:00
Maxim 0b1b37e0af Merge origin/main into feat/reskinnable-demo-keel-skin
Resolves the append-only registry conflict this PR's description predicted:
#6302 (logistics / Meridian) landed first and took the same two lines, so
keel takes the trivial conflict. Both edits are additive, so the resolution
is keep-both in both registries — logistics first (it landed first), keel
second.

Why the merge was necessary rather than optional: GitHub does not create
pull_request workflow runs for a PR whose mergeable state is CONFLICTING,
because it cannot compute the merge ref those events run against. The push
of the review fixes therefore produced ZERO CI runs — verified over 12
minutes of polling the Actions API by head SHA, and corroborated by Actions
being healthy repo-wide at the same time. Not a timing artifact, and not a
path filter: test_e2e-legacy-v1 and auto_merge_showcases both trigger on
pull_request with paths: examples/**, and synchronize is a default activity
type.

Scope of the drift: main is 127 commits ahead of the merge-base, but within
reskinnable-demo it added only logistics' own files (48 skin + 17 API route
+ 2 reskin-skill docs) plus +7 append-only lines across exactly the two
registry files. No globals.css change, no skin-contract.ts change, no
skins-config.ts change — so the shared token vocabulary and the frozen Skin
contract are untouched, and keel's isolation claim still holds against a
four-skin registry.

All four skins are now registered under the same id in both registries
(banking, airline, logistics, keel), which is the invariant the two-registry
split exists to maintain.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 13:52:24 +02:00
Maxim c01b637bdc fix(keel): harden report surface, catalog, and remaining chrome
Remaining fixes across the ops-report builder, the a2ui canvas surface,
the catalog, agent wiring, suggestions, and the knowledge/playbooks pages:
correct the report surface output and its catalog registration so the
canvas renders the intended report, and tighten the surrounding chrome so
these entry points behave consistently. Tests cover the report builder and
canvas surface.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 04:46:21 +02:00
Maxim 28adff5dbf fix(keel): anchor seeded runs to a per-call now
Seed data used a fixed or module-load timestamp, so seeded run timelines
drifted out of a plausible window as time passed and could not be
reproduced deterministically in tests. This anchors every seeded run
relative to a `now` passed in at seed time, keeping the demo's timeline
coherent whenever it is generated and making the seed testable. A test
pins the relative anchoring.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 04:46:13 +02:00
Maxim 5bbe483073 fix(keel): pin date/time formatting and fix in-chat interactivity
Dates and times were formatted with the runtime's ambient locale and
timezone, so server and client rendered different strings and React
hydration mismatched; several call sites also built ad-hoc formatters that
drifted from one another. This centralizes formatting behind pinned
locale/timezone formatters — one per format — so output is stable across
environments. It also fixes the ChatSurface pointer-events boundary so
interactive controls rendered inside the chat transcript (playbook,
approval, and run cards) actually receive clicks. Tests cover the pinned
formatters.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 04:46:05 +02:00
Maxim 43cce12c53 fix(keel): guard prototype access on untrusted lookup keys
URL route segments and client-forwarded runtime properties were used
directly as object keys, so a crafted value like "__proto__" or
"constructor" could resolve to an inherited Object.prototype member
instead of a real entry — misrouting a page or silently matching a bogus
user identity. This constrains both lookups to own, real keys so untrusted
input can no longer reach the prototype chain. Tests cover the malicious
key cases for page resolution and user identification.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 04:45:55 +02:00
Maxim 576b3c4df8 fix(keel): make retrieval deterministic and citations grounded
Search results depended on the runtime locale for tie-breaking, synonym
expansion chained transitively into unrelated terms, and phrase matching
hit partial tokens, so the same query could return different or wrong
results across environments. Citations could also duplicate and fail to
land on their source document. This pins result ordering to a
locale-independent comparison, bounds synonym expansion to direct
(non-transitive) matches, matches phrases on whole-token boundaries,
deduplicates citations, and makes a citation click land on the correct
document. Tests lock in the deterministic ordering and citation targeting.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 04:45:46 +02:00
Maxim 3eb7d33e4f fix(keel): make engine mutations honest and single-path
The data engine could report success for mutations that changed nothing
and exposed multiple divergent write paths, so callers could not trust a
returned status or reason about a gate from one place. This routes every
mutation through one shared commit path, returns values that reflect what
actually happened, and tightens the gates so a no-op or a rejected change
can no longer be misrepresented as applied. Types and the seed-backed data
hook are updated to match, with tests covering the corrected return
contract and persona-scoped access.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkMi54kqfNwAoUB3M3CBp3
2026-08-04 04:45:36 +02:00
Maxim a8ab77d05e docs(reskinnable-demo): correct reskin skill's layout, theme, tools, and canvas guidance
The reskin authoring skill taught several patterns that ship a broken skin when
followed literally. Corrected against the debugged logistics skin (the frozen
contract wins on conflict):

- layout: h-screen overflow-hidden (not min-h-screen) + h-full aside, so the nav
  stays pinned and <main> scrolls inside it
- layout: publish --nw-nav-inset-left/right with cleanup, so the floating skin
  selector never docks on the nav and the inset does not leak between skins
- layout: document the meta-utility strip (Reset/ThemeToggle/Help) as
  skin-authored chrome, with the reset-route gating coupling
- theme: document --nw-dark-capable as the dark-mode opt-in + a .dark .theme-<id>
  example
- tools: every useComponent/useFrontendTool/useHumanInTheLoop registration must
  close with a deps array, or the closure captures empty pre-fetch data forever
- tools: a parameterized useComponent render receives the schema output directly,
  not { args }
- a2ui: a CanvasSurface must be fed by a server defineTool, never a client
  useFrontendTool
- contract: nav is display-only; resolvePage is the sole segment validator
- fix airline useData contradiction; NPE-safe no-data tools shape; skin.tsx
  dangling-import note

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 02:25:01 +02:00
Maxim 675a5d8568 fix(reskinnable-demo): pin the logistics nav to the viewport
The shell root used `min-h-screen`, which is a MINIMUM: on a page taller
than the viewport the container grew with the content, so the whole
document scrolled and the sidebar scrolled away with it. It also left
`<main>`'s own `overflow-y-auto` inert, because an unbounded parent gives
it no height to overflow against.

Switch the root to `h-screen overflow-hidden` and give the aside `h-full`,
mirroring banking's layout. The shell is now exactly one viewport tall, the
nav stays pinned, and `<main>` scrolls internally.

Measured on /logistics/lanes at a 700px viewport: document scrollHeight
700 (page no longer scrolls), aside top 0 / bottom 700, main scrollHeight
1472 vs clientHeight 700, and the aside stays at top 0 after scrolling
main by 700px.
2026-08-04 01:56:16 +02:00
Maxim deffa6c3d6 feat(reskinnable-demo): add meta-utility strip and dark mode to logistics skin
Port banking's sidebar-footer utility controls into Meridian: a Reset
(presenter-gated), the shared ThemeToggle, and a copilot Help shortcut, placed
directly above the existing "On duty" planner switcher. Give the skin a warm-
graphite dark palette (--nw-dark-capable + a .dark .theme-logistics block) so
the theme toggle is a live control. Port useAskCopilot into the skin (no cross-
skin import) and widen the reset endpoint's gate to allow presenter/booth
deployments (presenterResetEnabled OR non-production).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:16 +02:00
Maxim cf16befb11 fix(reskinnable-demo): re-register logistics gen-UI and HITL tools when data loads
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:16 +02:00
Maxim e2bd6cec43 fix(reskinnable-demo): server-side brief tool, decision records, honest greeting
Applies the final whole-branch review wave for the logistics skin.

- renderBrief: move from a client useFrontendTool to a server defineTool on the
  BuiltInAgent (mirrors banking's render_report). A client frontend-tool result
  never yields an in-stream TOOL_CALL_RESULT, so the a2ui middleware never
  emitted the a2ui-surface activity and the brief canvas stayed blank. Tool name
  kept exactly "renderBrief". build-brief-ops + catalog/definitions confirmed
  server-safe (plain Zod, no React/.tsx), so agent.ts stays server-safe.
- createDecisionRecord: implement the missing tool (globally registered) to log
  a decision NOT executed through commitMitigation, wiring the previously-dead
  fileDecision + POST /decisions path. Harden the route: require/resolve
  plannerId, derive decidedBy/role server-side (never from body), 404 unknown
  shipmentId, clamp costUsd. Client forwards plannerId only. Prompt + toolLabels
  updated. Adds a decisions route test (planner-derived identity vs body decoys,
  404, 400).
- greeting: drop the wrong "six lanes / three shipments" figures (seed has 10
  lanes, 4 exception shipments); quantitative claims bind to data, not prose.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:15 +02:00
Maxim 8fef990752 feat(reskinnable-demo): register the logistics skin in both registries
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:15 +02:00
Maxim 74f187db5d feat(reskinnable-demo): add logistics agent, suggestions, and OGUI design brief
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:15 +02:00
Maxim 5db4703b94 feat(reskinnable-demo): register logistics gen-UI, HITL, and brief tools
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:15 +02:00
Maxim 7413152a4a feat(reskinnable-demo): add logistics provider stack and per-planner identity
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:14 +02:00
Maxim 60ad3d353a feat(reskinnable-demo): expose logistics data to OGUI sandbox via projected DTOs 2026-08-04 01:56:14 +02:00
Maxim 244763f93a feat(reskinnable-demo): add logistics a2ui decision-brief canvas surface
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:14 +02:00
Maxim 34b4703341 feat(reskinnable-demo): add deterministic decision-brief op builder
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:13 +02:00
Maxim 6c19d49e31 feat(reskinnable-demo): add logistics a2ui catalog bound to live ledger data
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:13 +02:00
Maxim 16937b460d feat(reskinnable-demo): add logistics pages 2026-08-04 01:56:13 +02:00
Maxim d176d3b5ba fix(reskinnable-demo): give TradeoffTable an empty state
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:13 +02:00
Maxim 28b49eefb2 feat(reskinnable-demo): add logistics presentational components
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:12 +02:00
Maxim bc51e57e3f feat(reskinnable-demo): add logistics REST client hook with revalidation bus 2026-08-04 01:56:12 +02:00
Maxim 563665e03a feat(reskinnable-demo): add logistics identity, theme, nav, and layout chrome
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:12 +02:00
Maxim e25ff08c76 fix(reskinnable-demo): allow-list PATCH fields so pricing inputs cannot bypass the authority gate
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:12 +02:00
Maxim bcdaa51f55 feat(reskinnable-demo): add logistics REST routes with server-enforced authority gate
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:11 +02:00
Maxim b540b5db27 fix(reskinnable-demo): add same-destination alternate lanes so reroute is offerable
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 01:56:11 +02:00
Maxim dfe1ef259d feat(reskinnable-demo): add logistics role-based authority gate 2026-08-04 01:56:11 +02:00
Maxim c670247de6 feat(reskinnable-demo): compute logistics mitigation options 2026-08-04 01:56:10 +02:00
Maxim 8c7f983125 feat(reskinnable-demo): add logistics escalation code catalogue 2026-08-04 01:56:10 +02:00