Skip to content

Commit 0fcf904

Browse files
committed
fix(showcase): switch langgraph-python gen-ui-agent to v2 useAgent
The langgraph-python gen-ui-agent demo was the only one of 18 integrations using the V1 `useCoAgentStateRender` hook. That hook binds renders to messages via per-message claims, so each state-changing tool call (each `set_steps` invocation) produced its own card snapshot in the chat — a typical 3-step plan run pushed ~7+ stacked cards instead of one updating card. Migrate the page to the canonical V2 pattern already used by every other gen-ui-agent demo (mastra, strands, ag2, agno, crewai-crews, langgraph-typescript, pydantic-ai, ...): subscribe to live state via `useAgent` and render a single `InlineAgentStateCard` inside `messageView.children`. The card now re-renders in place as state streams — no per-message claims, no duplicates. Also tighten the agent system prompt with an explicit numbered tool sequence (1 plan + 6 transitions + final message) to make the "step 3 stuck in_progress" tail-of-run failure less likely with gpt-4o-mini. The UI is robust to a missed final transition either way: when `agent.isRunning` flips to false, the card headlines "All N steps complete" regardless of step.status. Replace the stale e2e spec (which targeted a long-removed `task-progress` test id) with one that pins the contract: - exactly one `agent-state-card` rendered, even after the run finishes - every `agent-step` ends in `data-status="completed"`
1 parent 1a3424b commit 0fcf904

3 files changed

Lines changed: 102 additions & 62 deletions

File tree

showcase/integrations/langgraph-python/src/agents/gen_ui_agent.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,24 @@ class _GenUiStateMiddleware(AgentMiddleware):
5959

6060

6161
SYSTEM_PROMPT = (
62-
"You are an agentic planner. For each user request: (1) plan exactly 3 "
63-
"concrete steps and call `set_steps` ONCE to publish them all with "
64-
"status=pending; (2) for each step in order: call `set_steps` with that "
65-
"step flipped to in_progress (all others unchanged); briefly simulate the "
66-
"work; then call `set_steps` with that step flipped to completed. Finally "
67-
"respond conversationally. Never call set_steps in parallel - always wait "
68-
"for one call to return before the next. Do NOT use write_todos - use "
69-
"set_steps only."
62+
"You are an agentic planner. For each user request, follow this exact "
63+
"sequence:\n"
64+
"1. Plan exactly 3 concrete steps and call `set_steps` ONCE with all "
65+
"three steps at status=\"pending\".\n"
66+
"2. Step 1: call `set_steps` with step 1 at status=\"in_progress\", "
67+
"then call `set_steps` again with step 1 at status=\"completed\".\n"
68+
"3. Step 2: call `set_steps` with step 2 at status=\"in_progress\", "
69+
"then call `set_steps` again with step 2 at status=\"completed\".\n"
70+
"4. Step 3: call `set_steps` with step 3 at status=\"in_progress\", "
71+
"then call `set_steps` again with step 3 at status=\"completed\".\n"
72+
"5. Send ONE final conversational assistant message summarizing the "
73+
"plan, then stop. Do not call any more tools after step 3 is "
74+
"completed.\n"
75+
"\n"
76+
"Rules: never call set_steps in parallel — always wait for one call to "
77+
"return before the next. Do NOT use write_todos — use set_steps only. "
78+
"After all three steps are completed you MUST send a final assistant "
79+
"message and terminate."
7080
)
7181

