Skip to content

Commit a0770ea

Browse files
authored
feat(showcase): close 28 unsupported gaps — 93.5% → 97.4% coverage (CopilotKit#4545)
## Summary Closes 28 of 29 "unsupported" showcase cells, moving the dashboard from 673 to 701 wired features (93.5% → 97.4% coverage). ### Interrupt demos (26 cells, 13 integrations) - Added gen-ui-interrupt + interrupt-headless to: ag2, agno, built-in-agent, claude-sdk-python, claude-sdk-typescript, crewai-crews, google-adk, langroid, llamaindex, mastra, pydantic-ai, spring-ai, strands - Uses MS Agent Python "Strategy B": useFrontendTool + async Promise handler — no native interrupt primitive needed - Backend agents use system prompt + tools=[] — CopilotKit runtime routes tool calls to frontend ### Spring AI additional features (2 cells) - **byoc-json-render**: zero-tool agent + @json-render frontend (routine port, PARITY_NOTES justification was incorrect) - **shared-state-streaming**: per-token STATE_SNAPSHOT emission via tool-call argument interception in a dedicated Spring controller ### What remains unsupported (1 cell) - hitl on built-in-agent — genuinely unsupported (TanStack AI chat-completions has no graph to pause). Equivalent UX ships as hitl-in-chat. ## Test plan - [x] showcase-harness: 1480 tests pass (91 files) - [ ] CI green - [ ] Registry regeneration validates 701 wired / 1 unsupported
2 parents 7d04e0c + be01722 commit a0770ea

121 files changed

Lines changed: 9520 additions & 1175 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

showcase/integrations/ag2/manifest.yaml

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ generative_ui:
1616
- constrained-explicit
1717
- a2ui-fixed-schema
1818
- a2ui-dynamic-schema
19-
not_supported_features:
20-
- gen-ui-interrupt
21-
- interrupt-headless
19+
not_supported_features: []
2220
interaction_modalities:
2321
- sidebar
2422
- embedded
@@ -55,6 +53,8 @@ features:
5553
- frontend-tools-async
5654
- shared-state-read-write
5755
- subagents
56+
- gen-ui-interrupt
57+
- interrupt-headless
5858
- auth
5959
- agent-config
6060
- voice
@@ -338,6 +338,29 @@ demos:
338338
- src/app/demos/subagents/page.tsx
339339
- src/app/demos/subagents/delegation-log.tsx
340340
- src/app/api/copilotkit/route.ts
341+
- id: gen-ui-interrupt
342+
name: Gen UI Interrupt (Frontend Tool + async Promise)
343+
description: In-chat time-picker card via useFrontendTool with an async handler that blocks until the user picks a slot — AG2 adaptation of the LangGraph interrupt() primitive
344+
tags:
345+
- interactivity
346+
route: /demos/gen-ui-interrupt
347+
animated_preview_url:
348+
highlight:
349+
- src/agents/interrupt_agent.py
350+
- src/app/demos/gen-ui-interrupt/page.tsx
351+
- src/app/demos/gen-ui-interrupt/time-picker-card.tsx
352+
- src/app/api/copilotkit/route.ts
353+
- id: interrupt-headless
354+
name: Headless Interrupt (Frontend Tool + async Promise)
355+
description: Time-picker popup rendered outside the chat in the app surface via useFrontendTool with an async handler — AG2 adaptation of the LangGraph headless interrupt pattern
356+
tags:
357+
- interactivity
358+
route: /demos/interrupt-headless
359+
animated_preview_url:
360+
highlight:
361+
- src/agents/interrupt_agent.py
362+
- src/app/demos/interrupt-headless/page.tsx
363+
- src/app/api/copilotkit/route.ts
341364
- id: declarative-gen-ui
342365
name: Declarative Generative UI (A2UI Dynamic Schema)
343366
description: Agent dynamically composes UI from a registered catalog of branded components via the A2UI middleware

showcase/integrations/ag2/src/agent_server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
shared_state_read_write_app,
3535
)
3636
from agents.subagents import subagents_app
37+
from agents.interrupt_agent import interrupt_app
3738
from agents.tool_rendering_reasoning_chain import (
3839
tool_rendering_reasoning_chain_app,
3940
)
@@ -90,6 +91,11 @@ async def dispatch(self, request, call_next):
9091
app.mount("/byoc-hashbrown", byoc_hashbrown_app)
9192
app.mount("/byoc-json-render", byoc_json_render_app)
9293

