Skip to content

Commit 104674f

Browse files
authored
feat(examples): add Intelligence threads to agentcore (CopilotKit#5207)
## Summary - add env-gated CopilotKit Intelligence wiring to the shared AgentCore Hono/Lambda runtime bridge - wire the Vite frontend to REST Threads transport with the Threads drawer, locked gate, and shared `threadId` chat/canvas context - add local Docker env propagation for Intelligence vars and the Vite client-safe Threads gate - pin AgentCore frontend/runtime packages to the threads-capable CopilotKit/AG-UI versions and add missing frontend peer deps needed for a clean build - extend the integration migration regression suite with AgentCore-specific coverage ## Verification - `pnpm exec vitest run scripts/__tests__/integration-intelligence-migration.test.ts` - `pnpm exec oxfmt --check scripts/__tests__/integration-intelligence-migration.test.ts examples/integrations/agentcore/frontend/src/components/chat/CopilotKit/index.tsx examples/integrations/agentcore/frontend/src/components/threads-drawer/locked-state.tsx examples/integrations/agentcore/frontend/src/components/threads-drawer/threads-drawer.tsx examples/integrations/agentcore/frontend/vite.config.ts examples/integrations/agentcore/infra-cdk/lambdas/copilotkit-runtime/src/runtime.ts` - `npm run build` from `examples/integrations/agentcore/frontend` - `npm run build` from `examples/integrations/agentcore/infra-cdk/lambdas/copilotkit-runtime` - pre-commit hook: `check-binaries`, `sync-lockfile`, `lint-fix`, `test-and-check-packages`, `commitlint` - local Intelligence stack: copied `/Users/mothra/Projects/test-signups4/.env` into AgentCore's ignored `docker/.env`, started `docker compose up -d --wait`, seeded `demo-user`/`1_demo-user` - local bridge smoke: `GET http://localhost:3101/copilotkit/threads?agentId=default` returned `200` - Playwright smoke with local generated `aws-exports.json` + seeded OIDC storage: authenticated Vite app rendered, licensed Threads drawer visible on desktop, mobile floating pill visible, no horizontal overflow, reload preserved authenticated render ## Notes - Full live AgentCore response verification was not completed because this path requires a deployed AWS AgentCore/Cognito stack and real AgentCore agent runtime. The browser smoke used fake OIDC storage and a dummy agent URL, so it verified frontend/runtime Threads wiring but not a real agent response. - In the fake-auth local smoke, the Intelligence realtime websocket returned `403`, while the REST Threads list rendered successfully. I did not treat that as proof of a production websocket issue because the smoke bypassed real Cognito/auth, but it is worth keeping an eye on in a deployed AgentCore environment.
2 parents 2806cfd + 7b4e5d1 commit 104674f

16 files changed

Lines changed: 2927 additions & 2599 deletions

File tree

examples/integrations/agentcore/docker/.env.example

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,11 @@ AWS_DEFAULT_REGION=us-east-1
1414

1515
# ── Agent selection ────────────────────────────────────────────────────────────
1616
# Which agent to run locally: langgraph or strands (default: strands)
17-
AGENT=strands
17+
AGENT=strands
18+
19+
# ── CopilotKit Intelligence / Threads (optional) ──────────────────────────────
20+
# Enables persistent Threads in the CopilotKit bridge + frontend.
21+
COPILOTKIT_LICENSE_TOKEN=
22+
INTELLIGENCE_API_KEY=
23+
INTELLIGENCE_API_URL=http://localhost:4201
24+
INTELLIGENCE_GATEWAY_WS_URL=ws://localhost:4401

examples/integrations/agentcore/docker/docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ services:
6767
- AGENTCORE_AG_UI_URL=http://agent:8080/invocations
6868
- PORT=3001
6969
- OTEL_SDK_DISABLED=true
70+
- COPILOTKIT_LICENSE_TOKEN=${COPILOTKIT_LICENSE_TOKEN}
71+
- INTELLIGENCE_API_KEY=${INTELLIGENCE_API_KEY}
72+
- INTELLIGENCE_API_URL=${INTELLIGENCE_API_URL:-http://localhost:4201}
73+
- INTELLIGENCE_GATEWAY_WS_URL=${INTELLIGENCE_GATEWAY_WS_URL:-ws://localhost:4401}
7074
depends_on:
7175
agent:
7276
condition: service_healthy
@@ -84,6 +88,7 @@ services:
8488
- /app/node_modules
8589
environment:
8690
- NODE_ENV=development
91+
- VITE_COPILOTKIT_THREADS_ENABLED=${COPILOTKIT_LICENSE_TOKEN:+true}
8792
depends_on:
8893
bridge:
8994
condition: service_started

examples/integrations/agentcore/frontend/package-lock.json

Lines changed: 494 additions & 2114 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/integrations/agentcore/frontend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"clean": "rm -rf build/ node_modules/ .vite/"
1313
},
1414
"dependencies": {
15-
"@copilotkit/react-core": "^1.54.0",
15+
"@copilotkit/react-core": "1.59.3",
1616
"@radix-ui/react-alert-dialog": "^1.1.15",
1717
"@radix-ui/react-dialog": "^1.1.15",
1818
"@radix-ui/react-progress": "^1.1.7",
@@ -23,10 +23,12 @@
2323
"class-variance-authority": "^0.7.1",
2424
"clsx": "^2.1.1",
2525
"lucide-react": "^0.562.0",
26+
"oidc-client-ts": "^3.5.0",
2627
"radix-ui": "^1.4.3",
2728
"react": "^19.2.1",
2829
"react-dom": "^19.2.1",
2930
"react-dropzone": "^14.3.8",
31+
"react-is": "^19.2.7",
3032
"react-markdown": "^10.1.0",
3133
"react-oidc-context": "^3.3.0",
3234
"react-router-dom": "^6.21.0",

examples/integrations/agentcore/frontend/src/components/chat/CopilotKit/index.tsx

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,40 @@
22
"use client";
33

44
import "@copilotkit/react-core/v2/styles.css";
5-
import { useEffect, useState } from "react";
5+
import { useEffect, useMemo, useState } from "react";
66
import {
77
CopilotChat,
8+
CopilotChatConfigurationProvider,
89
CopilotKitProvider,
910
useFrontendTool,
1011
} from "@copilotkit/react-core/v2";
1112
import { useAuth as useOidcAuth } from "react-oidc-context";
12-
import { loadAwsConfig, type AwsExportsConfig } from "@/lib/runtime-config";
13+
import { loadAwsConfig } from "@/lib/runtime-config";
14+
import type { AwsExportsConfig } from "@/lib/runtime-config";
1315
import { useExampleSuggestions } from "@/hooks/useExampleSuggestions";
1416
import { useCopilotExamples } from "@/hooks/useCopilotExamples";
1517
import { ThemeProvider } from "@/hooks/useTheme";
1618
import { TodoCanvas } from "@/components/canvas/TodoCanvas";
1719
import { ModeToggle } from "@/components/ui/mode-toggle";
20+
import { ThreadsDrawer } from "@/components/threads-drawer";
21+
import { ThreadsPanelGate } from "@/components/threads-drawer/locked-state";
1822

19-
const COPILOTKIT_AGENT_ID = "default";
23+
import styles from "@/components/threads-drawer/threads-drawer.module.css";
2024

21-
function CopilotChatContent() {
25+
const COPILOTKIT_AGENT_ID = "default";
26+
type ResolvedAwsExportsConfig = AwsExportsConfig & {
27+
copilotKitRuntimeUrl: string;
28+
};
29+
30+
function CopilotChatContent({
31+
runtimeUrl,
32+
headers,
33+
}: {
34+
runtimeUrl: string;
35+
headers: Record<string, string> | undefined;
36+
}) {
2237
const [mode, setMode] = useState<"chat" | "app">("chat");
38+
const [threadId, setThreadId] = useState<string | undefined>(undefined);
2339

2440
useExampleSuggestions();
2541
useCopilotExamples();
@@ -41,30 +57,77 @@ function CopilotChatContent() {
4157
});
4258

4359
return (
44-
<div className="h-full flex flex-row">
45-
<ModeToggle mode={mode} onModeChange={setMode} />
46-
<div
47-
className={`max-h-full overflow-y-auto [&_.copilotKitChat]:h-full [&_.copilotKitChat]:border-0 [&_.copilotKitChat]:shadow-none ${
48-
mode === "app" ? "w-1/3 px-6 max-lg:hidden" : "flex-1 px-4 lg:px-6"
49-
}`}
50-
>
51-
<CopilotChat agentId={COPILOTKIT_AGENT_ID} className="h-full" />
52-
</div>
53-
<div
54-
className={`h-full overflow-hidden ${
55-
mode === "app"
56-
? "w-2/3 border-l dark:border-zinc-700 max-lg:w-full max-lg:border-l-0"
57-
: "w-0 border-l-0"
58-
}`}
59-
>
60-
<div className="h-full w-full lg:w-[66.666vw]">
61-
<TodoCanvas />
62-
</div>
60+
<div className={styles.layout}>
61+
<ThreadsPanelGate>
62+
<ThreadsDrawer
63+
agentId={COPILOTKIT_AGENT_ID}
64+
threadId={threadId}
65+
onThreadChange={setThreadId}
66+
runtimeUrl={runtimeUrl}
67+
headers={headers}
68+
/>
69+
</ThreadsPanelGate>
70+
<div className={styles.mainPanel}>
71+
<CopilotChatConfigurationProvider
72+
agentId={COPILOTKIT_AGENT_ID}
73+
threadId={threadId}
74+
>
75+
<div className="h-full flex flex-row">
76+
<ModeToggle mode={mode} onModeChange={setMode} />
77+
<div
78+
className={`max-h-full overflow-y-auto [&_.copilotKitChat]:h-full [&_.copilotKitChat]:border-0 [&_.copilotKitChat]:shadow-none ${
79+
mode === "app"
80+
? "w-1/3 px-6 max-lg:hidden"
81+
: "flex-1 px-4 lg:px-6"
82+
}`}
83+
>
84+
<CopilotChat agentId={COPILOTKIT_AGENT_ID} className="h-full" />
85+
</div>
86+
<div
87+
className={`h-full overflow-hidden ${
88+
mode === "app"
89+
? "w-2/3 border-l dark:border-zinc-700 max-lg:w-full max-lg:border-l-0"
90+
: "w-0 border-l-0"
91+
}`}
92+
>
93+
<div className="h-full w-full lg:w-[66.666vw]">
94+
<TodoCanvas />
95+
</div>
96+
</div>
97+
</div>
98+
</CopilotChatConfigurationProvider>
6399
</div>
64100
</div>
65101
);
66102
}
67103

104+
function CopilotKitShell({
105+
config,
106+
accessToken,
107+
}: {
108+
config: ResolvedAwsExportsConfig;
109+
accessToken: string | undefined;
110+
}) {
111+
const headers = useMemo(
112+
() =>
113+
accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
114+
[accessToken],
115+
);
116+
117+
return (
118+
<CopilotKitProvider
119+
runtimeUrl={config.copilotKitRuntimeUrl}
120+
headers={headers}
121+
useSingleEndpoint={false}
122+
>
123+
<CopilotChatContent
124+
runtimeUrl={config.copilotKitRuntimeUrl}
125+
headers={headers}
126+
/>
127+
</CopilotKitProvider>
128+
);
129+
}
130+
68131
export default function CopilotChatInterface() {
69132
const auth = useOidcAuth();
70133
const [config, setConfig] = useState<AwsExportsConfig | null>(null);
@@ -117,14 +180,10 @@ export default function CopilotChatInterface() {
117180
return (
118181
<ThemeProvider>
119182
<div className="h-full bg-[#f5f7fb]">
120-
<CopilotKitProvider
121-
runtimeUrl={config.copilotKitRuntimeUrl}
122-
headers={
123-
accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined
124-
}
125-
>
126-
<CopilotChatContent />
127-
</CopilotKitProvider>
183+
<CopilotKitShell
184+
config={config as ResolvedAwsExportsConfig}
185+
accessToken={accessToken}
186+
/>
128187
</div>
129188
</ThemeProvider>
130189
);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Threads Drawer — Theming Contract
2+
3+
The threads-drawer is a BASE component. It is fully driven by CSS variables and
4+
contains no hardcoded colors, shadows, or surface radii. To theme it for an
5+
example, (re)define the tokens below on any ancestor (e.g. `:root`, `body`, or a
6+
wrapper element) — **never edit the drawer files**.
7+
8+
The drawer first consumes the shared design-system tokens (`--card`,
9+
`--border`, `--radius`, …) that `ui/card.tsx` and `ui/button.tsx` also consume.
10+
For a handful of drawer-specific visuals (scrim, shadows, delete-hover tint) it
11+
exposes dedicated `--threads-*` tokens, each with a fallback to a shared token or
12+
the original literal — so defining nothing reproduces the default look exactly.
13+
14+
## Shared design-system tokens consumed
15+
16+
| Token | Controls |
17+
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
18+
| `--background` | Tooltip text color (`color: var(--background)` on the dark tooltip body) |
19+
| `--foreground` | Drawer/dialog title + body text, active segment text, tooltip surface bg |
20+
| `--card` | Drawer surface bg (via `--threads-drawer-bg`), active segment bg, empty/dialog/load-more bg |
21+
| `--border` | Drawer + header + filter + dialog borders, thread accent (idle), selected-row inset ring, secondary-button hover bg, tooltip border |
22+
| `--radius` | Drawer/dialog/button/segment/thread/empty-card radii; tooltip radius derives from it |
23+
| `--primary` | New-thread button bg, primary dialog button bg, selected thread accent |
24+
| `--primary-foreground` | New-thread button text, primary dialog button text |
25+
| `--secondary` | Icon-button + thread-row + load-more hover bg, segment track, archived badge bg, secondary dialog button bg, loading skeleton bars, thread-enter start bg |
26+
| `--secondary-foreground` | (locked-state) inline code text |
27+
| `--muted-foreground` | Icon-button idle color, segment idle text, meta text, placeholder/archived titles, empty/dialog description, load-more text, collapsed-rail icon |
28+
| `--accent` | Selected thread-row bg |
29+
| `--ring` | Focus-visible outline on buttons, thread items, segments, dialog buttons |
30+
| `--destructive` | Delete-button icon color + delete-hover text |
31+
| `--destructive-foreground` | Destructive dialog button text |
32+
| `--font-body` | Header, segments, tooltip, empty card, and dialog typography |
33+
34+
(locked-state additionally uses `--secondary`, `--muted-foreground`, `--border`,
35+
`--radius`, `--secondary-foreground`, and the `ui/card` + `ui/button` tokens via
36+
those components.)
37+
38+
## Drawer-specific tokens (with fallbacks)
39+
40+
| Token | Controls | Fallback |
41+
| --------------------------------- | ----------------------------------------- | --------------------------------------------------------- |
42+
| `--threads-drawer-bg` | Drawer surface background | `var(--card)` |
43+
| `--threads-drawer-border` | Drawer right border color | `var(--border)` |
44+
| `--threads-drawer-shadow` | Open-drawer drop shadow | `4px 0 20px rgb(0 0 0 / 0.04)` |
45+
| `--threads-segment-active-shadow` | Active filter-segment shadow | `0 1px 2px rgb(0 0 0 / 0.06)` |
46+
| `--threads-delete-hover-bg` | Delete-button hover/focus background tint | `color-mix(in srgb, var(--destructive) 10%, transparent)` |
47+
| `--threads-overlay-bg` | Confirm-dialog overlay scrim | `rgb(0 0 0 / 0.5)` |
48+
| `--threads-dialog-shadow` | Confirm-dialog drop shadow | `0 20px 50px rgb(0 0 0 / 0.25)` |
49+
| `--threads-tooltip-radius` | Action-button tooltip corner radius | `calc(var(--radius) - 0.45rem)` (= `0.3rem` at default) |
50+
51+
All fallbacks resolve to the original hardcoded values in the north-star, so an
52+
example that defines none of the `--threads-*` tokens renders pixel-identical to
53+
the pre-tokenization drawer.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"use client";
2+
3+
export { default as ThreadsDrawer } from "./threads-drawer";
4+
export type { ThreadsDrawerProps } from "./threads-drawer";
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import {
5+
Card,
6+
CardContent,
7+
CardDescription,
8+
CardFooter,
9+
CardHeader,
10+
CardTitle,
11+
} from "@/components/ui/card";
12+
import { Button } from "@/components/ui/button";
13+
import styles from "./threads-drawer.module.css";
14+
15+
export function ThreadsPanelGate({ children }: { children: React.ReactNode }) {
16+
// The Threads drawer reads a client-only external store (useThreads /
17+
// useSyncExternalStore) with no server snapshot, so it must not render during
18+
// SSR/prerender — Next would fail to prerender "/". Defer to client mount.
19+
const [mounted, setMounted] = React.useState(false);
20+
React.useEffect(() => setMounted(true), []);
21+
22+
if (import.meta.env.VITE_COPILOTKIT_THREADS_ENABLED === "true") {
23+
if (!mounted) {
24+
// SSR / first-paint placeholder: matches the open drawer's footprint +
25+
// surface (and collapses to nothing on mobile) so the panel doesn't flash
26+
// a bare-background column or shift the content when the drawer mounts.
27+
return <div className={styles.drawerPlaceholder} aria-hidden />;
28+
}
29+
return <>{children}</>;
30+
}
31+
32+
return (
33+
<div className="flex w-80 shrink-0 flex-col items-center justify-center p-4 bg-[var(--threads-drawer-bg,var(--card))] border-r border-[var(--threads-drawer-border,var(--border))] max-lg:hidden">
34+
<Card className="w-full">
35+
<CardHeader>
36+
<div className="mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-[var(--secondary)]">
37+
<svg
38+
xmlns="http://www.w3.org/2000/svg"
39+
width="20"
40+
height="20"
41+
viewBox="0 0 24 24"
42+
fill="none"
43+
stroke="currentColor"
44+
strokeWidth="2"
45+
strokeLinecap="round"
46+
strokeLinejoin="round"
47+
className="text-[var(--muted-foreground)]"
48+
aria-hidden="true"
49+
>
50+
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
51+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
52+
</svg>
53+
</div>
54+
<CardTitle>Threads</CardTitle>
55+
<CardDescription>
56+
Threads is a licensed CopilotKit Intelligence feature. Unlock
57+
persistent conversation history, multi-session context, and thread
58+
management across your application.
59+
</CardDescription>
60+
</CardHeader>
61+
<CardContent>
62+
<p className="text-sm text-[var(--muted-foreground)]">
63+
To enable Threads, add a CopilotKit Intelligence license to your
64+
project with:
65+
</p>
66+
</CardContent>
67+
<CardFooter className="flex-col items-start gap-3">
68+
<div className="w-full rounded-[var(--radius)] border border-[var(--border)] bg-[var(--secondary)] px-3 py-2">
69+
<code className="text-xs whitespace-nowrap text-[var(--secondary-foreground)]">
70+
copilotkit add-intelligence
71+
</code>
72+
</div>
73+
<Button
74+
variant="default"
75+
size="sm"
76+
className="w-full"
77+
onClick={() =>
78+
window.open(
79+
"https://docs.copilotkit.ai/intelligence",
80+
"_blank",
81+
"noopener,noreferrer",
82+
)
83+
}
84+
>
85+
Learn more
86+
</Button>
87+
</CardFooter>
88+
</Card>
89+
</div>
90+
);
91+
}

0 commit comments

Comments
 (0)