Showcase

Multi-agent

Planner + worker + reviewer topology.

multi-agent
Live preview
Source
'use client'import { useMemo } from 'react'import type { AdapterFactory, StreamChunk, ToolDefinition } from '@agentskit/core'import { useChat, ChatContainer, InputBar } from '@agentskit/react'import '@/styles/agentskit-theme.css'import { initialAssistant } from './_shared/mock-adapter'import { ToolBadge } from './_shared/tool-badge'import { MdRenderer } from './_shared/md-renderer'/** * Narrative demo of a planner → researcher → drafter → reviewer topology. * One send cycles through four agent tools (each executed by the runtime) * then streams a composed markdown report showing every agent's contribution. */type AgentTool = {  name: string  args: Record<string, unknown>  result: unknown  durationMs: number}const PLAN_TOOLS: AgentTool[] = [  {    name: 'planner.decompose',    args: { goal: 'Launch announcement for AgentsKit 2.0' },    result: {      subtasks: [        'Gather shipped features since 1.8',        'Draft a punchy hero paragraph',        'Bullet the top three developer wins',        'Review tone + compliance',      ],    },    durationMs: 420,  },  {    name: 'researcher.search',    args: { query: 'AgentsKit 2.0 changelog highlights' },    result: {      hits: 8,      highlights: [        'New shared-state hooks (useFramework / useProvider / useMemory)',        '19 provider adapters, 10 memory backends',        'First-class tool_call streaming + confirmation gates',      ],    },    durationMs: 560,  },  {    name: 'drafter.compose',    args: { tone: 'confident-but-chill' },    result: { words: 142, sections: 3 },    durationMs: 640,  },  {    name: 'reviewer.critique',    args: { target: 'draft-v1' },    result: { verdict: 'approved', edits: ['tighten the CTA line', 'drop the "very"'] },    durationMs: 380,  },]const FINAL_REPORT = `### Draft ready> Shipped by the planner → researcher → drafter → reviewer topology.**Title.** AgentsKit 2.0 — the agent toolkit JavaScript actually deserves.**Hero.** AgentsKit 2.0 is the biggest release since launch. One mental model, 19 providers, 10 memory backends, and a shared-state layer so your stack selector, docs, and IDE agree on what you picked.**Top wins**1. **Shared stack hooks** — \`useFramework\`, \`useProvider\`, \`useMemory\`, \`usePackageManager\` sync across every surface.2. **Tool-first streaming** — \`tool_call\` chunks, confirmation gates, and live \`ToolBadge\` UI out of the box.3. **Bring your own provider** — openai, anthropic, gemini, grok, groq, mistral, together, cohere, deepseek, fireworks, huggingface, kimi, llamacpp, lm-studio, vllm, langchain, vercel-ai.**Reviewer notes.** Approved. Minor edits: tighten the CTA line, drop the \\"very\\". Shipping to the blog queue.\`\`\`ts// ship itawait publish('blog/agentskit-2-0.mdx', { status: 'scheduled' })\`\`\``const PLANNER_REASONING = `[planner] decompose the launch brief into research → draft → review.[planner] hand research off to researcher with search scope "changelog since 1.8".[planner] drafter gets the highlights + tone brief.[planner] reviewer has final sign-off before queue.`function createMultiAgentAdapter(): AdapterFactory {  let phase: 'tools' | 'final' = 'tools'  return {    createSource: () => ({      stream: async function* (): AsyncIterableIterator<StreamChunk> {        if (phase === 'tools') {          for (const ch of PLANNER_REASONING) {            await sleep(10)            yield { type: 'reasoning', content: ch }          }          for (const tool of PLAN_TOOLS) {            yield {              type: 'tool_call',              toolCall: {                id: `call-${tool.name}`,                name: tool.name,                args: JSON.stringify(tool.args),              },            }          }          phase = 'final'          yield { type: 'done' }          return        }        phase = 'tools'        for (const ch of FINAL_REPORT) {          await sleep(8)          yield { type: 'text', content: ch }        }        yield { type: 'done' }      },      abort() {},    }),    capabilities: { streaming: true, tools: true, reasoning: true },  }}function buildTools(): ToolDefinition[] {  return PLAN_TOOLS.map<ToolDefinition>((t) => ({    name: t.name,    description: `Mock ${t.name}`,    schema: {},    async execute() {      await sleep(t.durationMs)      return JSON.stringify(t.result)    },  }))}function sleep(ms: number) {  return new Promise<void>((r) => setTimeout(r, ms))}export function MultiAgentChat() {  const adapter = useMemo(() => createMultiAgentAdapter(), [])  const tools = useMemo(() => buildTools(), [])  const chat = useChat({    adapter,    tools,    // Two passes: first streams reasoning + tool_calls, runtime executes the    // four agent tools, then the adapter's second pass streams the final    // composed markdown report.    maxToolIterations: 2,    initialMessages: [      initialAssistant(        'Planner → Researcher → Drafter → Reviewer. Ask me to draft something — you will see every hand-off live.',      ),    ],  })  return (    <div      data-ak-example      className="flex h-[640px] flex-col overflow-hidden rounded-lg border border-ak-border bg-ak-surface"    >      <ChatContainer className="flex-1 space-y-3 p-4">        {chat.messages          .filter((m) => m.role !== 'tool')          .map((m) => {            const reasoning = (m.metadata?.reasoning as string | undefined) ?? null            return (              <div key={m.id} className="flex flex-col gap-2">                {reasoning ? <ReasoningTrace text={reasoning} /> : null}                {m.toolCalls?.map((t) => (                  <ToolBadge key={t.id} call={t} />                ))}                {m.content ? (                  <div                    data-ak-message                    data-ak-role={m.role}                    className="rounded-lg bg-ak-midnight/40 p-3"                  >                    <MdRenderer content={m.content} />                  </div>                ) : null}              </div>            )          })}        {chat.status === 'streaming' ? (          <div className="inline-flex items-center gap-2 font-mono text-[11px] text-ak-graphite">            <span className="inline-block h-2 w-2 animate-pulse rounded-full bg-ak-blue" />            agents coordinating…          </div>        ) : null}      </ChatContainer>      <InputBar chat={chat} />    </div>  )}function ReasoningTrace({ text }: { text: string }) {  return (    <details className="rounded border border-ak-border bg-ak-midnight/40 p-2 font-mono text-[11px] text-ak-graphite">      <summary className="cursor-pointer uppercase tracking-widest">Planner reasoning</summary>      <pre className="mt-2 whitespace-pre-wrap">{text}</pre>    </details>  )}
import type { AdapterFactory, StreamChunk, ToolDefinition } from '@agentskit/core'export type ToolCallEmit = {  name: string  args?: Record<string, unknown>  result?: unknown  durationMs?: number}export type Turn = {  /** Text streamed as the assistant reply. */  text: string  /** Optional tool calls to emit before the text. The runtime executes each via   *  the matching tool stub exposed by `toolsFor(turns)`. */  toolCalls?: ToolCallEmit[]  /** Optional reasoning stream emitted before tool calls / text. */  reasoning?: string}export function createMockAdapter(turns: Turn[], cps = 80): AdapterFactory {  let idx = 0  return {    createSource: () => ({      stream: async function* (): AsyncIterableIterator<StreamChunk> {        const turn = turns[idx % turns.length]        idx += 1        if (turn.reasoning) {          for (const ch of turn.reasoning) {            await sleep(1000 / cps)            yield { type: 'reasoning', content: ch }          }        }        if (turn.toolCalls) {          for (const call of turn.toolCalls) {            yield {              type: 'tool_call',              toolCall: {                id: `call-${Math.random().toString(36).slice(2, 8)}`,                name: call.name,                args: JSON.stringify(call.args ?? {}),              },            }          }        }        for (const ch of turn.text) {          await sleep(1000 / cps)          yield { type: 'text', content: ch }        }        yield { type: 'done' }      },      abort() {},    }),    capabilities: { streaming: true, tools: true },  }}/** * Build a registry of tool stubs whose `execute()` resolves to the mocked * result declared for that tool name in `turns`. When the controller sees a * `tool_call` chunk from the mock adapter it looks up the name here, runs the * stub (with a simulated latency), and emits `tool_result`. */export function toolsFor(turns: Turn[]): ToolDefinition[] {  const byName = new Map<string, ToolCallEmit>()  for (const t of turns) {    for (const c of t.toolCalls ?? []) {      if (!byName.has(c.name)) byName.set(c.name, c)    }  }  return Array.from(byName.values()).map<ToolDefinition>((call) => ({    name: call.name,    description: `Mock ${call.name}`,    schema: {},    async execute() {      if (call.durationMs) await sleep(call.durationMs)      return JSON.stringify(call.result ?? { ok: true })    },  }))}function sleep(ms: number) {  return new Promise<void>((r) => setTimeout(r, ms))}export function initialAssistant(content: string) {  return {    id: 'init',    role: 'assistant' as const,    content,    status: 'complete' as const,    createdAt: new Date(),  }}
'use client'import type { ToolCall } from '@agentskit/core'/** * Compact tool-call badge that mirrors the home hero demo: * `✓ name({ args }) Nms` in a green pill when complete, blue with a spinner * while pending. Use inside <Message>{m.toolCalls?.map(...)}</Message>. */export function ToolBadge({ call }: { call: ToolCall }) {  const done = call.status === 'complete' || call.status === 'error'  const error = call.status === 'error'  const args = formatArgs(call.args)  return (    <div      data-ak-tool-badge      className={`inline-flex max-w-full items-start gap-2 rounded-md border px-2.5 py-1 font-mono text-xs ${        error          ? 'border-ak-red/30 bg-ak-red/5 text-ak-red'          : done          ? 'border-ak-green/30 bg-ak-green/5 text-ak-green'          : 'border-ak-blue/30 bg-ak-blue/5 text-ak-blue'      }`}    >      <span className="mt-0.5 shrink-0">        {done ? (error ? '✗' : '✓') : <Spinner />}      </span>      <span className="min-w-0 break-all">        {call.name}        {args ? `(${args})` : '()'}      </span>    </div>  )}function formatArgs(raw: Record<string, unknown> | string | undefined): string {  if (!raw) return ''  const parsed =    typeof raw === 'string'      ? (() => {          try {            return JSON.parse(raw)          } catch {            return raw          }        })()      : raw  if (!parsed || typeof parsed !== 'object') return String(parsed)  const entries = Object.entries(parsed)  if (entries.length === 0) return ''  return entries.map(([k, v]) => `${k}: ${stringify(v)}`).join(', ')}function stringify(value: unknown): string {  if (typeof value === 'string') return `"${value}"`  if (value === null) return 'null'  if (typeof value === 'object') return JSON.stringify(value)  return String(value)}function Spinner() {  return (    <span className="inline-block h-3 w-3 animate-spin rounded-full border-2 border-ak-blue border-t-transparent" />  )}
'use client'import ReactMarkdown from 'react-markdown'import remarkGfm from 'remark-gfm'import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'/** * Markdown renderer used across showcase examples. * react-markdown + remark-gfm handles tables, task lists, strikethrough, etc. * Fenced code blocks are rendered by fumadocs' DynamicCodeBlock for shiki * syntax highlighting consistent with the rest of the docs. */export function MdRenderer({ content }: { content: string }) {  return (    <div data-ak-md className="ak-md space-y-2 text-sm text-ak-foam">      <ReactMarkdown        remarkPlugins={[remarkGfm]}        components={{          code({ className, children }) {            const text = String(children ?? '').replace(/\n$/, '')            const match = /language-(\w+)/.exec(className ?? '')            // react-markdown v9 dropped the `inline` flag. Treat anything            // without a `language-*` class (and no newline) as inline.            if (!match && !text.includes('\n')) {              return (                <code className="rounded bg-ak-midnight px-1 py-0.5 font-mono text-[0.85em] text-ak-blue">                  {text}                </code>              )            }            return <DynamicCodeBlock lang={match?.[1] ?? 'text'} code={text} />          },          pre({ children }) {            // Block <code> is rendered directly by the `code` component above.            // Returning the children raw avoids nesting <div> (DynamicCodeBlock)            // inside <pre> or the surrounding <p>.            return <>{children}</>          },          a({ href, children }) {            return (              <a href={href} target="_blank" rel="noreferrer" className="text-ak-blue underline">                {children}              </a>            )          },          table({ children }) {            return (              <div className="my-2 overflow-x-auto">                <table className="w-full border-collapse text-left text-xs">{children}</table>              </div>            )          },          thead({ children }) {            return <thead className="bg-ak-midnight/60 text-ak-foam">{children}</thead>          },          th({ children }) {            return <th className="border-b border-ak-border px-3 py-2 font-semibold">{children}</th>          },          td({ children }) {            return <td className="border-b border-ak-border px-3 py-2 align-top">{children}</td>          },          blockquote({ children }) {            return (              <blockquote className="border-l-2 border-ak-border pl-3 text-ak-graphite">                {children}              </blockquote>            )          },          ul({ children }) {            return <ul className="my-1 list-disc space-y-1 pl-5">{children}</ul>          },          ol({ children }) {            return <ol className="my-1 list-decimal space-y-1 pl-5">{children}</ol>          },          h1({ children }) {            return <h2 className="mt-2 font-display text-lg font-semibold text-ak-foam">{children}</h2>          },          h2({ children }) {            return <h3 className="mt-2 font-display text-base font-semibold text-ak-foam">{children}</h3>          },          h3({ children }) {            return <h4 className="mt-1 font-display text-sm font-semibold text-ak-foam">{children}</h4>          },          p({ children }) {            // Render every paragraph as a div to avoid invalid nesting when            // fenced code blocks (rendered as <figure>/<div> via DynamicCodeBlock)            // land inside a <p> because of react-markdown's default wrapping.            return <div className="leading-relaxed">{children}</div>          },        }}      >        {content}      </ReactMarkdown>    </div>  )}
More examples
See all →