94+
# Interrupt-adapted scheduling agent. Shared by gen-ui-interrupt and
95+
# interrupt-headless demos — backend has tools=[], the frontend provides
96+
# `schedule_meeting` via `useFrontendTool` with an async Promise handler.
97+
app.mount("/interrupt-adapted", interrupt_app)
98+
9399

94100
# Mount the default AG2 AG-UI endpoint at the root.
95101
# `app.mount("/", ...)` is a catch-all Mount that shadows any later route
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
AG2 scheduling agent -- interrupt-adapted.
3+
4+
This agent powers two demos (gen-ui-interrupt, interrupt-headless) that in the
5+
LangGraph showcase rely on the native `interrupt()` primitive with
6+
checkpoint/resume. AG2 does NOT have that primitive, so we adapt using the
7+
same "Strategy B" pattern as the MS Agent Framework port: the backend agent's
8+
system prompt tells the LLM to call `schedule_meeting`, but no local
9+
implementation is registered -- the tool is provided entirely by the frontend
10+
via `useFrontendTool` with an async handler that returns a Promise resolving
11+
only once the user picks a time slot (or cancels).
12+
13+
See `src/agents/agent.py` for the shared ConversableAgent used by most other
14+
AG2 demos.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
from autogen import ConversableAgent, LLMConfig
20+
from autogen.ag_ui import AGUIStream
21+
from fastapi import FastAPI
22+
23+
24+
# @region[backend-tool-call]
25+
SYSTEM_PROMPT = (
26+
"You are a scheduling assistant. Whenever the user asks you to book a call "
27+
"or schedule a meeting, you MUST call the `schedule_meeting` tool. Pass a "
28+
"short `topic` describing the purpose of the meeting and, if known, an "
29+
"`attendee` describing who the meeting is with.\n\n"
30+
"The `schedule_meeting` tool is implemented on the client: it surfaces a "
31+
"time-picker UI to the user and returns the user's selection. After the "
32+
"tool returns, briefly confirm whether the meeting was scheduled and at "
33+
"what time, or note that the user cancelled. Do NOT ask for approval "
34+
"yourself -- always call the tool and let the picker handle the decision.\n\n"
35+
"Keep responses short and friendly. After you finish executing tools, "
36+
"always send a brief final assistant message summarizing what happened so "
37+
"the message persists."
38+
)
39+
40+
interrupt_agent = ConversableAgent(
41+
name="scheduling_agent",
42+
system_message=SYSTEM_PROMPT,
43+
llm_config=LLMConfig({"model": "gpt-4o-mini", "stream": True}),
44+
human_input_mode="NEVER",
45+
max_consecutive_auto_reply=5,
46+
# No backend tools. `schedule_meeting` is registered on the frontend
47+
# via `useFrontendTool` and dispatched through the CopilotKit runtime.
48+
# When the agent calls `schedule_meeting`, the request is routed to
49+
# the frontend handler, which returns a Promise that only resolves
50+
# once the user picks a slot -- equivalent to `interrupt()` in the
51+
# LangGraph reference.
52+
functions=[],
53+
)
54+
# @endregion[backend-tool-call]
55+
56+
# AG-UI stream wrapper
57+
interrupt_stream = AGUIStream(interrupt_agent)
58+
59+
# FastAPI sub-app so agent_server.py can mount at /interrupt-adapted
60+
interrupt_app = FastAPI(title="AG2 Interrupt Agent")
61+
interrupt_app.mount("/", interrupt_stream.build_asgi())

showcase/integrations/ag2/src/app/api/copilotkit/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,21 @@ const dedicatedAgents: Record<string, string> = {
6161
"agent-config-demo": "/agent-config/",
6262
};
6363

64+
// Interrupt-adapted demos: gen-ui-interrupt and interrupt-headless share the
65+
// same AG2 scheduling agent at /interrupt-adapted. The agent has tools=[];
66+
// `schedule_meeting` is provided by the frontend via `useFrontendTool`.
67+
const interruptAgentNames = ["gen-ui-interrupt", "interrupt-headless"];
68+
6469
const agents: Record<string, AbstractAgent> = {};
6570
for (const name of sharedAgentNames) {
6671
agents[name] = createAgent();
6772
}
6873
for (const [name, path] of Object.entries(dedicatedAgents)) {
6974
agents[name] = createAgent(path);
7075
}
76+
for (const name of interruptAgentNames) {
77+
agents[name] = createAgent("/interrupt-adapted/");
78+
}
7179
agents["default"] = createAgent();
7280

