1692 Commits

Author SHA1 Message Date
Tyler Slaton b3b339f544 Revert "feat(web-inspector): add Event Snippets and save-as-snippet (#6649)"
This reverts commit ba4260ad66, reversing
changes made to 47c5510b49.
2026-08-26 02:11:19 +02:00
Tyler Slaton 79c02f0f02 style(web-inspector): fewer layers on the launcher, a lens for its dot (#6688)
## What does this PR do?

Design review on the launcher and its notification dot asked for three
things: a milder face than solid black, fewer borders and background
layers, and a subtle shadow in place of the dot's heavy border. This is
all three, plus the removal of six utilities that never had any effect.

Everything here was compared side by side at production size, on a light
*and* a dark host page, before it was chosen. Two of my own first
proposals were dropped after measuring them, both described below.

**Two concerns, three commits.** `4fa38d91f` and `cd711b369` are the
launcher itself — the package change, 2 files. `295f75495` gives the
react-router lab a dark mode, because a dark host page is what this
change has to be judged against and the lab could not produce one. If
you would rather review those separately, say so and I will split them.

## The face

`#181C1F` at 95%, which review asked for.

Worth recording so it does not come up again: the near-black the review
saw was `#010507`, 20.5:1 against white. What shipped yesterday was
already `#1C1F24` at 16.5:1, so this value is a hair *darker* than the
one it replaces (17.2:1) and the difference between them is a ΔE of 2.3,
at the floor of what an eye can separate. It settles the question rather
than changing the look.

## Fewer layers

Six Tailwind utilities on the launcher set properties the unlayered
`css` block sets again — `bg-slate-950/95`, `border-white/20`, `ring-1`,
`ring-white/10` and the two hover variants. Unlayered declarations beat
layered ones regardless of specificity or source order, so none of them
has ever had any effect. Each was the package's only use, so the
checked-in stylesheet drops 980 bytes.

Of the *visible* layers, two went:

**The outer hairline.** The launcher carried two concentric lilac rings:
the border, and a second one 1px outside it as a box-shadow. The outer
one also hardcoded the lilac rather than reading `--cpk-launcher-edge`,
so it silently could not follow the token. It is replaced by a one-pixel
light edge along the top, which is what keeps the face from reading flat
without drawing a frame.

**`backdrop-blur-md`.** It sat behind a 95%-opaque fill and bought close
to nothing, while mounting a permanent blur compositing layer over a
customer's page.

**The border stays, and this is the finding that changed my mind.** I
first proposed removing it too. Against a dark host page the face
measures 1.10:1 (GitHub dark), 1.04:1 (Tailwind slate-900) and 1.22:1
(black) — indistinguishable from the page. The border is the only thing
that gives the launcher an outline there. It is not decoration.

## The dot

The collar was `1.5px`, opaque, zero blur, and 21% of the dot's
footprint. Because the dot's centre sits *on* the rim, its outer half
painted a hard dark crescent onto the **host page** rather than onto the
launcher — which is what read as "heavy". A hairline plus a soft drop
separates it just as well.

The fill becomes a lens lit from the upper left. Both stops are derived
from `--cpk-launcher-signal`, so a new tone needs no new values;
verified for the rose error tone and the violet announcement tone.

**Dropped after looking at it:** a coloured glow around the dot. It was
the obvious reading of "more premium", but the launcher already pulses
in that same colour when a failure is new, and a permanent glow competes
with the thing that is supposed to draw the eye.

**Also dropped:** tinting the border in the signal colour, which was
suggested in review. On a dark page the border is the entire silhouette,
so tinting it recolours the whole launcher for a state that can persist
for hours.

## One non-obvious consequence

Removing the blur removed a side effect nobody had written down:
`backdrop-filter` promotes the element to its own compositing layer.
Without a layer, the hover `scale(1.05)` re-rasterises the mark every
frame and it visibly jitters — geometrically nothing moves, the mark's
centre holds to three decimals, but the vector is re-rendered at
fractional offsets. `will-change: transform` asks for the layer directly
and the jitter is gone. Confirmed by eye on the running demo before this
was chosen.

## Tests

`packages/web-inspector` stays at **28 files / 611 tests**, all passing.

No new tests. The colour tests here are deliberately token-shaped rather
than value-shaped — they assert the custom property and the *sharing* of
one face and one edge between the launcher and its pill, never a hex —
so face and edge values are free to move and this change is exactly the
kind they were written to allow. The one test that constrains it, `"the
pill and the launcher share one surface and one edge"`, still passes.

What is genuinely unguarded, and was before this PR too: the dot's
collar width, the double hairline, and the Tailwind class list.
Asserting rendered geometry would need a browser test runner, which this
package does not have — jsdom computes no layout.

## How to see it

`pnpm --filter react-router-example dev`, then `http://localhost:5173`.
The launcher is top right; `Break runtime` arms the error tone and
`Break run` the announcement one. Hover it to check the mark no longer
jitters.

One thing worth knowing while reviewing: the launcher anchors top-right
and is `position: fixed` on an element mounted directly under `<body>`,
so on this page it sits over the lab's toolbar. Drag it to the lower
right and it is out of the way.

## The lab's dark mode

A dark host page is where the launcher's border earns its place, and the
lab had no way to produce one, so reviewing this change was not possible
without it.

It follows `examples/v2/react/demo` rather than inventing anything: the
host owns a `theme` state, and `CopilotChat` gets `className="dark"` —
which is what makes the package swap its own variable set. The colours
are the demo's by another route; it writes the oklch literals
CopilotKit's variables use, and those are Tailwind's neutral steps
(`neutral-950` is `oklch(0.145 0 0)`, `neutral-50` is `oklch(0.985 0
0)`, `neutral-800` is `oklch(0.269 0 0)`). Measured identical on the
running lab.

`@custom-variant dark (&:is(.dark *))` is needed in the lab's stylesheet
because Tailwind v4 points `dark:` at `prefers-color-scheme` by default,
so the toggle would have lost to the OS. Same declaration the package
uses for its own sheet.

Two details that are decisions rather than oversights. The **error
banner keeps a rose tint** in dark mode instead of going neutral,
because an error banner that looks like every other surface is not an
error banner. And the **toolbar buttons keep a visible on/off contrast**
— active inverts to a light face, inactive sits on `neutral-800` —
because the lab's whole purpose is knowing which failure is currently
armed.

My first attempt stripped every background instead of theming, and that
is worth recording because it looked plausible: the chat bubble, the
send button, the button states and the banner all collapsed into one
flat grey. The chat paints its own surfaces and has to be told what
theme it is in, not undressed.

## A separate bug found on the way

`CopilotKitProvider` documents `inspectorDefaultAnchor` — *"Default
anchor corner for the inspector button and window"* — and it has no
effect. `defaultAnchor` is typed on the React wrapper and forwarded to
the element, but the string `defaultAnchor` does not occur anywhere in
`packages/web-inspector`, so it lands as `defaultanchor="[object
Object]"` and is ignored. The corner stays hardcoded `{ horizontal:
"right", vertical: "top" }` in two places.

Not fixed here, to keep this PR to one concern. It is worth fixing: any
host with a top navigation bar hits exactly this, finds exactly that
prop, and it does nothing.

## Related PRs and Issues

- Follows #6646
2026-08-25 13:27:06 -07:00
Ben Taylor 1602ac3bb7 updated with-mcp-use README to not have stale references (#3614)
Removed redundant text and improved clarity in the README. Removed stale
and internal references

## What does this PR do?

Fixed the Readme references for the open mcp app example, it was
referencing stale docs folder which does not exist.

## Related PRs and Issues

- (Direct link to related PR or issue, if relevant)

## Checklist

- [ *] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [ ] If the PR changes or adds functionality, I have updated the
relevant documentation
2026-08-25 12:32:17 -05:00
Ben Taylor c59c1c891b fix(showcases): make MCP Apps deployable (#6634)
# fix(showcases): make MCP Apps deployable

## Summary

- keep the approved `@copilotkit/*` `1.68.1` upgrade and frontend
Railway health check
- import the endpoint stack and built-in agent from
`@copilotkit/runtime/v2`, matching the current
`examples/integrations/mcp-apps` runtime pattern
- align MCP Apps on one AG-UI `0.0.58` client/core/encoder/proto graph
with `@ag-ui/mcp-apps-middleware@^0.0.3`
- replace the stale standalone pnpm lock with the npm lock consumed by
Docker, and make Docker install it deterministically with `npm ci
--legacy-peer-deps`
- remove only unused frontend direct dependencies;
`@copilotkit/react-core`, `@copilotkit/runtime`, and
`@copilotkit/shared` remain direct `1.68.1` dependencies, while the
separately packaged MCP server retains its own `zod@^4.3.5`

## Root cause and RED evidence

- Baseline commit: `0daa38f9a7e50522e246a273802ef3ffde92556b` on current
`origin/main` `c2abbea9cf`.
- `./node_modules/.bin/tsc -p examples/showcases/mcp-apps/tsconfig.json
--noEmit --pretty false` reproduced three compatibility failures:
`TS2305` for `createCopilotEndpoint`, `TS2305` for
`InMemoryAgentRunner`, and `TS2345` because `MCPAppsMiddleware` and the
runtime resolved incompatible `@ag-ui/client` types.
- Before production edits, `npm test` ran the new compatibility contract
and failed `3/3` assertions for the legacy runtime entrypoint, stale
AG-UI versions, and missing npm lockfile.
- Before changing the Docker install command, the deployment contract
failed `1/4` because the Dockerfile still used `npm install
--legacy-peer-deps`.
- The first Docker build sent `889.11 MB` because no `.dockerignore`
excluded host `node_modules` and `.next`; the final context is `5.64
kB`.

## GREEN and verification evidence

- PASS — scoped formatter: repo-pinned `oxfmt --check` exited `0` for
the changed source, contract, and manifest inputs.
- PASS — scoped lint: repo-pinned `oxlint` reported `0 warnings` and `0
errors` for the changed route and compatibility test.
- PASS — explicit typecheck: standalone `./node_modules/.bin/tsc
--noEmit --pretty false` exited `0`.
- PASS — focused tests: `npm test` passed `5/5` contracts covering the
v2 runtime entrypoint, AG-UI graph, npm lock truth, deterministic Docker
install, and Docker context exclusions.
- PASS — production build: `npm run build` compiled, typechecked,
generated all four static pages, and emitted the
`/api/copilotkit/[[...slug]]` dynamic route.
- PASS — npm lock/install truth: `npm ci --legacy-peer-deps
--ignore-scripts` installed `1,234` packages from `package-lock.json`;
`npm ls @ag-ui/client @ag-ui/core @ag-ui/encoder @ag-ui/proto --all`
showed every instance deduped/overridden to `0.0.58`.
- PASS — no-cache container gate: `docker build --no-cache -f Dockerfile
-t codex-mcp-apps-pr .` completed `npm ci`, the in-image Next production
build, image export, and unpack on `node:20-slim` with exit `0`.
- PASS — scope/secret/type hygiene: the final PR changes only
`examples/showcases/mcp-apps/**`; high-confidence secret patterns, type
suppressions, swallowed catches, and floating calls were absent from the
staged diff.
- PASS — branch commit: `db929211253c5f6e4a9567833bdb0d4b6e7c7d8c`
(`fix(showcases): align MCP Apps runtime graph`), following the approved
deployability commit without modifying its Railway file.

## Non-blocking warnings

- npm reports 10 transitive audit findings (5 low, 1 moderate, 4 high);
this compatibility change does not force unrelated breaking upgrades.
- The Node 20 container install emits `EBADENGINE` warnings for
transitive `@azure/*`, `@typespec/ts-http-runtime`, and `openai@7.5.0`
packages that declare Node 22, but the clean Node 20 install and
production build both complete successfully. The existing `node:20-slim`
base is intentionally unchanged because the gate did not reproduce a
failure.
- Local Next builds inside the monorepo warn about the root pnpm lock
plus the standalone npm lock; the standalone build still completes
successfully and Docker uses only the showcase-local npm lock.
2026-08-25 12:01:09 -05:00
Ben Taylor 1ebeae46c2 fix(world): preserve LangGraph assistant config (#6631)
# fix(world): preserve LangGraph assistant config

## Summary

- restore the World demo's browser-supplied OpenAI key under
`assistantConfig`
- preserve the behavior of the authoritative legacy World commit
- add a dependency-free migration contract test

## Verification

- rebased cleanly onto current `main` at
`c2abbea9cf6a48c22b1dcd19e9dc469d5cfd458f`
- verified the authoritative legacy source still uses `assistantConfig`
at `markmdev/copilotkit-world@7a27a37b628b9e339d1284ec42382008e67884f5`
- verified `@ag-ui/langgraph@0.0.7` declares, stores, and merges
`assistantConfig`
- regression check against current `main` failed for the expected
missing-`assistantConfig` reason; the branch's dependency-free Node test
passed (`1/1`)
- `oxfmt --write` plus `oxfmt --check` passed on both changed files
- Nx reported no affected lint targets; direct `oxlint` completed with
`0` errors and one pre-existing `NextRequest` type-import warning on the
unchanged import line
- root dependency bootstrap passed with the repository-pinned pnpm
`10.33.4` and `--frozen-lockfile --ignore-scripts`
- `nx run @copilotkit/runtime-client-gql:build` and its 13 dependency
builds passed; this does not make that package resolvable from the
independently managed nested ChatKit Studio workspace
- exact-base and branch World typechecks used the same pnpm `9.15.0`
provisioning: base reports seven diagnostics, branch reports six, and
the branch-only diagnostic count is zero
- the removed base diagnostic is the changed route's invalid `config`
property; all six remaining diagnostics are identical current-`main`
errors in unchanged `page.tsx`, `useCountryData.ts`, and
`countryData.ts`
- both production builds compiled the optimized application source; base
then failed on the invalid `config`, while the branch advanced past the
changed route and stopped on the pre-existing undeclared
`@copilotkit/runtime-client-gql` import
- Python agent compilation passed
- worktree is clean; commit `a25941f9017fcd2a0a8e7660fe768c69cfaa28b6`
changes only the World route and its migration contract test

## Existing baseline debt

The branch introduces no type or production-build regression and removes
the route error it targets. Six unrelated current-`main` type
diagnostics remain:

```text
src/app/page.tsx(14,42): error TS2307: Cannot find module '@copilotkit/runtime-client-gql'
src/hooks/useCountryData.ts(2,50): error TS2307: Cannot find module 'geojson'
src/hooks/useCountryData.ts(4,51): error TS2307: Cannot find module 'topojson-specification'
src/hooks/useCountryData.ts(32,27): error TS2345: Argument of type 'unknown' is not assignable to parameter of type 'string'.
src/hooks/useCountryData.ts(43,12): error TS7006: Parameter 'country' implicitly has an 'any' type.
src/utils/countryData.ts(1,40): error TS2307: Cannot find module 'geojson'
```

The nested ChatKit Studio frozen install also reproduces the separate
current-`main` Playground importer drift covered by D006. D007 no longer
changes `package.json`, so it does not duplicate that lockfile fix.

Legacy source: `markmdev/copilotkit-world` at
`7a27a37b628b9e339d1284ec42382008e67884f5`.
2026-08-25 12:00:59 -05:00
Ben Taylor 96773e56dc fix(showcases): sync chatkit studio lockfile (#6630)
# fix(showcases): sync chatkit studio lockfile

## Summary

- remove stale ESLint importer entries from the shared ChatKit Studio
lockfile
- restore frozen-lockfile validation for Playground, Studio, and World
without reserializing the lockfile
- keep the change limited to the shared dependency contract required by
the Playground deployment

## Verification

- Formatter: N/A for the changed YAML lockfile. The repository's oxfmt
0.36.0 does not accept YAML targets (`Expected at least one target
file`); its full-repository check listed only 28 pre-existing files
outside this branch's one-file diff.
- YAML parsing and structural validation passed: lockfile version 9.0,
four exact manifest importers, 1,431 packages, and 1,431 snapshots.
- The shared four-project workspace completed `pnpm install
--frozen-lockfile --ignore-scripts` with repository-pinned pnpm 9.15.0;
the resolution step was skipped because the lockfile is current.
- Playground and Studio passed explicit `tsc --noEmit`; their Next.js
production builds also passed. Both Python agent modules compiled
successfully, and the workspace defines no JavaScript test suite.
- Lint is N/A for this lockfile-only diff: Playground and Studio's
existing `next lint` scripts prompt to create an ESLint configuration,
while World defines no lint script.
- World's existing source/dependency type errors reproduce in both `tsc
--noEmit` and `next build`; they are unrelated to the removed
ESLint-only importer metadata. World otherwise compiled before its
existing type-validation failure.
- Diff, scope, secret, and worktree hygiene passed: the commit changes
only `examples/showcases/chatkit-studio/pnpm-lock.yaml` with 24
deletions and no additions.
2026-08-25 12:00:43 -05:00
Lukas Moschitz 295f75495e feat(examples): give the inspector lab a dark mode like the react demo
The launcher floats over a customer's page, and its border only earns its
place against a dark one -- so reviewing this branch needs a dark host page,
and the lab had no way to produce one.

Copied from `examples/v2/react/demo` rather than invented: the host owns a
theme state, and `CopilotChat` gets `className="dark"`, which is what makes
the package swap its own variable set. My first attempt stripped every
background instead, which is why the chat bubble, the send button, the
toolbar's on/off states and the error banner all vanished into one flat
grey -- the chat paints its own surfaces and has to be told, not undressed.

The colours are the demo's, by another route: it writes the oklch literals
that CopilotKit's variables use, and those are Tailwind's neutral steps --
`neutral-950` is `oklch(0.145 0 0)`, `neutral-50` is `oklch(0.985 0 0)`,
`neutral-800` is `oklch(0.269 0 0)`. Measured identical on the running lab.

`@custom-variant dark (&:is(.dark *))` is needed because Tailwind v4 points
`dark:` at `prefers-color-scheme` by default, so the toggle would have been
ignored in favour of the OS. Same declaration the package uses for its own
sheet.

The toggle sits top left, where the react demo puts it. Top right is where
the launcher floats.
2026-08-25 17:42:01 +02:00
Tyler Slaton d7ac976636 fix(web-inspector): preserve independent error signals 2026-08-25 13:46:59 +02:00
Lukas Moschitz 306ddaa5df Merge remote-tracking branch 'origin/main' into lukas/oss-903-presentation-wire-errors-notifications-to-emanate-from-and
# Conflicts:
#	packages/web-inspector/src/lib/__tests__/telemetry.test.ts
#	packages/web-inspector/src/styles/generated.css
2026-08-25 12:09:52 +02:00
Alem Tuzlak ba4260ad66 feat(web-inspector): add Event Snippets and save-as-snippet (#6649)
Open Inspector Event Snippets on localhost. You can compile, save, and
replay AG-UI events in chat. Chat shows a bookmark icon next to a tool
call, an A2UI block, or generative UI. Click the icon to save that turn
as a snippet.

## What does this PR do?

This PR adds the Inspector Event Snippets pane.

You can:

- Compile a snippet from a recipe (tool-call, reasoning, text, activity,
raw)
- Save snippets in origin-scoped localStorage
(`cpk:inspector:event-snippets`)
- Import and export snippets from the pane header
- Replay a snippet into live chat through Inspector-only Core inject

Each Run remints `messageId`, `parentMessageId`, `toolCallId`, and
`runId`. The second Run of the same snippet is a new turn.

On localhost, chat shows a bookmark icon beside a tool call, A2UI block,
or generative UI. The icon is absolutely positioned. It hangs to the
right when there is room. Otherwise it hangs to the left. The card stays
full chat width.

The React demo adds `sayHello`, `getTime`, `addNumbers`, and a **Call 3
tools** suggestion.

## Related PRs and Issues

- Linear
[OSS-874](https://linear.app/copilotkit/issue/OSS-874/new-features-also-allow-users-to-emit-specific-events-from-the)

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)

## Testing

### Commands run

1. Lefthook pre-commit ran `nx` targets `test`, `publint`, and `attw`
for 27 affected projects. All passed.
2. I did not run `pnpm test:pr` (full repo). Lefthook ran the affected
package matrix only.

### Manual test

1. Run `pnpm demo:react` from the repo root.
2. Open http://localhost:3000
3. Open Inspector and select Event Snippets.
4. In chat, click **Call 3 tools**. Confirm three tool cards at full
chat width, with the bookmark hanging outside the card.
5. Click a bookmark, then click Run twice. Chat shows a second turn with
new IDs.

### How this PR makes testing easy

- `packages/web-inspector/src/lib/__tests__/event-snippets.test.ts`
- `packages/core/src/__tests__/inspect-inject.test.ts` (covers two
injects)
- React demo: `examples/v2/react/demo/src/app/page.tsx`

## Linked issues

Linear
[OSS-874](https://linear.app/copilotkit/issue/OSS-874/new-features-also-allow-users-to-emit-specific-events-from-the)

## Risk / rollback

- If ID remint is wrong, a second Run can no-op or duplicate a turn.
- The save icon shows on localhost Inspector (or when `showDevConsole`
is `true`).
- Rollback: revert this PR.

## Public API change

**Before**

Angular has no Inspector service.

```ts
// no CopilotInspector export from @copilotkit/angular
```

**After**

```ts
import { CopilotInspector } from "@copilotkit/angular";

const inspector = inject(CopilotInspector);
inspector.openInspector({
  messageId: "msg-1",
  menu: "event-snippets",
});
```

React and Vue apps that already mount Inspector on localhost need no new
caller code. Chat wires the bookmark through Inspector context.

`@copilotkit/core` exports `ɵinjectInspectorEvents` for Inspector only.
App code must not call it. There is no public Core emit API.
2026-08-25 09:39:37 +02:00
Mike Ryan db88826432 chore: rename Enterprise Intelligence product copy 2026-08-24 09:38:15 -07:00
Lukas Moschitz 4a2b1a3a6e fix(examples): type the react-router lab's error handler and drop an undeclared import
Two type errors the example could not see. It has no check-types target,
and the import is type-only, so esbuild erases it and the build passes.

- `CopilotKitCoreFriendsAccess` came from `@copilotkit/core`, which the
  example does not depend on. `react-core/v2` re-exports it.
- `onError` also accepts React's DOM error handler, so the parameter is a
  union and reading `.error` / `.context` off it is not allowed. Narrow on
  the CopilotKit shape first; a synthetic DOM event has nothing to report.

Example type errors go from 9 to 3. The three left are older: two react
-router codegen paths and one model name.
2026-08-24 16:34:18 +02:00
Alem Tuzlak 4bd576d4ea Merge branch 'main' into alem/oss-874-inspector-event-snippets 2026-08-24 15:48:38 +02:00
Alem Tuzlak 0c6f52fc41 feat(examples): add crash-the-tool and crash-the-run inspector lab
Add chat phrases and lab buttons that fail a frontend tool or emit RUN_ERROR.
Clear the Break threads cookie on load so a refresh does not keep a fake
thread-list failure.
2026-08-24 15:22:07 +02:00
Martha Kelly Schumann dfac2c6347 Merge branch 'main' into codex/ent-1157-shared-clerk-session 2026-08-21 11:25:41 -07:00
Benjamin Taylor 6e0f5773f0 Merge remote-tracking branch 'origin/main' into ben1/v2-runtime-migration
# Conflicts:
#	showcase/shell-docs/src/content/snippets/shared/generative-ui/a2ui.mdx
2026-08-21 12:49:37 -05:00
Alem Tuzlak 76c8e23a0b feat(web-inspector): add Event Snippets and save-as-snippet
Developers can compile, save, and replay AG-UI events from Inspector.

Localhost chat can save a live turn as a snippet.
2026-08-21 19:39:18 +02:00
Tyler Slaton 7bff49739e fix(runtime): replace retired Anthropic model IDs (#6616) 2026-08-21 09:06:33 -07:00
Benjamin Taylor b53edc0a6e docs: drop the hono dependency and the deprecated endpoint alias
The earlier commits in this PR taught `createCopilotEndpoint` paired with
`handle` from `hono/vercel`, and added `hono` to 20 install commands with a
callout explaining why readers must install it. Both were wrong, and the second
was a consequence of the first.

`createCopilotEndpoint` is a **deprecated alias**. This repo's own handler table
says so — `docs/backend/runtime-endpoints.mdx`:

    | Deprecated                          | Use instead                |
    | `createCopilotEndpoint`             | `createCopilotHonoHandler` |
    | `createCopilotEndpointSingleRoute`  | ... with mode: "single-route" |

`createCopilotRuntimeHandler` serves the same multi-route mode (it is the
default), returns a plain fetch handler, is not deprecated, and needs **no hono
at all**. So the route collapses to:

    const handler = createCopilotRuntimeHandler({
      runtime,
      basePath: "/api/copilotkit",
    });

    export const GET = handler;
    export const POST = handler;

`hono` was therefore an artifact of the shape, not a requirement of the library.
The install lines and the callout are reverted; nothing tells readers to install
it any more.

## Verified with hono deleted, not merely absent from package.json

`examples/shadcn` converted to this shape, `hono` removed from its
`package.json`, and `node_modules/hono` deleted outright so a hoisted copy could
not mask the result:

    GET  /api/copilotkit/info              -> 200
    POST /api/copilotkit/agent/default/run -> 200, chat turn rendered
    tsc --noEmit / eslint / next build     -> clean
    next build route                       -> ƒ /api/copilotkit/[[...slug]]

The doctest sidecar drops `hono` too, so the CI gate now typechecks the
canonical snippet against `@copilotkit/runtime` alone — proof by construction
that the snippet needs nothing else.

    pnpm tsx scripts/doc-tests/run.ts   -> 2 passed, 0 failed
    showcase/shell-docs: typecheck      -> exit 0
    showcase/shell-docs: build          -> exit 0
    structural audit: 31/31 mdx files, fence + JSX identical to HEAD

## Also corrected

`docs/backend/custom-agent.mdx` repeated the same incorrect transport claim the
earlier commit fixed in four other places ("Both `<CopilotKit>` and
`<CopilotKitProvider>` negotiate the transport when the prop is omitted").
Corrected to match released behaviour.

## Left alone deliberately

`snippets/shared/backend/custom-agent.mdx` and `docs/backend/custom-agent.mdx`
still call `createCopilotEndpoint` in three fences each, as
`export default copilotEndpoint` — the Hono-app deployment pattern rather than a
Next.js route handler. That predates this PR, the documented replacement is
`createCopilotHonoHandler`, and I have not run that shape. Recorded as follow-up
rather than guessed at. (The two files have also drifted from each other, which
is a separate problem.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 08:32:30 -05:00
Benjamin Taylor 0be2aac315 fix(examples): move shadcn onto the v2 multi-route runtime endpoint
`examples/shadcn` paired a v1, POST-only runtime route with a
`@copilotkit/react-core/v2` frontend. `GET /api/copilotkit` answered 405 and
`GET /api/copilotkit/info` did not route at all (404), so nothing could probe
the runtime.

Convert the route to the v2 multi-route shape at a catch-all path, exporting
both verbs:

  app/api/copilotkit/[[...slug]]/route.ts
  createCopilotEndpoint({ runtime, basePath: "/api/copilotkit" })
  export const GET = handle(app)
  export const POST = handle(app)

Two co-changes this shape requires, both found by running the app rather than
by reading it:

- `hono` becomes a direct dependency. It is a dependency of
  `@copilotkit/runtime`, not a peer, so under pnpm's strict layout
  `import { handle } from "hono/vercel"` does not resolve from the app
  without declaring it.
- the provider must pass `useSingleEndpoint={false}`. The published
  `<CopilotKit>` from `@copilotkit/react-core/v2` defaults to the
  single-route transport, which posts a single-route envelope to the bare
  basePath; a multi-route runtime answers that with 404. The runtime says so
  itself in the error body.

`@copilotkit/*` moves 1.61.2 -> 1.68.3 so the app runs the versions a reader
installing today would get.

Verified live (aimock on :4010 as the model backend):
  GET  /api/copilotkit/info              -> 200, runtime /info payload
  POST /api/copilotkit/agent/default/run -> 200, chat turn renders
  before: GET /api/copilotkit -> 405, GET /api/copilotkit/info -> 404
  tsc --noEmit, eslint, next build all clean

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 08:32:29 -05:00
Rainer Hahnekamp 053b136634 fix(angular): retain lucide for demo inspector 2026-08-21 15:19:22 +02:00
Rainer Hahnekamp f0275a4431 chore(angular): migrate demo icons to @lucide/angular 2026-08-21 15:18:03 +02:00
Rainer Hahnekamp d248f0a7f9 chore(angular): remove empty component imports 2026-08-21 15:18:03 +02:00
github-actions[bot] 8b8097508e style: auto-fix formatting 2026-08-21 15:18:02 +02:00
Rainer Hahnekamp 79c235035a Apply Angular 22 framework migrations 2026-08-21 15:18:02 +02:00
Rainer Hahnekamp fea1a13b58 Upgrade Angular toolchains to version 22 2026-08-21 15:18:01 +02:00
Jerel John Velarde db92921125 fix(showcases): align MCP Apps runtime graph 2026-08-20 22:28:27 -07:00
Jerel John Velarde a25941f901 fix(world): preserve LangGraph assistant config 2026-08-20 21:35:36 -07:00
Jerel John Velarde e0528d379e fix(showcases): sync chatkit studio lockfile 2026-08-20 21:10:08 -07:00
Jerel John Velarde 0daa38f9a7 fix(showcases): make MCP Apps deployable 2026-08-20 21:09:44 -07:00
Tyler Slaton 22ca41fa1b fix(examples): default Anthropic integrations to Opus 4.8 2026-08-20 16:39:46 -07:00
Dusty d7d774fc9b Auto-merged main into codex/ent-1157-shared-clerk-session on deployment. 2026-08-20 13:57:57 -07:00
David McKay f3f32aafff fix(showcase): tighten the offsite-expenses beat
The "Sort out my offsite expenses" beat had three problems on stage: the
harness console was a black slab in a light-mode transcript, the tool
activity grew a stack of finished steps that pushed the report card off
the screen, and the run took a full minute.

Console (banking skin):
- Every colour is now a semantic token, so the pane follows the app into
  dark or light instead of being hardcoded dark. It was `bg-ink` with
  `text-white/45`-style overlays, which only ever looked right in one mode.
- Collapsed by default. The status strip still carries the live state, so
  the run reads as alive while closed.
- Still the FULL log when open. It is the detail view, and windowing it as
  well left two lines and nowhere to read the rest.

Tool activity (shell, all skins):
- Rolls to the last two lines; older ones are REMOVED, not collapsed.
- Registration is a layout effect, and that is load-bearing. A new line
  renders before it is registered, and registering is what evicts the
  oldest, so with a passive effect the browser painted the in-between
  state: three rows for one frame on every tool call, and again when the
  end-of-run MESSAGES_SNAPSHOT remounts every line at once. Measured per
  animation frame over a full run: 12,734 frames, never more than two.
- A shared registry rather than something simpler because CopilotKit
  renders one component per tool call and owns the container. CSS
  `:nth-last-child` needs siblings and the lines had one parent each;
  mount-order counters drift across the snapshot remount.

Agent (run time 1m 0s -> ~45s):
- Research is gated on the offsite dates. A charge dated outside the
  window is settled by its date whatever the merchant turns out to be, so
  half the researcher dispatches were buying nothing. Travel on the
  adjacent days is still kept in scope.
- Filings go out in one command instead of one curl per row. The
  researchers already ran concurrently, so the serial per-row round-trips
  through the model were most of the wall clock, not the research.
- Fetch and verify are one command; there is nothing to decide between
  the halves.
- Analyst reasoning effort defaults to medium, overridable with
  BANKING_EXPENSE_EFFORT.

Filing is idempotent, which the batching made necessary: the script got
run twice and every charge was filed twice, so the report card claimed six
filings while the ledger held twelve. The script now writes `filed.json`
and exits early if it exists. That marker is cleared once per run, because
the workspace is a fixed directory shared by every run and a stale marker
would convince the next demo it had already filed and post nothing at all
— the same bug wearing the opposite mask, and a quieter one, since a run
that files nothing still writes a confident report.

Deduping server-side on merchant+amount would have been wrong: Hotel
Verrano legitimately appears twice at the identical 318.55 for the two
nights of the offsite.

Does this change make anything in .claude/skills/reskin/ wrong,
incomplete, or misleading? No. Nothing there documents the harness
console, the tool-activity renderer, or the analyst prompt; its only
matches for "console" are `console.error` in unrelated template code.

Verified on the real path against a live Intelligence stack: six rows
filed, all status=pending, stable across 60s of polling, and present in
the Pending Approval queue with their notes and approve/decline actions.
2026-08-20 13:09:25 -07:00
Mark 0298616223 docs: add Claude Managed Agents cookbook (#6430)
## Summary

- add an independently runnable Claude Managed Agents finance-assistant
example
- add a cookbook recipe that explains the CopilotKit runtime,
managed-session mapping, and tool rendering flow
- add the recipe to cookbook navigation, the overview grid, sidebar icon
mapping, and render coverage
- use the real Claude vector mark for the cookbook instead of the
text-placeholder SDK asset
- register the example's Vite configuration and managed-agent model with
the repository CI allowlists
- include a compact architecture diagram and links to the relevant
rendering and CopilotKit Intelligence documentation
- disable Claude's complete built-in toolset and expose only the scoped
`show_growth_projection` runtime tool
- make the provisioning model configurable through `ANTHROPIC_MODEL`,
defaulting to `claude-fable-5`
- bound CopilotKit request bodies to 256 KB and managed-agent turns to
90 seconds, while relying on the adapter's per-thread serialization
- cap public run traffic at 20 provider-like attempts per client IP per
minute and 2,000 successful starts per process per 24-hour window
- restrict browser runtime requests with an exact Origin allowlist that
supports same-origin or separately hosted frontends, and restrict iframe
parents with CSP `frame-ancestors`
- validate persisted managed-agent IDs at startup so malformed local
configuration fails immediately
- publish the interactive example on Railway and embed the live
deployment in the cookbook
- align the demo with the existing cookbook chat styling and show the
`Project monthly investing` starter on first load

## Demo

![Claude Managed Agents finance assistant rendering an interactive
growth
projection](https://github.com/CopilotKit/CopilotKit/blob/f36cb2b8eee8084aa6e37a200175532f4ecdca02/examples/showcases/claude-managed-agents/demo.png?raw=1)

## Why

This gives developers a focused example of connecting CopilotKit to
Anthropic Claude Managed Agents without the extra surface area of a
larger analyst application. The recipe follows the existing cookbook
structure and keeps the live demo compact enough for the standard
cookbook pane. Its managed environment has no outbound network or
package-manager access, and its agent cannot use bash, filesystem,
search, or fetch tools.

The request, turn, per-IP, and process-wide limits bound public demo
traffic without adding authentication or user friction. The traffic
counters are intentionally in memory, reset on process restart, and are
not shared across replicas, so the dedicated Anthropic workspace spend
limit remains the durable cost backstop. The exact-Origin browser check
reduces drive-by use but is explicitly documented as a control rather
than authentication. The model override allows operators to select a
lower-cost supported model during provisioning without editing source
code.

## Validation

- scoped formatting: passed
- scoped lint: 0 warnings, 0 errors
- shell-docs typecheck: passed
- standalone example typecheck: passed
- docs render tests: 26/26 passed
- standalone example tests: 23/23 passed
- shell-docs tests: 375/375 passed
- shell-docs production build: passed (222/222 pages)
- standalone example production build: passed
- standalone npm lockfile validation: passed
- build-config allowlist validator: passed
- docs model-name validator: passed
- exact-Origin regression coverage for run requests plus headerless
same-origin runtime discovery: passed
- malformed persisted agent-ID regression coverage: passed
- live Railway root and iframe CSP: passed
- live Railway runtime discovery, exact welcome copy, and first-load
starter pill: passed
- live three-turn AG-UI managed-agent run with `show_growth_projection`:
passed
- cookbook verified in the browser at desktop and narrow widths with no
console errors or horizontal overflow
2026-08-20 12:07:52 -07:00
Mike Ryan b8b19834a2 fix(runtime): unify the Intelligence key name and publish the wiring (refs OSS-881) (#6595)
## What does this PR do?

Closes the naming and documentation half of
[OSS-881](https://linear.app/copilotkit/issue/OSS-881). Paired with
CopilotKit/Intelligence#890, which adds `copilotkit verify` and tightens
the evaluation rubric.

### 1. One name for the Intelligence key

**Three** names for one value were live in CopilotKit's own
documentation, and following the wrong one with a CLI-provisioned
project yields an undefined key:

| Name | Where | Code readers |
| --- | --- | --- |
| `INTELLIGENCE_API_KEY` | what `copilotkit project select` writes; all
34 integration examples; the docs site | 34 |
| `COPILOTKIT_INTELLIGENCE_API_KEY` | 7 Channels package READMEs +
packaged skills | **0** |
| `COPILOTKIT_API_KEY` | `examples/slack`, `examples/teams`, and the
TSDoc on `CopilotKitIntelligence` itself | 2 |

`INTELLIGENCE_API_KEY` wins — it is the name the CLI provisions, and
changing it would break every scaffolded project in the wild.

- `COPILOTKIT_INTELLIGENCE_API_KEY` is **retired outright**. Nothing
ever read it, so there is nothing to keep compatible.
- `COPILOTKIT_API_KEY` stays **readable as a deprecated alias** in the
two examples that consume it, so an existing `.env` keeps working, and
is documented as deprecated everywhere it appears.

The third name was the worst placed: it was in the TSDoc on
`CopilotKitIntelligence`, which is what an IDE shows on hover.

This was not only untidy. The CLI's own `channels-preflight` accepts
`INTELLIGENCE_API_KEY` or `COPILOTKIT_API_KEY` — **not**
`COPILOTKIT_INTELLIGENCE_API_KEY`, the name the Channels READMEs told
people to set. So following a Channels README verbatim made `copilotkit
channels` warn that no runtime API key was present while the key sat
visibly in `.env`. After this PR the documented name is one preflight
accepts.

> [!NOTE]
> `NEXT_PUBLIC_COPILOTKIT_API_KEY` is a **different value** — the legacy
Copilot Cloud public key — and is deliberately left alone.

### 2. A real defect, not just naming skew

`skills/runtime/references/intelligence-mode.md` documented
`organizationId` as a `CopilotKitIntelligence` option, sourced from two
further env names (`COPILOTKIT_INTELLIGENCE_ORG_ID`,
`COPILOTKIT_ORG_ID`).

`CopilotKitIntelligenceConfig` has no such field — the copy-pasteable
sample it appeared in **would not compile**. Removed from the samples,
and the prose telling readers to fetch a value for it corrected. That
file is the only place those two names ever existed, which is very
likely why the failing validation run reported that "the runtime reads
`COPILOTKIT_INTELLIGENCE_API_KEY` and `COPILOTKIT_INTELLIGENCE_ORG_ID`".

### 3. Publish the Intelligence wiring

The wiring instructions existed only inside
`node_modules/@copilotkit/runtime/skills/`, and the only docs pages
mentioning `CopilotKitIntelligence` at all were the two Channels
frontends — so a developer on the plain web path had no page to reach it
from.

Adds **`/premium/connect-your-runtime`**: the wiring itself, how to
confirm the credential is actually consumed, the self-hosted
both-URLs-or-neither rule, and a troubleshooting table. Linked into both
navs, and the skills reference now points at the published page.

### 4. A guard so it cannot drift back

`scripts/validate-intelligence-env-names.ts` (`pnpm
check:intelligence-env-names`), wired to lefthook and a new workflow.

The workflow is **intentionally unfiltered**. The two workflows that
would otherwise cover this both filter: `plugin-skills-check` by
`paths:`, and `static/quality` by `paths-ignore: examples/**` — which is
exactly where the deprecated alias lives. Scoping the job would re-open
the hole it exists to close. Legitimate alias sites live in
`ALIAS_ALLOWLIST`.

## Related PRs and Issues

- [OSS-881](https://linear.app/copilotkit/issue/OSS-881) — needs
**both** PRs; neither closes it alone
- CopilotKit/Intelligence#890 — items 1 and 4 (`copilotkit verify` +
rubric contract 1.3.0)

## Verification

- Full lefthook pre-commit ran green: `check-plugin-skills`, `lint-fix`,
the new `check-intelligence-env-names`, and `test`/`publint`/`attw`
across **25 projects**.
- `examples/slack` `managed.test.ts` extended to cover **both** the
canonical name and the alias fallback, and proven non-vacuous — removing
the fallback turns the new test red.
- The drift guard proven non-vacuous the same way: reintroducing a
retired name fails it, exit 1.
- `oxfmt` and `oxlint` clean on every file touched (0 errors).

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-20 12:00:52 -07:00
Mark f36cb2b8ee fix(showcase): validate persisted setup IDs 2026-08-20 11:55:46 -07:00
Mark 65f07610a1 fix(showcase): allow runtime discovery request 2026-08-20 11:43:50 -07:00
Mark d50e8d7e7c Merge branch 'main' into codex/claude-managed-agents-cookbook 2026-08-20 11:40:54 -07:00
Mark ff060eb97b feat(showcase): polish managed-agent cookbook demo 2026-08-20 11:38:26 -07:00
Mark 7089b3cf53 fix(showcase): harden managed-agent deployment setup 2026-08-20 11:01:06 -07:00
David McKay 35ea9eee36 feat(reskinnable-demo): run banking on a nested LangChain deep agent with a streaming CLI console (#6581)
Replaces the Codex CLI harness with **LangChain deep agents (Python)**:
sandboxed shell execution, parallel research subagents, no external
harness binary, our own API key.

All ten beats have been walked in a browser, and the demo now has its
own CI job. **Ready for review.**

## What banking's agent is now

```
banking                       gpt-5.4, temp 0
  │  banking's own prompt · the browser's frontend tools · Intelligence
  │  memory tools · render_report for the canvas
  └─ expense-analyst          gpt-5.6-sol, reasoning_effort=high
       │  sandboxed shell (LocalShellBackend) · submit_expense_report
       └─ merchant-researcher  gpt-5.4, one per merchant
            search_merchant (Tavily)
```

Six skins still run in-process as `BuiltInAgent`s. `banking` is an
`HttpAgent` pointed at `agent/`.

Each level exists because something about it must differ from its parent
— prompt, model, reasoning effort, tool set. A single agent had nowhere
to put any of that.

### Why the whole agent moved, not just the expense beat

The obvious design was a second agent id for the long-running beat,
leaving banking's `BuiltInAgent` alone. That was built first, and it
does not survive the requirement *"start the analysis, switch threads,
run other pills, come back to it."*

**Threads are scoped per agent.** `listThreads` takes `agentId` as a
required parameter, and measured against the running app the two lists
were disjoint — `banking` 46 threads, `banking-expenses` 10. The v2
runtime has no handoff mechanism, and `defineTool`'s `execute` takes
`(args)` with no emitter, so a tool cannot stream a multi-minute run
into the conversation. One conversation list means one agent.

### Why the beat is a subagent rather than prose in one prompt

It started as a section appended to banking's prompt. That left per-beat
configuration homeless (model, effort and recursion limit are all
agent-level, and there was one agent), and made banking's
~21,000-character rulebook ride every one of the ~20 model calls the run
makes — re-sending rules about markdown tables while the agent read a
CSV.

Reached as a `CompiledSubAgent`, because a raw `SubAgent` spec has no
`subagents` field and this one needs its own: the per-merchant fan-out
is a headline of the beat, and a flat subagent could only research
serially. A probe confirmed nesting survives — the analyst's `task`
dispatches, the researchers' `search_merchant` calls and the final
report tool all reach `astream_events`.

## The console

One CLI window in the transcript carries the whole run: narration, `$
execute`, `search "…"`, `→ merchant-researcher: …`, results, indented by
depth so a ten-way fan-out reads as a fan-out. Subagent narration is
suppressed from the conversation so the console is the single place the
harness is visible; banking's own replies still render normally.

`shell/subagents/subagent-activity.tsx` subscribes to the agent's
**event stream**. Reading `agent.messages` — the first design — was
wrong twice: messages materialise at the `MESSAGES_SNAPSHOT` (two per
run), so the pane sat still for minutes then filled at once; and
persisted messages carry no `subagentRunId`, so the harness's narration
could not be told from banking's reply. The fold is pure and idempotent,
so the same code serves the live subscription and a replay of the
thread's stored events when a conversation is reopened.

Three heuristics were deleted along the way, each replaced by identity
the protocol already carried:

| heuristic | replaced by |
|---|---|
| console anchored on "the first tool call" | the run's first `task`
call, from **message order** (durable; the event-derived version
rendered one console *per delegation* on a restored thread — six,
measured) |
| `CONSOLE_TOOL_NAMES` allowlist | presence of `subagentRunId` |
| `disable_streaming` on the researchers | the canary's per-lane state |

The message filter suppresses **prose** and keeps **tool calls**: an
agent routinely narrates and calls a tool in one message, and returning
`null` for the whole message hid the report card — the run looked
perfect and ended with nothing to show.

## Canary stack, contained to this app

The subagent surface only exists on the canary line, and a released
`@ag-ui/client` ≤ 0.0.57 rejects `SUBAGENT_*` events **in the HTTP
transport before any middleware runs**, killing the stream. So the demo
leaves the root pnpm workspace and ships its own lockfile:

- `@copilotkit/* 1.68.3-canary.1786716392`
- `@ag-ui/* 0.0.59-canary.1786716392.0`
- `ag-ui-langgraph 0.0.43.dev1786716392` → `ag-ui-protocol
0.1.20.dev1786716392`

That keeps an unreleased protocol out of every other package in the
monorepo.

A 1.62.2-based canary was tried first and **could not compile the app**:
`workspace:*` is not a version, so the demo had no recorded lower bound
on the CopilotKit API it needs, and pointing it at 1.62.2 silently
rewound that API five minors. `OpenGenerativeUIActivityRenderer` — a
public `/v2` export since ~1.63 — was the first thing to break, and
there was no reason to think it was the only one. Rebasing the canary on
1.68.3 collapsed that whole class of risk.

## Verified against the running app

Every row is a measurement, not a claim.

| | |
|---|---|
| Registry shape | `/info`: `banking` = HttpAgent, other six =
BuiltInAgent |
| Sandboxed execution | 8 `execute` calls; agent writes and runs its own
Python |
| Parallel subagents | 6 `SUBAGENT_STARTED`/`FINISHED` pairs, 10
`search_merchant` |
| Canvas beat | `render_report` → a2ui middleware emits `activityType:
a2ui-surface` |
| Frontend tools | given two, picks `showTransactions`, emits no result,
emits **no prose** — the prompt's restraint rule surviving the port |
| Memory (Intelligence) | run is handed `recall_memory`, `save_memory`,
`forget_memory`, `knowledge_base_shell` |
| HITL round-trip | tool call out, answer back in, agent continues |
| Durable background run | client disconnected at 8s; run finished
unattended; thread replayed |
| Thread restore | 61 messages persisted incl. the report tool (an
earlier flat-subagent attempt collapsed to 4) |
| Report correctness | 14 rows, 9 researched, 6 filed with ids read out
of real 201 bodies, totals reconciling against their own rows |
| Run duration | 86s reported vs 98s wall clock |

## Correctness bugs found by running it

Each produced a confident, complete-looking wrong answer rather than an
error — the characteristic failure of a multi-minute agentic beat.

- **Beat 3d was dead, and it took the whole thread with it.** The bet
against this one was right. `ag_ui_langgraph` routes *every* attachment
to the model as an `image_url` block, documents included, so the Q2
invoice was rejected before the first token: `400 Invalid MIME type.
Only image types are supported`. The exception is raised inside the
model node, which kills the SSE stream — the runtime sees `RUN_ERROR:
terminated` with no cause and the browser renders **nothing**: no error
bubble, no failed message. And the crashed run is still checkpointed, so
every later message on that thread replays the rejected content and dies
the same way. One click on the pill killed the conversation permanently;
only restarting the service cleared it. Fixed here by a
`wrap_model_call` middleware that rewrites those blocks into LangChain
standard `file` blocks, and upstream in
[ag-ui#2476](https://github.com/ag-ui-protocol/ag-ui/pull/2476) (both
adapters, plus the return leg so a non-image attachment survives
`MESSAGES_SNAPSHOT`). The middleware is a stopgap with its deletion
condition written into its header — this service installs the adapter
from PyPI, so the upstream fix cannot reach it until published.
- **Totals did not match their own rows.** Every per-row amount matched
the CSV while the headline totals came back $1.00 and $0.20 high: the
model authored them instead of adding them. The card prints the total
directly above the rows it is the sum of. Fixed structurally — derived
in `submit_expense_report`, removed as parameters.
- **`amount` arrived as a string** on all 14 rows, silently defeating
`toLocaleString(…currency…)` so it printed `842.10` with no currency.
- **`merchantKind` non-answers.** With no search tool the model wrote a
bare `"unclear"`; with Tavily live it hedges in prose (`"unknown (likely
bookbindery/bookshop retail, but not established for this exact
merchant)"`), which an exact-match filter passed into a 60-character
label glued to the merchant name. Now rejected on leading token, hedging
language, and a 40-character cap.
- **The run clock reported 333s for a two-minute run.** It took the
oldest open stamp across the process because the tool could not name its
own run; model calls *after* the report re-stamped it and the leftover
became the next run's start. Now keyed per run via an injected
`ToolRuntime`.
- **`graph.with_config({"recursion_limit"})` is silently dropped** by
the AG-UI adapter. The agent completed the entire analysis, streamed
every argument of the final report, then died at LangGraph's default of
25 supersteps.
- **A sync `wrap_model_call` under `astream`** surfaces as a bare
`RUN_ERROR: terminated`, cause only in the service log.
- **`emit_raw_events` defaults to `True`**, piggybacking LangChain's
internal events onto the stream: 27,950,261 bytes → 374,086 with it off,
identical report. Matters because the thread *persists* those events for
replay.
- **`gpt-5.6-sol` + `reasoning_effort` + function tools 400s** on
`/v1/chat/completions`; needs `use_responses_api=True`. The first model
probe missed it by binding no tools — a model probe for an agent has to
bind one.

## Upstream findings (reported separately, not fixed here)

1. **`@copilotkit/runtime` drops `subagentRunId` when persisting
messages.** 2888 of 3026 stream events carry it; 0 of 53 persisted
messages do. Reproduced with Intelligence removed entirely, so it is the
runtime's message shape rather than the platform store — and
`@copilotkit/runtime`'s dist contains no occurrence of the field, while
`@ag-ui/core`, `ag-ui-protocol` and `@copilotkit/core` all model it. One
field threaded through would let the console rebuild from message
history and delete the event-replay seeding here.
2. **`copilotkit` 0.1.95 × `ag-ui-langgraph` 0.0.43** — the FastAPI
endpoint calls `agent.clone()` per request; 0.0.43's base `clone()`
hard-passes three kwargs the SDK subclass does not accept, so **every
request 500s**. Verified with a minimal repro on stock classes and by
reading published wheels (0.0.41/0.0.42 are fine — the window is 0.0.43
only). `sdk-python` requires `>=0.0.42` unbounded, so fresh installs
break, and `examples/showcases/deep-agents{,-finance-erp,-job-search}`
are one `uv lock --upgrade` away. Now open as #6592 (`**kwargs`
passthrough + 9 regression tests incl. a forward-compat guard); it needs
a `0.1.96` bump to reach PyPI before `main.py`'s
`BankingAGUIAgent.clone()` workaround can go — and note `agent/uv.lock`
pins `copilotkit 0.1.95`, so removing the workaround is a re-lock as
well as a delete. That branch also fixes 4 pre-existing failures in the
sdk-python suite from the same root cause — **`test_unit-python-sdk` may
currently be red on `main`; worth checking independently.**

3. **Both LangGraph AG-UI adapters send non-image attachments as
`image_url`**, so a PDF, audio clip or video is rejected on the block
kind — see beat 3d in *Correctness bugs*. Fixed in
[ag-ui#2476](https://github.com/ag-ui-protocol/ag-ui/pull/2476); carried
here as a middleware until that publishes.

Also verified and **not** a problem: pnpm 10.33.4 still applies
`package.json` `pnpm.overrides` despite warning that it ignores them —
the ~70 root overrides including the security pins are live, nothing was
silently unpinned. (Migration to `pnpm-workspace.yaml` is worth doing
anyway, since the installed pnpm is 11.21.0 where the field genuinely is
dropped; branch exists, byte-identical lockfile.)

## What is not done

1. **Docs are stale.** `CLAUDE.md` still says `AgentRegistration` is `{
createAgent: () => BuiltInAgent }` and that the route "builds one
`BuiltInAgent` per registered skin"; the reskin skill says the same in
three places, and `templates.md` scaffolds it. The real type is `() =>
AbstractAgent`. This app has a standing rule that every change answers
whether the skill went stale. It did — the skill's launcher step is
fixed, the `BuiltInAgent` claims are not.
3. **Pre-release dependencies**, now on both halves: the JS canaries in
this app's own `pnpm-lock.yaml`, and `ag-ui-langgraph` /
`ag-ui-protocol` pinned `==` to the matching `.dev` canaries in
`agent/pyproject.toml`. Both move to stable when the subagent work
ships.
4. `pnpm test:e2e` has not been run.

### Closed since this PR was opened

- **The demo has its own CI job** —
`.github/workflows/test_reskinnable-demo.yml`. Nx discovers projects
*through* the pnpm workspace (no `workspaceLayout` in `nx.json`), so
leaving it also left the repo-wide `nx run-many -t build`
(`static_compat.yml`), `-t check-types` (`static_quality.yml`) and `-t
test` (`test_unit.yml`) sweeps, and both static workflows carry
`paths-ignore: ["examples/**"]` besides. One job runs the four gates
(lint, typecheck, 2460 unit tests, build); a second syncs
`agent/uv.lock` with `--frozen` and asserts the subagent *capability*
rather than a version string, so it keeps meaning something after the
pin moves.

The build gate is the one a developer can least run locally: `next
build` corrupts a concurrently running dev server's PostCSS/Turbopack
cache — measured, `globals.css` transforms to garbage, every route 500s,
and a dev-server restart does not clear it because the corruption is on
disk.

**Its first two runs failed, both usefully**, and both bugs were
pre-existing:

1. `pnpm/action-setup@v6.0.10` with `package_json_file:` pointed at this
app still installed the **root's** pnpm 10.33.4 rather than this app's
pinned 10.10.0 — the resolver that ignores `pnpm.overrides`, which is
the only place three of the five `@ag-ui/*` canaries are pinned. Caught
only because the job asserts `pnpm --version` against the pin; it would
otherwise have installed under the wrong resolver and stayed green. Now
activated through corepack, which reads the nearest `package.json`.
2. `pnpm install` in this directory **never installed this app**. pnpm
walks *up* for a workspace root, found the repo's, and installed all 70
monorepo projects (4645 packages) while leaving this directory with no
`node_modules` — the next command failing as `sh: 1: eslint: not found`.
So the app that ships its own lockfile was uninstallable by its own
documented instruction. Fixed by giving it its own
`pnpm-workspace.yaml`; `ignore-workspace=true` in an `.npmrc` does
**not** work (CLI-only in pnpm 10.10, measured).

- **The canary `overrides` now live in `pnpm-workspace.yaml`**, their
supported home, as a side effect of that fix. They no longer depend on
`packageManager: pnpm@10.10.0` being the version that still reads
`package.json` — which matters because `@ag-ui/core`, `@ag-ui/encoder`
and `@ag-ui/proto` are pinned nowhere else and a released `@ag-ui/core`
rejects `SUBAGENT_*` in the HTTP transport.
- **All ten beats walked in a browser**, including 3d — which was
broken, for the reason in *Correctness bugs* above.
- **`./run-demo.sh` starts a complete demo.** It now brings up the
Python agent between the compose wait and `pnpm dev`, guarded on
`/health` so a re-run reuses a live one, and dies with `(cd agent && uv
sync)` when the venv is missing. `./stop-demo.sh` had the mirror gap and
now stops it too — that one mattered more than it looks: the start
script *reuses* a live :8124, so an orphan surviving teardown is
silently adopted by the next cold start, serving whatever code it was
launched with.
- **The agent's Python deps are pinned and locked.** They were not, and
the JS half was — so a colleague's fresh `uv sync` resolved the
*release* `ag-ui-langgraph 0.0.43`, which does not accept
`emit_subagent_events` and ships no subagent symbols. That is this PR's
headline feature, and it would have failed silently: `main.py` sets the
flag as an attribute, so the assignment succeeds against an object
nobody reads and the service starts clean. Now `==` pins plus a
committed `agent/uv.lock`, verified by building a venv from only the
tracked files and replaying a captured Q2-with-PDF payload through it.
- **The README told people to install from the repo root** "as a
workspace package". It is deliberately not one, so a root install did
nothing for this app — and installing in the right place did nothing
either, until the `pnpm-workspace.yaml` above.

## Relationship to #6501

Based on `b94e4bfb5d`, the last commit before Codex appears, which is
**not on `main`**. So this PR carries **9 commits: the 4 here plus the 5
foundation commits it shares with #6501** — harness types + fixture,
prompt/workspace, the OFFSITE-to-fixture invariant guard, the
summary-shape/filing-contract fix, and `POST /transactions`. Whichever
merges first shrinks the other. #6501 and #6565 are deliberately
untouched.

## A demo-design question, not a bug

The fixture's merchant names are invented, so `Cardinal & Ash` — the
prompt's own worked example of *"could be a restaurant or a law firm,
find out"* — cannot be resolved by real web search and stays `unclear`
alongside `Bluebonnet Provisions`. The beat's headline claim is that the
agent researches every merchant; real merchant names in the CSV would
make that land harder.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01JRubZT6AS6LCGkcE2KzcfA
2026-08-20 10:49:40 -07:00
Maxim 6623dcfd4f fix(reskinnable-demo): make the app its own pnpm root
`pnpm install` in this directory did not install this app. Leaving the
root workspace's member list was only half of it: pnpm walks UP from the
cwd for a workspace root, found the repo's, and installed THAT — measured
in CI (run 32398378642), "Scope: all 70 workspace projects", 4645 packages
resolved for the monorepo, and this app left with no node_modules. The
next command then failed as `sh: 1: eslint: not found`, which reads as a
broken toolchain rather than an install that went elsewhere.

A `pnpm-workspace.yaml` here stops the walk. `ignore-workspace=true` in an
`.npmrc` does not — it is CLI-only in pnpm 10.10 (measured: the Scope line
was unchanged), so the alternative would have been a flag every human and
job had to remember.

The five canary `overrides` move into that file, their supported home.
They stay duplicated in `package.json`'s `pnpm` field for now, which is
read only because this app pins `packageManager: pnpm@10.10.0` — three of
the five (`@ag-ui/core`, `@ag-ui/encoder`, `@ag-ui/proto`) are pinned
nowhere else, and a released `@ag-ui/core` rejects SUBAGENT_* events in the
HTTP transport, so a packageManager bump would have silently killed the
harness console.

Verified read-only: `pnpm install --frozen-lockfile --lockfile-only` in
this directory resolves the single project, satisfies the committed
lockfile, and leaves it byte-identical.
2026-08-20 19:38:13 +02:00
Maxim cf9de905ac fix(reskinnable-demo): pin and lock the agent's Python canaries
A colleague cloning this branch could not reproduce the demo. The JS half
is pinned exactly — this app left the root pnpm workspace and ships its
own `pnpm-lock.yaml` with `@ag-ui/client 0.0.59-canary.1786716392.0` — but
the Python half pinned nothing: `ag-ui-langgraph>=0.0.43` and no
`uv.lock`, so a fresh `uv sync` resolved the RELEASE.

Measured, on the release that `>=0.0.43` actually selects:

    ag-ui-langgraph==0.0.43
      emit_subagent_events accepted by LangGraphAgent.__init__:  False
      subagent symbols in ag_ui.core:                            NONE

That is this branch's headline feature — the streaming CLI console — and
it would have failed SILENTLY. `main.py` sets `emit_subagent_events` as an
attribute (copilotkit's subclass takes only four kwargs), so on a release
without the feature the assignment succeeds, lands on an object nobody
reads, and the service starts clean. No `subagentRunId` reaches the
browser, the console cannot separate the harness's work from the parent's,
and a reopened thread collapses a multi-minute run to one tool message.
Every gate stays green.

So `ag-ui-langgraph` and `ag-ui-protocol` are now `==` pins, and
`agent/uv.lock` is committed. `ag-ui-protocol` is pinned as a DIRECT
dependency although nothing imports it by name: it carries the SUBAGENT_*
event types and the adapter asks only for `>=0.1.15`, so left transitive
it resolves the release and undoes the other pin.

README: the quick start said `pnpm install # from the repo root — this is a
workspace package`. It is not one — it is absent from
`pnpm-workspace.yaml`, deliberately, so the canary line cannot leak into
the rest of the monorepo. A root install therefore installs nothing for
this app, which is a confusing first five minutes for anyone who reads it
and follows it.

Verified by cold start rather than by inspection: copied ONLY the files
git tracks (the five .py modules, pyproject.toml, the new uv.lock) into an
empty directory, ran `uv sync --frozen`, and got
ag-ui-langgraph 0.0.43.dev1786716392 with `emit_subagent_events accepted:
True` and the five SUBAGENT symbols present. Then booted that venv on a
spare port and replayed the browser's captured Q2-with-PDF payload
through it: RUN_FINISHED, with createReport carrying the invoice's real
line items. The live stack was not touched.

Reskin skill: checked, no impact. Its install/verify step is `pnpm dev`
inside this app, which is correct either way; the root-vs-here distinction
is a README concern and the skill never mentions the workspace.
2026-08-20 17:49:18 +02:00
Maxim ffbb01be08 fix(reskinnable-demo): stop banking's agent in stop-demo.sh
The teardown mirror of the previous commit. `./stop-demo.sh` stopped the
dev server, the docker stack and the native TEI — never :8124 — so
banking's Python agent survived every teardown.

That leftover is not merely litter. `run-demo.sh` health-checks :8124
before starting (so a re-run reuses a live agent instead of colliding on
the port), which means the next cold start silently ADOPTS the orphan and
serves whatever code it was launched with. Edit `agent/`, re-run the
script, observe no change, conclude the edit did nothing.

No `--keep-agent` flag to match `--keep-tei`: TEI has that flag because it
is slow to warm, and the agent boots in seconds, so keeping it would only
reintroduce the failure above.

Also corrects two things in the same breath:

  * The Ctrl-C claim I got backwards one commit ago. MEASURED this time,
    with the same shell construct the script uses: SIGINT reaches the
    foreground process group, which the backgrounded children are still in,
    but a NON-INTERACTIVE shell sets background jobs to ignore SIGINT
    (POSIX) — so only the exec'd dev server dies (exit=-2) and the stack,
    TEI and agent all survive. `nohup` is not what saves them; that covers
    SIGHUP, a different signal. Both scripts and the README now say this.

  * `ok "docker stack down${PURGE:+ (volumes removed)}"` printed "(volumes
    removed)" on EVERY teardown, because the flag holds the string "0" when
    unset and `:+` expands on non-empty. The action was always right
    (`--volumes` is gated on `-eq 1`) — verified: the postgres/redis/minio
    volumes are still there after a flagless run — but the line told anyone
    reading it that their seeded data had just been deleted.

Verified with a full cycle through both scripts: `./stop-demo.sh
--keep-tei` reported the agent stopped and left the volumes in place, then
`./run-demo.sh` came back with `banking agent ready (200)` and `stack
healthy`. The idempotency guard was exercised against the live service and
reports "already up" rather than starting a second uvicorn.

Reskin skill: checked, no impact. It documents authoring a skin, not
running the stack; its one launcher line got the note it needed in the
previous commit.
2026-08-20 17:37:50 +02:00
Maxim b96a9449de fix(reskinnable-demo): start banking's agent from run-demo.sh
`./run-demo.sh` brought up the embedder, the Intelligence stack and the
dev server, then handed over an app whose DEFAULT skin could not answer a
single message. Banking's agent is a Python service (`agent/`, :8124) and
it is not a compose service, so it had to be launched by hand — and
nothing said so: no line in the script, no line in the README, no line in
any markdown in this tree.

The failure mode is the expensive kind. Nothing errors at startup: the
stack comes up healthy, the app boots, the dashboard renders off the REST
ledger, every pill is present. Only sending a message fails, and the six
in-process skins keep working, so the obvious reading is "my machine is
fine, the demo is broken".

The script now starts the agent between the compose wait and `pnpm dev`,
guarded on `/health` so a re-run reuses a live one instead of colliding on
the port, and dies with `(cd agent && uv sync)` when the venv is missing —
the same shape as the native-TEI branch above it.

Also corrects the Ctrl-C line, which claimed Ctrl-C "stops only the dev
server". Measured: the docker stack survives, and the dev server, the
native TEI and now the agent all go down with the script.

README: the quick start said `pnpm dev` and described OSS mode as needing
only `OPENAI_API_KEY`. True for six skins, false for the default one. It
now starts the agent too and says why the whole agent lives out of
process.

Reskin skill: updated, one sentence in the Verification list's step 2. A
skin author runs `pnpm dev`, gets redirected from `/` to banking, sends a
test message to see if anything works, and gets silence — then debugs
their own registration. The skill now points them at `/<their-id>` or
`./run-demo.sh`.

Verified by doing it: stopped the dev server and the agent, re-ran the
script, and it reported `banking agent ready (200)` and `stack healthy`
without touching the warm TEI. Then walked beat 3d in the browser — the
Q2 pill filed a report citing "the Meridian Creative Agency invoice from
page 1", i.e. the model read the attached PDF.
2026-08-20 17:29:32 +02:00
Maxim 11838db020 fix(reskinnable-demo): carry an attached PDF to the model as a file block
Beat 3d was dead on banking: clicking the Q2 pill staged the invoice,
sent the message, and then nothing happened at all — no report, no error,
no failed message in the transcript.

`ag_ui_langgraph` hands every attachment to the model as an `image_url`
block, documents included, so the PDF was rejected before the first
token:

    openai.BadRequestError: 400 - Invalid MIME type. Only image types are
    supported. (code: invalid_image_format)

The exception is raised inside the model node, which kills the SSE
stream. The runtime sees `RUN_ERROR: terminated` with no cause and the
browser renders nothing. Worse, the crashed run is still checkpointed, so
every LATER message on that thread replays the rejected content and dies
the same way: one click on the pill killed the whole conversation, and
only restarting this service cleared it (`MemorySaver` is in-process).

`_repair_document_attachments` rewrites those blocks into LangChain
standard `file` blocks before the model call. It walks every message, not
just the newest, because the offending content comes back from the
checkpoint on later turns.

STOPGAP, not a design. The real fix is open upstream as
ag-ui-protocol/ag-ui#2476 (both adapters, plus the return leg so a
non-image attachment survives MESSAGES_SNAPSHOT). This service installs
the adapter from PyPI, so that fix cannot reach this venv until it is
published; the middleware's header says when to delete it.

No test: `agent/` has no python test runner, and standing one up for code
whose deletion is already scheduled is the wrong trade. The durable
tests — PDF, audio, video, filename, round-trip, legacy binary — ship
with the upstream PR instead. Verified here by replaying the browser's
real captured run payload against the service: 400 before, RUN_FINISHED
with `createReport` carrying the invoice's line items after.

Reskin skill: checked, no impact. Its beat-3d guidance is entirely the
CLIENT half (staging into the composer, the `AttachmentFailureCause`
union, do not copy `@/shell/attach`), and it names no model-side
conversion. A skin authored from the skill gets a `BuiltInAgent`, whose
converter already maps documents onto file parts — banking is the only
skin whose agent is a LangGraph service, so this failure is unreachable
from the skill's path.

Gates: lint clean, typecheck clean, 2460 unit tests pass. `pnpm build`
deliberately not run — the diff touches no TypeScript, and `next build`
would clobber the `.next` state of the dev server currently serving the
demo.
2026-08-20 17:17:03 +02:00
Maxim 0163beab8e feat(reskinnable-demo): stream the harness into a CLI console via AG-UI subagents
Makes the offsite-expenses beat legible while it runs, and gives it its own
model, by taking the AG-UI subagent surface from the canary line.

## The expense analyst is now a real subagent

    banking                       gpt-5.4, temp 0
      │  banking's prompt; browser frontend tools; Intelligence memory tools
      └─ expense-analyst          gpt-5.6-sol, reasoning_effort=high
           │  sandboxed shell, submit_expense_report
           └─ merchant-researcher gpt-5.4, one per merchant, Tavily

Previously the beat was a section of banking's prompt, which left nowhere to put
per-beat configuration: model, effort and recursion limit are all agent-level and
there was one agent. It also meant banking's ~21,000-character rulebook rode
every one of the ~20 model calls the run makes, re-sending rules about markdown
tables while the agent read a CSV.

Reached as a `CompiledSubAgent` because a raw `SubAgent` spec has no `subagents`
field and this one needs its own — the per-merchant fan-out is a headline of the
beat, and a flat subagent could only research serially. Verified nesting
survives: a probe showed the analyst's `task` dispatches, the researchers'
`search_merchant` calls and the final report tool all reaching `astream_events`.

`gpt-5.6-sol` additionally needs `use_responses_api=True`: with function tools
and `reasoning_effort` it 400s on /v1/chat/completions. The first probe missed
that by asking the model a question with NO tools bound — a model probe for an
agent has to bind one.

## The console: one CLI window, streaming

`shell/subagents/subagent-activity.tsx` subscribes to the agent's event stream
and folds it into console lines. Reading `agent.messages` (the previous design)
was wrong twice over: messages materialise at the `MESSAGES_SNAPSHOT`, two per
run, so the pane sat still for minutes and then filled at once; and persisted
messages carry no `subagentRunId`, so the harness's narration could not be told
from banking's own reply.

The fold is pure and idempotent — every line keyed by the id of the thing that
produced it — so the same code serves the live subscription and a replay of the
thread's stored events when a conversation is reopened.

Three heuristics are deleted, each replaced by identity the protocol already
carried:

- the console's "first tool call" anchor      -> the run's first `task` call, from
                                                 MESSAGE order (durable; the
                                                 event-derived version rendered
                                                 one console per delegation on a
                                                 restored thread — six, measured)
- `CONSOLE_TOOL_NAMES` suppression list       -> `subagentRunId` presence
- `disable_streaming` on the researchers      -> the canary's per-lane state

`shell/subagents/subagent-message-filter.tsx` keeps subagent narration out of the
conversation. It suppresses the PROSE and keeps the TOOL CALLS: an agent
routinely narrates and calls a tool in one message, and returning null for the
whole message hid the REPORT CARD — the run looked perfect and ended with nothing
to show. Shell-level and inert for a skin whose agent has no subagents.

## Canary stack, contained to this app

The subagent surface only exists on the canary line, and a released
`@ag-ui/client` <= 0.0.57 rejects `SUBAGENT_*` events in the HTTP transport
before any middleware runs, killing the stream. So the demo leaves the root pnpm
workspace and ships its own lockfile, pinning `@copilotkit/* 1.68.3-canary` and
`@ag-ui/* 0.0.59-canary` locally instead of imposing an unreleased protocol on
every package in the monorepo.

A 1.62.2-based canary was tried first and could not compile the app: it silently
rewound the CopilotKit API five minors under a demo written against 1.67.1, and
`OpenGenerativeUIActivityRenderer` (a public `/v2` export since ~1.63) was the
first thing to break. `workspace:*` is not a version, so the app had no recorded
lower bound on the API it needs.

KNOWN GAP, deliberately not fixed here: Nx discovers projects THROUGH the pnpm
workspace (there is no `workspaceLayout` in `nx.json`), so leaving it also
removes the demo from the repo-wide `nx run-many -t build` and `-t check-types`
sweeps. Verified — `nx show project deep-agents` and the other standalone
showcases return "Could not find project". No workflow names this demo, so it is
currently unbuilt and untype-checked in CI and needs its own job. Run the four
gates locally until that lands. Documented in `pnpm-workspace.yaml`.

## Fixes

- The run clock is keyed per run and read through an injected `ToolRuntime`
  instead of taking the oldest open stamp across the process. Model calls AFTER
  the report re-stamped the clock and that leftover became the next run's start:
  a two-minute run reported 333s. Now 86s reported against 98s wall clock — the
  gap is thread-naming and delegation, before the analyst's first model call,
  which is what the tile claims to measure.
- `merchantKind` non-answers are rejected on the leading token, on hedging
  language, and over 40 characters. With no search tool the model wrote a bare
  "unclear"; with Tavily live it hedges in prose ("unknown (likely
  bookbindery/bookshop retail, but not established for this exact merchant)"),
  which an exact-match filter passed into a 60-character label glued to the
  merchant name.
- `vitest` no longer externalises `@copilotkit/*`. Installing them from npm moved
  `src/app/layout.tsx`'s stylesheet import under `node_modules/.pnpm/`, where
  Node's ESM loader threw `Unknown file extension ".css"` and took out 16 suites
  while naming a stylesheet nobody had touched.
- `agent/main.py` reads the demo's `.env` as well as its own, so `TAVILY_API_KEY`
  works wherever an operator puts it. Two env files to keep in sync is a trap
  whose failure mode is "the agent ignores a key that is plainly sitting in .env".

## Upstream finding (reported separately, not fixed here)

`@copilotkit/runtime` drops `subagentRunId` when persisting messages: 2888 of
3026 stream events carry it, 0 of 53 persisted messages do. Reproduced with
Intelligence removed entirely, so it is the runtime's message shape rather than
the platform store — and `@copilotkit/runtime`'s dist contains no occurrence of
the field at all, while `@ag-ui/core`, `ag-ui-protocol` and `@copilotkit/core`
all model it. One field threaded through would let the console rebuild from
message history and delete the event-replay seeding added here.

`CLAUDE.md`'s appended block is generated by `next dev`
(`next/dist/server/lib/generate-agent-files.js`) and committed per its own
instruction to keep the tree clean.

Gates: lint 0, typecheck 0, test:unit 2460 passed across 216 files, build 0.
Measured end to end in Intelligence mode: 14 rows, 9 merchants researched, 6
charges filed with ids read out of real 201 bodies, totals reconciling against
their own rows, 3220 events with 6 SUBAGENT_STARTED/FINISHED pairs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRubZT6AS6LCGkcE2KzcfA
2026-08-20 13:54:58 +02:00
Maxim a29bf0720a feat(reskinnable-demo): add the offsite-expenses pill and harden the report fields
THE PILL. The offsite-expenses beat had no entry point in the UI. The agent was
wired, the service was up, all four gates were green, and every verification I
ran POSTed directly at the runtime — so the beat was fully working and completely
unreachable for anyone opening the app. It is the ninth and last pill, after the
AWS charge, because it is the only one that does not answer in seconds.

`suggestions.test.ts` now pins it. That guard is not decoration: this pill has no
type referencing it, no matcher and no route, so nothing else in the tree
notices if it is dropped or retitled. It checks REACHABILITY, which is the
property nothing else was holding.

MERCHANT KIND HARDENING. `merchantKind` is a KIND ("hotel", "pharmacy") that the
report card prints inline beside the merchant name, where it has room for about
two words. The previous filter rejected exact matches of "unclear"/"unknown",
which was enough when there was no search tool and the model wrote a bare
"unclear". With Tavily live it hedges in prose instead — measured:
"unknown (likely wellness-related business)" and "unknown (likely
bookbindery/bookshop retail, but not established for this exact merchant)". Both
sailed through and would have rendered as a 60-character label glued to the
merchant name.

Now rejects on the leading token, on hedging language anywhere, and on anything
over 40 characters. The length cap is the backstop: it encodes the actual
constraint rather than a list of phrasings already observed, so it catches the
next wording nobody predicted. A measured run after the change leaked none, with
a longest surviving kind of 23 chars.

ONE ENV FILE. `agent/main.py` now loads the demo's `.env` in addition to its own,
with `agent/.env` winning on conflict. The app and this service need the same
keys, and asking an operator to keep two files in sync is a trap whose failure
mode is "the agent ignores a key that is plainly sitting in .env" — which is
exactly what happened when TAVILY_API_KEY was added to the demo's `.env` and the
service, reading only its own, never saw it.

`.env.example` documents TAVILY_API_KEY and BANKING_AGENT_URL, including what a
keyless run actually looks like: the research subagents are told plainly that no
search happened and instructed to report "could not establish" rather than
guess, so the run completes with several rows marked `unclear` and a "merchants
researched" tile reading 0. Nothing errors, which is why it needs writing down.

Measured with Tavily live: 14 rows, 10 merchants researched, 10 parallel
subagents, 31s, totals reconciling against their own rows, 7 charges filed with
ids read out of real 201 bodies, zero non-answer kinds. Research also CHANGES a
decision rather than only labelling one — The Copper Room resolved to
`restaurant_lounge` and moved from `unclear` to `expensable`.

Gates: lint 0, typecheck 0, test:unit 2460 passed across 216 files, build 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRubZT6AS6LCGkcE2KzcfA
2026-08-20 02:42:45 +02:00
Maxim e4eb96f8d7 feat(reskinnable-demo): make banking's agent the Python deep agent
Banking's `agent.ts` stops returning a `BuiltInAgent` and returns an `HttpAgent`
pointed at the Python deep agent instead. Six skins still run in-process;
banking is the one that does not.

WHY THE WHOLE AGENT, not just the offsite-expenses beat. Threads are scoped per
agent — `listThreads` takes `agentId` as a REQUIRED parameter, and measured
against the running app the two lists are disjoint (banking 46 threads,
banking-expenses 10). The v2 runtime has no handoff mechanism, and
`defineTool`'s `execute` takes `(args)` with no emitter, so a tool cannot stream
a multi-minute run. Under a second agent id a presenter could start the
analysis, switch threads, and have nothing to come back to. One conversation
list means one agent.

The expense task spec is appended to banking's own prompt as a CONDITIONAL job
rather than a second identity — it opens by saying it applies only when the user
hands over a statement, and to ignore it otherwise.

Ported to Python:
- `agent/prompt.py` — banking's system prompt, extracted from the TS template
  literal programmatically and asserted equal, not paraphrased. 21,208 chars
  carrying 56 distinct rules; a dropped rule silently breaks a beat that still
  looks like it works.
- `agent/report.py` — `render_report` and the a2ui operations builder. Component
  order, ids and the `columns` arithmetic match the TS builder; the unique
  surface suffix uses uuid rather than a timestamp so two reports in the same
  millisecond cannot collide.

Carried over from the TS `BuiltInAgent` because each had a reason written beside
it: the non-mini model (the teach-and-recall arc routes unreliably on mini) and
`temperature: 0` (tool routing must be deterministic).

Also sets `emit_raw_events = False`. It defaults to True, which piggybacks
LangChain's internal events onto the AG-UI stream: a measured run streamed
27,950,261 bytes, and the same run with it off streamed 374,086 — a 75x
reduction with an identical report. This matters more than it looks because the
thread PERSISTS those events for replay, and leaving a running thread and coming
back to it is the point of this beat. Nothing downstream reads RAW.

Verified against the running app, not reasoned about:
- `/info` reports banking as HttpAgent and the other six as BuiltInAgent.
- Canvas beat: `render_report` -> TOOL_CALL_RESULT -> the a2ui middleware emits
  `activityType: a2ui-surface` with createSurface + updateComponents.
- Frontend-tool beat: given showTransactions + showPendingApprovals the agent
  picks showTransactions, emits no result (the client executes it) and emits no
  prose — the prompt's "the rendered list is the single source of truth" rule
  surviving the port.
- Intelligence mode: the run is handed the browser's tool AND
  recall_memory / save_memory / forget_memory / copilotkit_knowledge_base_shell.
- Durability: a run whose client disconnected at 8s finished unattended and left
  a replayable thread.

Removes `expenses-agent.ts` and the non-skin `banking-expenses` registry key;
banking's own id now serves both. `AgentRegistration.createAgent`'s comment is
rewritten — it described a second remote key that no longer exists — and now
gives a grep to derive the BuiltInAgent/HttpAgent split rather than asserting it.

Gates: lint 0, typecheck 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRubZT6AS6LCGkcE2KzcfA
2026-08-20 01:56:24 +02:00
Maxim a6418bcc09 feat(reskinnable-demo): run the offsite-expenses beat on a LangChain deep agent
Replaces the Codex CLI subprocess with a Python LangChain deep agent
(`examples/showcases/reskinnable-demo/agent/`) reached over AG-UI: sandboxed
shell execution via deepagents' `LocalShellBackend`, parallel per-merchant
research subagents, no external harness binary, and our own API key.

Registered as `banking-expenses` in the server agent registry. That required
widening `AgentRegistration.createAgent` from `() => BuiltInAgent` to
`() => AbstractAgent` — the runtime's own `agents` option is
`Record<string, AbstractAgent>` and the v2 tree contains no
`instanceof BuiltInAgent` branch, so the demo's type was stricter than the
runtime it described.

Measured end to end against the real runtime: 58 `execute` calls, 7 concurrent
`task` subagents (peak concurrency 4/4 on a dedicated probe), RUN_FINISHED,
eight charges filed with ids read out of real 201 bodies, and 8221 replayable
AG-UI events persisted on the thread after the client disconnected 8s in.

Correctness fixes found by running it rather than reading it:
- `submit_expense_report` DERIVES the totals from the verdicts instead of
  accepting them as arguments. A measured run had every per-row amount matching
  the CSV while the headline totals came back $1.00 and $0.20 high — the model
  authored them. The report card prints the total directly above the rows it is
  the sum of, so tiles and rows must not be able to disagree.
- `amount` is coerced at the tool boundary; it arrived as a string on all 14
  rows, which silently defeats the widget's currency formatting.
- `merchantKind` values that are non-answers ("unclear") are dropped rather than
  rendered beside the merchant name as if they were findings.
- The recursion limit is set on the AGENT, not via `graph.with_config(...)`,
  which the AG-UI adapter drops — the run completed the whole analysis and then
  died at LangGraph's default of 25 supersteps.
- A `clone()` override works around `copilotkit` 0.1.95 being incompatible with
  `ag-ui-langgraph` >= 0.0.42, whose base `clone()` (called per request) passes
  kwargs the SDK subclass does not accept. Every request 500s without it.

Deletes the superseded Node-side harness scaffolding: `prompt.ts` (the task
specification now lives in the Python agent's system prompt), `workspace.ts`
(the sandbox owns the workspace) and their tests. `types.ts` stays — it is the
shared vocabulary the report card and the tool renderer both import.

Also drops the three unused `@tanstack/*` dependencies.

Reskin-skill impact: checked. `.claude/skills/reskin/` documents the `Skin`
contract, and no field of it changed — `banking-expenses` is NOT a skin and must
never look like one. It is absent from `registry.ts`, `skinIds`,
`skinIdentities` and `LINTED_SKIN_IDS`, and has no route, theme or identity. The
one documented thing that did change is `AgentRegistration.createAgent`'s type,
which SKILL.md describes in its registration section; that is updated in the
follow-up commit that makes banking itself a deep agent.

Gates: lint 0, typecheck 0, test:unit 2458 passed across 217 files, build 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRubZT6AS6LCGkcE2KzcfA
2026-08-20 01:07:56 +02:00