7282
graph = create_deep_agent(
Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
11
"use client";
22

33
import React from "react";
4-
// v1 CopilotKit provider enables the v1 `useCoAgentStateRender` hook.
5-
// Under the hood it also wraps the v2 provider, so v2 components such as
6-
// `<CopilotChat />` and `useConfigureSuggestions` still work inside it.
7-
import { CopilotKit, useCoAgentStateRender } from "@copilotkit/react-core";
4+
import { CopilotKit } from "@copilotkit/react-core";
85
import {
96
CopilotChat,
7+
useAgent,
8+
UseAgentUpdate,
109
useConfigureSuggestions,
1110
} from "@copilotkit/react-core/v2";
12-
import { InlineAgentStateCard, type Step } from "./InlineAgentStateCard";
11+
import { InlineAgentStateCard } from "./InlineAgentStateCard";
12+
import type { Step } from "./InlineAgentStateCard";
1313

1414
/**
15-
* Agentic Generative UI — v1 In-Chat State Rendering
15+
* Agentic Generative UI — In-Chat State Rendering
1616
*
17-
* A minimal deep agent defines its OWN state schema (`steps: list[Step]`) on
18-
* the backend and exposes a custom `set_steps` tool that the model calls to
19-
* mutate that state. Every `set_steps` call streams the updated `steps` to
20-
* the client. The v1 `useCoAgentStateRender` hook subscribes to that state
21-
* and renders an inline progress tracker inside the chat transcript — no
22-
* `messageView` plumbing, no manual `useAgent` subscription.
17+
* The deep agent on the backend defines its own state schema
18+
* (`steps: list[Step]`) and exposes a custom `set_steps` tool that the model
19+
* calls to mutate that state. Every `set_steps` call streams the updated
20+
* `steps` to the client.
2321
*
24-
* Reference: https://docs.copilotkit.ai/reference/v1/hooks/useCoAgentStateRender
22+
* On the client we subscribe to that live state via `useAgent` (v2) and
23+
* render a single `InlineAgentStateCard` inside the chat transcript via
24+
* `messageView.children`. The card re-renders in place as state arrives —
25+
* no per-message claims, no duplicate cards.
26+
*
27+
* This mirrors the pattern used by every other integration's gen-ui-agent
28+
* demo (mastra, strands, ag2, agno, crewai-crews, langgraph-typescript,
29+
* pydantic-ai, ...) and replaces the earlier `useCoAgentStateRender`
30+
* approach which produced one card per state-changing message.
2531
*/
2632
export default function GenUiAgentDemo() {
2733
return (
@@ -35,13 +41,16 @@ export default function GenUiAgentDemo() {
3541
);
3642
}
3743

38-
// State shape mirrors the deep agent's explicit `steps` field, declared in
39-
// `GenUiAgentState` on the backend and mutated by the custom `set_steps` tool.
4044
type AgentState = {
4145
steps?: Step[];
4246
};
4347

4448
function Chat() {
49+
const { agent } = useAgent({
50+
agentId: "gen-ui-agent",
51+
updates: [UseAgentUpdate.OnStateChanged],
52+
});
53+
4554
useConfigureSuggestions({
4655
suggestions: [
4756
{
@@ -61,21 +70,24 @@ function Chat() {
6170
available: "always",
6271
});
6372

64-
// @region[use-coagent-state-render]
65-
// Subscribe to the deep agent's `steps` state. The custom `set_steps` tool
66-
// updates it every time the model advances a step, streaming each
67-
// transition to the UI where this `render` callback re-runs with the
68-
// fresh state and an updated `status` ("inProgress" while the agent is
69-
// running, "complete" when done).
70-
useCoAgentStateRender<AgentState>({
71-
name: "gen-ui-agent",
72-
render: ({ state, status }) => {
73-
const steps = state?.steps ?? [];
74-
if (steps.length === 0) return null;
75-
return <InlineAgentStateCard steps={steps} status={status} />;
76-
},
77-
});
78-
// @endregion[use-coagent-state-render]
73+
const steps = (agent.state as AgentState | undefined)?.steps ?? [];
74+
const status = agent.isRunning ? "inProgress" : "complete";
7975

80-
return <CopilotChat agentId="gen-ui-agent" className="h-full rounded-2xl" />;
76+
return (
77+
<CopilotChat
78+
agentId="gen-ui-agent"
79+
className="h-full rounded-2xl"
80+
messageView={{
81+
children: ({ messageElements, interruptElement }) => (
82+
<div data-testid="copilot-message-list" className="flex flex-col">
83+
{messageElements}
84+
{steps.length > 0 && (
85+
<InlineAgentStateCard steps={steps} status={status} />
86+
)}
87+
{interruptElement}
88+
</div>
89+
),
90+
}}
91+
/>
92+
);
8193
}

showcase/integrations/langgraph-python/tests/e2e/gen-ui-agent.spec.ts

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,44 +20,62 @@ test.describe("Agentic Generative UI", () => {
2020
});
2121

2222
test("message list container exists", async ({ page }) => {
23-
// The custom messageView renders a container with data-testid
2423
await expect(
2524
page.locator('[data-testid="copilot-message-list"]'),
2625
).toBeVisible({ timeout: 10000 });
2726
});
2827

29-
test("complex task triggers task progress tracker", async ({ page }) => {
28+
// Regression: every set_steps tool call used to push a brand-new card into
29+
// the chat (one card per state-changing message), so a 7-call run produced
30+
// 7+ stacked duplicate cards. The fix moved the demo from
31+
// `useCoAgentStateRender` (V1, per-message claiming) to V2 `useAgent` +
32+
// `messageView.children`, which renders a single live-updating card. This
33+
// test pins that contract — one card, regardless of how many state updates
34+
// arrive during the run.
35+
test("renders a single agent-state-card that updates in place", async ({
36+
page,
37+
}) => {
3038
const input = page.getByPlaceholder("Type a message");
31-
await input.fill(
32-
"Create a comprehensive dashboard showing sales metrics, revenue trends, and customer segments",
33-
);
39+
await input.fill("Plan a product launch for a new mobile app.");
3440
await input.press("Enter");
3541

36-
// The TaskProgress component should appear when the agent reports steps
37-
const taskProgress = page.locator('[data-testid="task-progress"]');
38-
await expect(taskProgress).toBeVisible({ timeout: 60000 });
39-
40-
// Should show a "Task Progress" heading
41-
await expect(taskProgress.getByText("Task Progress")).toBeVisible();
42+
const card = page.locator('[data-testid="agent-state-card"]');
43+
await expect(card).toBeVisible({ timeout: 60000 });
4244

43-
// Should show a completion counter like "X/Y Complete"
44-
await expect(taskProgress.getByText(/\d+\/\d+\s*Complete/)).toBeVisible();
45+
// Wait for at least one step to be published, then assert there is still
46+
// only one card (not one per state update).
47+
await expect(
48+
page.locator('[data-testid="agent-step"]').first(),
49+
).toBeVisible({ timeout: 60000 });
50+
await expect(card).toHaveCount(1);
4551

46-
// Step descriptions should be visible
47-
const stepTexts = page.locator('[data-testid="task-step-text"]');
48-
await expect(stepTexts.first()).toBeVisible({ timeout: 5000 });
52+
// Wait until the agent finishes the run, then re-assert single card.
53+
// `agent.isRunning` flips to false → the card's spinner becomes a check.
54+
await expect(card.locator(".animate-spin")).toHaveCount(0, {
55+
timeout: 120000,
56+
});
57+
await expect(card).toHaveCount(1);
4958
});
5059

51-
test("task progress shows progress bar", async ({ page }) => {
60+
test("eventually marks every step as completed", async ({ page }) => {
5261
const input = page.getByPlaceholder("Type a message");
53-
await input.fill("Build a report analyzing quarterly performance data");
62+
await input.fill("Plan a product launch for a new mobile app.");
5463
await input.press("Enter");
5564

56-
const taskProgress = page.locator('[data-testid="task-progress"]');
57-
await expect(taskProgress).toBeVisible({ timeout: 60000 });
65+
// Wait for the run to settle (no in-progress markers anywhere on the page).
66+
await expect(
67+
page.locator('[data-testid="agent-step"][data-status="in_progress"]'),
68+
).toHaveCount(0, { timeout: 120000 });
69+
70+
const steps = page.locator('[data-testid="agent-step"]');
71+
const total = await steps.count();
72+
expect(total).toBeGreaterThan(0);
5873

59-
// The progress bar is a rounded-full div inside the tracker
60-
const progressBar = taskProgress.locator(".rounded-full").first();
61-
await expect(progressBar).toBeVisible();
74+
// Every step must end in `completed` — guards against the "step 3 stuck"
75+
// regression where the agent terminated without flipping the last step.
76+
const completed = page.locator(
77+
'[data-testid="agent-step"][data-status="completed"]',
78+
);
79+
await expect(completed).toHaveCount(total);
6280
});
6381
});

0 commit comments

Comments
 (0)