7381
console.log(
Lines changed: 112 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,121 @@
11
"use client";
22

3-
/**
4-
* gen-ui-interrupt — NOT SUPPORTED on AG2
5-
*
6-
* The LangGraph version of this demo uses LangGraph's native `interrupt()`
7-
* primitive, which pauses the running graph and emits a resumable interrupt
8-
* payload over the AG-UI stream. The frontend then resumes the SAME graph
9-
* run from a persisted checkpoint via
10-
* `copilotkit.runAgent({ forwardedProps: { command: { resume } } })`.
11-
*
12-
* AG2's `human_input_mode` is a synchronous request/reply hook on a
13-
* `ConversableAgent`; it does not pause-and-resume the same run from a
14-
* persisted checkpoint, so the resumable interrupt round-trip cannot be
15-
* reproduced faithfully.
16-
*
17-
* For an inline-in-chat HITL surface on AG2 see `/demos/hitl-in-chat`,
18-
* which uses the higher-level `useHumanInTheLoop` hook (the same UX that
19-
* langgraph's `useInterrupt` exposes via `useHumanInTheLoop` on top).
20-
*
21-
* For an out-of-chat HITL surface on AG2 see `/demos/hitl-in-app`.
22-
*/
3+
// Gen UI Interrupt demo (AG2 port).
4+
//
5+
// The LangGraph version of this demo uses `useInterrupt` with LangGraph's
6+
// native `interrupt()` primitive — the backend pauses the run and surfaces
7+
// a payload that the frontend renders into the chat via the `useInterrupt`
8+
// hook. AG2 does NOT have an equivalent interrupt primitive, so we adapt
9+
// the demo by registering a frontend tool with `useFrontendTool`. The
10+
// handler returns a Promise that only resolves once the user picks a time
11+
// (or cancels), which produces the same UX: the picker appears inline in
12+
// the chat and the agent's tool call blocks until the user decides.
2313

24-
import React from "react";
25-
import Link from "next/link";
14+
import React, { useRef } from "react";
15+
import { CopilotKit } from "@copilotkit/react-core";
16+
import {
17+
CopilotChat,
18+
useConfigureSuggestions,
19+
useFrontendTool,
20+
} from "@copilotkit/react-core/v2";
21+
import { z } from "zod";
22+
import { TimePickerCard, TimeSlot } from "./time-picker-card";
2623

27-
export default function GenUiInterruptUnsupportedPage() {
24+
const DEFAULT_SLOTS: TimeSlot[] = [
25+
{ label: "Tomorrow 10:00 AM", iso: "2026-04-25T10:00:00-07:00" },
26+
{ label: "Tomorrow 2:00 PM", iso: "2026-04-25T14:00:00-07:00" },
27+
{ label: "Monday 9:00 AM", iso: "2026-04-28T09:00:00-07:00" },
28+
{ label: "Monday 3:30 PM", iso: "2026-04-28T15:30:00-07:00" },
29+
];
30+
31+
type PickerResult =
32+
| { chosen_time: string; chosen_label: string }
33+
| { cancelled: true };
34+
35+
export default function GenUiInterruptDemo() {
2836
return (
29-
<div className="flex justify-center items-center h-screen w-full p-6">
30-
<div className="max-w-2xl w-full rounded-2xl border border-[var(--border)] bg-[var(--card)] p-8 shadow-sm">
31-
<div className="text-xs uppercase tracking-wider text-[var(--muted-foreground)] mb-2">
32-
Not supported on AG2
33-
</div>
34-
<h1 className="text-2xl font-semibold mb-3">
35-
gen-ui-interrupt is not available on AG2
36-
</h1>
37-
<p className="text-sm text-[var(--muted-foreground)] mb-4 leading-relaxed">
38-
This demo depends on LangGraph&apos;s native{" "}
39-
<code className="px-1 rounded bg-[var(--muted)]">interrupt()</code>{" "}
40-
primitive — the graph pauses, emits a resumable payload over the AG-UI
41-
stream, and the frontend resumes the same run from a persisted
42-
checkpoint. AG2&apos;s{" "}
43-
<code className="px-1 rounded bg-[var(--muted)]">
44-
human_input_mode
45-
</code>{" "}
46-
is a synchronous request/reply hook and does not round-trip a
47-
resumable pause through the event stream, so the AG-UI interrupt
48-
contract cannot be reproduced faithfully.
49-
</p>
50-
<p className="text-sm text-[var(--muted-foreground)] mb-6 leading-relaxed">
51-
Equivalent in-chat HITL flows on AG2 are available via the
52-
higher-level{" "}
53-
<code className="px-1 rounded bg-[var(--muted)]">
54-
useHumanInTheLoop
55-
</code>{" "}
56-
hook.
57-
</p>
58-
<div className="flex flex-wrap gap-3">
59-
<Link
60-
href="/demos/hitl-in-chat"
61-
className="inline-flex items-center px-4 py-2 rounded-lg border border-[var(--border)] bg-[var(--card)] hover:bg-[var(--muted)] text-sm font-medium"
62-
>
63-
Try hitl-in-chat (in-chat HITL on AG2) →
64-
</Link>
65-
<Link
66-
href="/demos/hitl-in-app"
67-
className="inline-flex items-center px-4 py-2 rounded-lg border border-[var(--border)] bg-[var(--card)] hover:bg-[var(--muted)] text-sm font-medium"
68-
>
69-
Try hitl-in-app (out-of-chat HITL on AG2) →
70-
</Link>
37+
<CopilotKit runtimeUrl="/api/copilotkit" agent="gen-ui-interrupt">
38+
<div className="flex justify-center items-center h-screen w-full">
39+
<div className="h-full w-full max-w-4xl">
40+
<Chat />
7141
</div>
7242
</div>
73-
</div>
43+
</CopilotKit>
44+
);
45+
}
46+
47+
function Chat() {
48+
// Pending-resolver ref: set by the async handler, called by the render
49+
// prop when the user clicks a slot or cancels. This is the AG2
50+
// adaptation of the LangGraph `resolve(...)` callback.
51+
const resolverRef = useRef<((result: PickerResult) => void) | null>(null);
52+
53+
useConfigureSuggestions({
54+
suggestions: [
55+
{
56+
title: "Book a call with sales",
57+
message: "Book an intro call with the sales team to discuss pricing.",
58+
},
59+
{
60+
title: "Schedule a 1:1 with Alice",
61+
message: "Schedule a 1:1 with Alice next week to review Q2 goals.",
62+
},
63+
],
64+
available: "always",
65+
});
66+
67+
// @region[frontend-promise-handler]
68+
useFrontendTool({
69+
name: "schedule_meeting",
70+
description:
71+
"Ask the user to pick a time slot for a meeting via an in-chat " +
72+
"picker. Blocks until the user chooses a slot or cancels.",
73+
parameters: z.object({
74+
topic: z
75+
.string()
76+
.describe("Short human-readable description of the meeting."),
77+
attendee: z
78+
.string()
79+
.optional()
80+
.describe("Who the meeting is with (optional)."),
81+
}),
82+
// Async handler: returns a Promise that resolves only once the user
83+
// acts on the picker. This is the AG2 shim for LangGraph's
84+
// `interrupt()`/`resolve()` pair.
85+
handler: async (): Promise<string> => {
86+
const result = await new Promise<PickerResult>((resolve) => {
87+
resolverRef.current = resolve;
88+
});
89+
if ("cancelled" in result && result.cancelled) {
90+
return "User cancelled. Meeting NOT scheduled.";
91+
}
92+
if ("chosen_label" in result) {
93+
return `Meeting scheduled for ${result.chosen_label}.`;
94+
}
95+
return "User did not pick a time. Meeting NOT scheduled.";
96+
},
97+
render: ({ args, status }) => {
98+
if (status === "complete") return null;
99+
const topic =
100+
(args as { topic?: string } | undefined)?.topic ?? "a meeting";
101+
const attendee = (args as { attendee?: string } | undefined)?.attendee;
102+
return (
103+
<TimePickerCard
104+
topic={topic}
105+
attendee={attendee}
106+
slots={DEFAULT_SLOTS}
107+
onSubmit={(result) => {
108+
const fn = resolverRef.current;
109+
resolverRef.current = null;
110+
fn?.(result);
111+
}}
112+
/>
113+
);
114+
},
115+
});
116+
// @endregion[frontend-promise-handler]
117+
118+
return (
119+
<CopilotChat agentId="gen-ui-interrupt" className="h-full rounded-2xl" />
74120
);
75121
}

0 commit comments

Comments
 (0)