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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ charts/
coverage/
.remember/
docs/pixels.js

# Example projects: build output and locally generated fixtures
examples/**/.next/
examples/**/fixtures/agui-recorded/*.json
examples/**/next-env.d.ts
examples/**/tsconfig.tsbuildinfo
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

## [Unreleased]

### Fixed

- **AG-UI recorder** — `extractLastUserMessage` now walks structured `content`
arrays (e.g. `[{ type: "text", text: "..." }, { type: "document", source: ... }]`)
and joins their text parts. Previously, structured content fell back to the
`__NO_USER_MESSAGE__` sentinel, producing fixtures that couldn't replay.

### Changed

- **`AGUIMessage.content` type widened** to `string | AGUIMessageContentPart[]`.
New exported type `AGUIMessageContentPart` describes the per-part shape.

## [1.26.1] - 2026-05-19

### Added
Expand Down
13 changes: 12 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
prettier,
{ ignores: ["dist/", "node_modules/", "fixtures/", ".worktrees/", "docs/pixels.js"] },
{
ignores: [
"dist/",
"node_modules/",
"fixtures/",
".worktrees/",
"docs/pixels.js",
"examples/**/.next/",
"examples/**/next-env.d.ts",
"examples/**/node_modules/",
],
},
{
files: ["*.config.{js,mjs,ts,cjs}"],
languageOptions: { globals: { module: "readonly", require: "readonly" } },
Expand Down
9 changes: 9 additions & 0 deletions examples/no-user-message-repro/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
.next
next-env.d.ts
tsconfig.tsbuildinfo
*.log

# Recorded fixtures from local runs — keep the directory but ignore generated files
fixtures/agui-recorded/*.json
!fixtures/agui-recorded/.gitkeep
92 changes: 92 additions & 0 deletions examples/no-user-message-repro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# aimock AG-UI repro: `__NO_USER_MESSAGE__` on file attachments

A minimal Next.js + CopilotKit project that reproduces a bug that existed in `@copilotkit/aimock@1.26.1` and earlier: when a CopilotKit chat user message included a file attachment alongside text, the AG-UI client sent `content` as a structured array (`[{ type: "text", text: "..." }, { type: "document", source: ... }]`) instead of a plain string. `aimock`'s `extractLastUserMessage` only handled `typeof content === "string"`, returned `""`, and the recorder wrote `match.message: "__NO_USER_MESSAGE__"` to disk. The resulting fixture was unmatchable on replay.

The bug was fixed by widening `AGUIMessage.content` and teaching `extractLastUserMessage` to walk structured content arrays. `recorded-sample/before-fix.json` (sentinel) and `recorded-sample/after-fix.json` (real user text) are pre-captured exhibits of the same request before and after the fix.

## Architecture

```
Browser (:3000) CopilotKit chat UI, file attachments enabled
Next.js /api/copilotkit CopilotRuntime + HttpAgent
aimock (:4010) AGUIMock recording proxy
upstream-agent (:4001) Tiny SSE stub that returns a canned assistant reply
```

## Run against the fixed local aimock

This example consumes the local aimock checkout via a `link:` dependency in `package.json` (`"@copilotkit/aimock": "link:../.."`), so by default it runs the _fixed_ code.

From the **repo root** (`@copilotkit/aimock`):

```sh
pnpm install
pnpm build
```

From **this directory**:

```sh
pnpm install
pnpm dev
```

`pnpm dev` starts three processes via `concurrently`:

- `upstream` on `:4001` — the noop SSE stub
- `aimock` on `:4010` — the recording proxy
- `next` on `:3000` — the Next.js dev server

Open <http://localhost:3000>, attach any small file (e.g. a `.txt`), send `summarize this`, and inspect `fixtures/agui-recorded/agui-*.json` — `match.message` will be `"summarize this"`.

## Run against the buggy published aimock (1.26.1)

To reproduce the original failure mode, swap the local link for the published 1.26.1 release. From this directory:

1. Edit `package.json` and change:
```json
"@copilotkit/aimock": "link:../.."
```
to:
```json
"@copilotkit/aimock": "1.26.1"
```
2. Reinstall and run:
```sh
pnpm install
pnpm dev
```
3. Repeat the chat reproduction above. The recorded fixture's `match.message` will be `"__NO_USER_MESSAGE__"` — the bug.

Revert the dep back to `"link:../.."` and `pnpm install` again to switch back to the fixed local code.

## Headless repro (no browser)

Skip Next.js by starting just `upstream` and `aimock`, then POSTing the bundled synthetic payload directly:

```sh
pnpm upstream &
pnpm aimock &
curl -X POST http://localhost:4010/ \
-H 'Content-Type: application/json' \
--data-binary @structured-input.json
cat fixtures/agui-recorded/agui-*.json
```

`structured-input.json` is a hand-crafted `RunAgentInput` whose user message uses the canonical AG-UI multimodal schema (`text` + `document` parts). Against fixed aimock, `match.message` is `"summarize this"`; against 1.26.1, it is `"__NO_USER_MESSAGE__"`.

## Compare to a text-only message

Sending a plain text message (no attachment) from the chat always produced the correct `match.message` — even on 1.26.1. The bug only manifested when `content` was structured (which in practice meant: when the user attached a file).

## Notes

- This example is not part of the aimock test suite — it's a manual reproduction kept around as a self-contained demonstration of the bug and its fix.
- `concurrently` shuts down all three processes when you Ctrl-C the parent.
- Recorded fixtures land under `fixtures/agui-recorded/` and are gitignored.
17 changes: 17 additions & 0 deletions examples/no-user-message-repro/app/api/copilotkit/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CopilotRuntime, copilotRuntimeNextJSAppRouterEndpoint } from "@copilotkit/runtime";
import { HttpAgent } from "@ag-ui/client";
import type { NextRequest } from "next/server";

const runtime = new CopilotRuntime({
agents: {
default: new HttpAgent({ url: "http://localhost:4010" }),
},
});

export const POST = async (req: NextRequest) => {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
endpoint: "/api/copilotkit",
});
return handleRequest(req);
};
15 changes: 15 additions & 0 deletions examples/no-user-message-repro/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
html,
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #fafafa;
color: #222;
}

code {
background: #f0f0f0;
padding: 1px 4px;
border-radius: 3px;
font-size: 0.9em;
}
20 changes: 20 additions & 0 deletions examples/no-user-message-repro/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/styles.css";
import "./globals.css";

export const metadata = {
title: "aimock AG-UI repro",
description: "Reproduction for __NO_USER_MESSAGE__ on file attachments",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<CopilotKit runtimeUrl="/api/copilotkit" agent="default">
{children}
</CopilotKit>
</body>
</html>
);
}
18 changes: 18 additions & 0 deletions examples/no-user-message-repro/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";
import { CopilotChat } from "@copilotkit/react-ui";

export default function Page() {
return (
<main style={{ maxWidth: 720, margin: "40px auto", padding: "0 16px", height: "85vh" }}>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>aimock AG-UI repro</h1>
<p style={{ marginBottom: 16, color: "#555" }}>
Click the paperclip icon, attach any small file, type a message, and send. Then check{" "}
<code>fixtures/agui-recorded/</code> — the produced fixture will have{" "}
<code>match.message: &quot;__NO_USER_MESSAGE__&quot;</code>.
</p>
<div style={{ height: "70vh", border: "1px solid #ddd", borderRadius: 8 }}>
<CopilotChat attachments={{ enabled: true }} />
</div>
</main>
);
}
Empty file.
6 changes: 6 additions & 0 deletions examples/no-user-message-repro/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};

export default nextConfig;
35 changes: 35 additions & 0 deletions examples/no-user-message-repro/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "aimock-no-user-message-repro",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"upstream": "tsx upstream-agent.ts",
"aimock": "tsx start-aimock.ts",
"next": "next dev",
"dev": "concurrently -k -n upstream,aimock,next -c blue,magenta,cyan 'pnpm upstream' 'pnpm aimock' 'pnpm next'"
},
"dependencies": {
"@ag-ui/client": "^0.0.53",
"@copilotkit/aimock": "link:../..",
"@copilotkit/react-core": "^1.57.2",
"@copilotkit/react-ui": "^1.57.2",
"@copilotkit/runtime": "^1.57.2",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"concurrently": "^9.0.0",
"tsx": "^4.19.0",
"typescript": "^5.6.0"
},
"pnpm": {
"overrides": {
"@copilotkit/core": "1.57.2"
}
}
}
Loading