feat: build an AI agent from 0 to 1 -- 11 progressive sessions

- 11 sessions from basic agent loop to autonomous teams
- Python MVP implementations for each session
- Mental-model-first docs in en/zh/ja
- Interactive web platform with step-through visualizations
- Incremental architecture: each session adds one mechanism
This commit is contained in:
CrazyBoyM
2026-02-21 14:37:42 +08:00
committed by CrazyBoyM
commit c6a27ef1d7
156 changed files with 28059 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
"use client";
import { ArchDiagram } from "@/components/architecture/arch-diagram";
import { WhatsNew } from "@/components/diff/whats-new";
import { DesignDecisions } from "@/components/architecture/design-decisions";
import { DocRenderer } from "@/components/docs/doc-renderer";
import { SourceViewer } from "@/components/code/source-viewer";
import { AgentLoopSimulator } from "@/components/simulator/agent-loop-simulator";
import { ExecutionFlow } from "@/components/architecture/execution-flow";
import { SessionVisualization } from "@/components/visualizations";
import { Tabs } from "@/components/ui/tabs";
import { useTranslations } from "@/lib/i18n";
interface VersionDetailClientProps {
version: string;
diff: {
from: string;
to: string;
newClasses: string[];
newFunctions: string[];
newTools: string[];
locDelta: number;
} | null;
source: string;
filename: string;
}
export function VersionDetailClient({
version,
diff,
source,
filename,
}: VersionDetailClientProps) {
const t = useTranslations("version");
const tabs = [
{ id: "learn", label: t("tab_learn") },
{ id: "simulate", label: t("tab_simulate") },
{ id: "code", label: t("tab_code") },
{ id: "deep-dive", label: t("tab_deep_dive") },
];
return (
<div className="space-y-6">
{/* Hero Visualization */}
<SessionVisualization version={version} />
{/* Tabbed content */}
<Tabs tabs={tabs} defaultTab="learn">
{(activeTab) => (
<>
{activeTab === "learn" && <DocRenderer version={version} />}
{activeTab === "simulate" && (
<AgentLoopSimulator version={version} />
)}
{activeTab === "code" && (
<SourceViewer source={source} filename={filename} />
)}
{activeTab === "deep-dive" && (
<div className="space-y-8">
<section>
<h2 className="mb-4 text-xl font-semibold">
{t("execution_flow")}
</h2>
<ExecutionFlow version={version} />
</section>
<section>
<h2 className="mb-4 text-xl font-semibold">
{t("architecture")}
</h2>
<ArchDiagram version={version} />
</section>
{diff && <WhatsNew diff={diff} />}
<DesignDecisions version={version} />
</div>
)}
</>
)}
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,202 @@
"use client";
import { useMemo } from "react";
import Link from "next/link";
import { useLocale } from "@/lib/i18n";
import { VERSION_META } from "@/lib/constants";
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import { LayerBadge } from "@/components/ui/badge";
import { CodeDiff } from "@/components/diff/code-diff";
import { ArrowLeft, Plus, Minus, FileCode, Wrench, Box, FunctionSquare } from "lucide-react";
import type { AgentVersion, VersionDiff, VersionIndex } from "@/types/agent-data";
import versionData from "@/data/generated/versions.json";
const data = versionData as VersionIndex;
interface DiffPageContentProps {
version: string;
}
export function DiffPageContent({ version }: DiffPageContentProps) {
const locale = useLocale();
const meta = VERSION_META[version];
const { currentVersion, prevVersion, diff } = useMemo(() => {
const current = data.versions.find((v) => v.id === version);
const prevId = meta?.prevVersion;
const prev = prevId ? data.versions.find((v) => v.id === prevId) : null;
const d = data.diffs.find((d) => d.to === version);
return { currentVersion: current, prevVersion: prev, diff: d };
}, [version, meta]);
if (!meta || !currentVersion) {
return (
<div className="py-12 text-center">
<p className="text-zinc-500">Version not found.</p>
<Link href={`/${locale}/timeline`} className="mt-4 inline-block text-sm text-blue-600 hover:underline">
Back to timeline
</Link>
</div>
);
}
if (!prevVersion || !diff) {
return (
<div className="py-12">
<Link
href={`/${locale}/${version}`}
className="mb-6 inline-flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
>
<ArrowLeft size={14} />
Back to {meta.title}
</Link>
<h1 className="text-3xl font-bold">{meta.title}</h1>
<p className="mt-4 text-zinc-500">
This is the first version -- there is no previous version to compare against.
</p>
</div>
);
}
const prevMeta = VERSION_META[prevVersion.id];
return (
<div className="py-4">
<Link
href={`/${locale}/${version}`}
className="mb-6 inline-flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
>
<ArrowLeft size={14} />
Back to {meta.title}
</Link>
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold">
{prevMeta?.title || prevVersion.id} {meta.title}
</h1>
<p className="mt-2 text-zinc-500 dark:text-zinc-400">
{prevVersion.id} ({prevVersion.loc} LOC) {version} ({currentVersion.loc} LOC)
</p>
</div>
{/* Structural Diff */}
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<FileCode size={16} />
<span className="text-sm">LOC Delta</span>
</div>
</CardHeader>
<CardTitle>
<span className={diff.locDelta >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
{diff.locDelta >= 0 ? "+" : ""}{diff.locDelta}
</span>
<span className="ml-2 text-sm font-normal text-zinc-500">lines</span>
</CardTitle>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<Wrench size={16} />
<span className="text-sm">New Tools</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-blue-600 dark:text-blue-400">{diff.newTools.length}</span>
</CardTitle>
{diff.newTools.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{diff.newTools.map((tool) => (
<span key={tool} className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
{tool}
</span>
))}
</div>
)}
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<Box size={16} />
<span className="text-sm">New Classes</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-purple-600 dark:text-purple-400">{diff.newClasses.length}</span>
</CardTitle>
{diff.newClasses.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{diff.newClasses.map((cls) => (
<span key={cls} className="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
{cls}
</span>
))}
</div>
)}
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<FunctionSquare size={16} />
<span className="text-sm">New Functions</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-amber-600 dark:text-amber-400">{diff.newFunctions.length}</span>
</CardTitle>
{diff.newFunctions.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{diff.newFunctions.map((fn) => (
<span key={fn} className="rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
{fn}
</span>
))}
</div>
)}
</Card>
</div>
{/* Version Info Comparison */}
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2">
<Card className="border-l-4 border-l-red-300 dark:border-l-red-700">
<CardHeader>
<CardTitle>{prevMeta?.title || prevVersion.id}</CardTitle>
<p className="text-sm text-zinc-500">{prevMeta?.subtitle}</p>
</CardHeader>
<div className="space-y-1 text-sm text-zinc-600 dark:text-zinc-400">
<p>{prevVersion.loc} LOC</p>
<p>{prevVersion.tools.length} tools: {prevVersion.tools.join(", ")}</p>
<LayerBadge layer={prevVersion.layer}>{prevVersion.layer}</LayerBadge>
</div>
</Card>
<Card className="border-l-4 border-l-green-300 dark:border-l-green-700">
<CardHeader>
<CardTitle>{meta.title}</CardTitle>
<p className="text-sm text-zinc-500">{meta.subtitle}</p>
</CardHeader>
<div className="space-y-1 text-sm text-zinc-600 dark:text-zinc-400">
<p>{currentVersion.loc} LOC</p>
<p>{currentVersion.tools.length} tools: {currentVersion.tools.join(", ")}</p>
<LayerBadge layer={currentVersion.layer}>{currentVersion.layer}</LayerBadge>
</div>
</Card>
</div>
{/* Code Diff */}
<div>
<h2 className="mb-4 text-xl font-semibold">Source Code Diff</h2>
<CodeDiff
oldSource={prevVersion.source}
newSource={currentVersion.source}
oldLabel={`${prevVersion.id} (${prevVersion.filename})`}
newLabel={`${version} (${currentVersion.filename})`}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { LEARNING_PATH } from "@/lib/constants";
import { DiffPageContent } from "./diff-content";
export function generateStaticParams() {
return LEARNING_PATH.map((version) => ({ version }));
}
export default async function DiffPage({
params,
}: {
params: Promise<{ locale: string; version: string }>;
}) {
const { version } = await params;
return <DiffPageContent version={version} />;
}

View File

@@ -0,0 +1,125 @@
import Link from "next/link";
import { LEARNING_PATH, VERSION_META, LAYERS } from "@/lib/constants";
import { LayerBadge } from "@/components/ui/badge";
import versionsData from "@/data/generated/versions.json";
import { VersionDetailClient } from "./client";
import { getTranslations } from "@/lib/i18n-server";
export function generateStaticParams() {
return LEARNING_PATH.map((version) => ({ version }));
}
export default async function VersionPage({
params,
}: {
params: Promise<{ locale: string; version: string }>;
}) {
const { locale, version } = await params;
const versionData = versionsData.versions.find((v) => v.id === version);
const meta = VERSION_META[version];
const diff = versionsData.diffs.find((d) => d.to === version) ?? null;
if (!versionData || !meta) {
return (
<div className="py-20 text-center">
<h1 className="text-2xl font-bold">Version not found</h1>
<p className="mt-2 text-zinc-500">{version}</p>
</div>
);
}
const t = getTranslations(locale, "version");
const tSession = getTranslations(locale, "sessions");
const tLayer = getTranslations(locale, "layer_labels");
const layer = LAYERS.find((l) => l.id === meta.layer);
const pathIndex = LEARNING_PATH.indexOf(version as typeof LEARNING_PATH[number]);
const prevVersion = pathIndex > 0 ? LEARNING_PATH[pathIndex - 1] : null;
const nextVersion =
pathIndex < LEARNING_PATH.length - 1
? LEARNING_PATH[pathIndex + 1]
: null;
return (
<div className="mx-auto max-w-3xl space-y-10 py-4">
{/* Header */}
<header className="space-y-3">
<div className="flex flex-wrap items-center gap-3">
<span className="rounded-lg bg-zinc-100 px-3 py-1 font-mono text-lg font-bold dark:bg-zinc-800">
{version}
</span>
<h1 className="text-2xl font-bold sm:text-3xl">{tSession(version) || meta.title}</h1>
{layer && (
<LayerBadge layer={meta.layer}>{tLayer(layer.id)}</LayerBadge>
)}
</div>
<p className="text-lg text-zinc-500 dark:text-zinc-400">
{meta.subtitle}
</p>
<div className="flex flex-wrap items-center gap-4 text-sm text-zinc-500 dark:text-zinc-400">
<span className="font-mono">{versionData.loc} LOC</span>
<span>{versionData.tools.length} {t("tools")}</span>
{meta.coreAddition && (
<span className="rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs dark:bg-zinc-800">
{meta.coreAddition}
</span>
)}
</div>
{meta.keyInsight && (
<blockquote className="border-l-4 border-zinc-300 pl-4 text-sm italic text-zinc-500 dark:border-zinc-600 dark:text-zinc-400">
{meta.keyInsight}
</blockquote>
)}
</header>
{/* Client-rendered interactive sections */}
<VersionDetailClient
version={version}
diff={diff}
source={versionData.source}
filename={versionData.filename}
/>
{/* Prev / Next navigation */}
<nav className="flex items-center justify-between border-t border-zinc-200 pt-6 dark:border-zinc-700">
{prevVersion ? (
<Link
href={`/${locale}/${prevVersion}`}
className="group flex items-center gap-2 text-sm text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-white"
>
<span className="transition-transform group-hover:-translate-x-1">
&larr;
</span>
<div>
<div className="text-xs text-zinc-400">{t("prev")}</div>
<div className="font-medium">
{prevVersion} - {tSession(prevVersion) || VERSION_META[prevVersion]?.title}
</div>
</div>
</Link>
) : (
<div />
)}
{nextVersion ? (
<Link
href={`/${locale}/${nextVersion}`}
className="group flex items-center gap-2 text-right text-sm text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-white"
>
<div>
<div className="text-xs text-zinc-400">{t("next")}</div>
<div className="font-medium">
{tSession(nextVersion) || VERSION_META[nextVersion]?.title} - {nextVersion}
</div>
</div>
<span className="transition-transform group-hover:translate-x-1">
&rarr;
</span>
</Link>
) : (
<div />
)}
</nav>
</div>
);
}

View File

@@ -0,0 +1,308 @@
"use client";
import { useState, useMemo } from "react";
import { useLocale, useTranslations } from "@/lib/i18n";
import { LEARNING_PATH, VERSION_META } from "@/lib/constants";
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import { LayerBadge } from "@/components/ui/badge";
import { CodeDiff } from "@/components/diff/code-diff";
import { ArchDiagram } from "@/components/architecture/arch-diagram";
import { ArrowRight, FileCode, Wrench, Box, FunctionSquare } from "lucide-react";
import type { VersionIndex } from "@/types/agent-data";
import versionData from "@/data/generated/versions.json";
const data = versionData as VersionIndex;
export default function ComparePage() {
const t = useTranslations("compare");
const locale = useLocale();
const [versionA, setVersionA] = useState<string>("");
const [versionB, setVersionB] = useState<string>("");
const infoA = useMemo(() => data.versions.find((v) => v.id === versionA), [versionA]);
const infoB = useMemo(() => data.versions.find((v) => v.id === versionB), [versionB]);
const metaA = versionA ? VERSION_META[versionA] : null;
const metaB = versionB ? VERSION_META[versionB] : null;
const comparison = useMemo(() => {
if (!infoA || !infoB) return null;
const toolsA = new Set(infoA.tools);
const toolsB = new Set(infoB.tools);
const onlyA = infoA.tools.filter((t) => !toolsB.has(t));
const onlyB = infoB.tools.filter((t) => !toolsA.has(t));
const shared = infoA.tools.filter((t) => toolsB.has(t));
const classesA = new Set(infoA.classes.map((c) => c.name));
const classesB = new Set(infoB.classes.map((c) => c.name));
const newClasses = infoB.classes.map((c) => c.name).filter((c) => !classesA.has(c));
const funcsA = new Set(infoA.functions.map((f) => f.name));
const funcsB = new Set(infoB.functions.map((f) => f.name));
const newFunctions = infoB.functions.map((f) => f.name).filter((f) => !funcsA.has(f));
return {
locDelta: infoB.loc - infoA.loc,
toolsOnlyA: onlyA,
toolsOnlyB: onlyB,
toolsShared: shared,
newClasses,
newFunctions,
};
}, [infoA, infoB]);
return (
<div className="py-4">
<div className="mb-8">
<h1 className="text-3xl font-bold">{t("title")}</h1>
<p className="mt-2 text-zinc-500 dark:text-zinc-400">{t("subtitle")}</p>
</div>
{/* Selectors */}
<div className="mb-8 flex flex-col items-start gap-4 sm:flex-row sm:items-center">
<div className="flex-1">
<label className="mb-1 block text-sm font-medium text-zinc-600 dark:text-zinc-400">
{t("select_a")}
</label>
<select
value={versionA}
onChange={(e) => setVersionA(e.target.value)}
className="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200"
>
<option value="">-- select --</option>
{LEARNING_PATH.map((v) => (
<option key={v} value={v}>
{v} - {VERSION_META[v]?.title}
</option>
))}
</select>
</div>
<ArrowRight size={20} className="mt-5 hidden text-zinc-400 sm:block" />
<div className="flex-1">
<label className="mb-1 block text-sm font-medium text-zinc-600 dark:text-zinc-400">
{t("select_b")}
</label>
<select
value={versionB}
onChange={(e) => setVersionB(e.target.value)}
className="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200"
>
<option value="">-- select --</option>
{LEARNING_PATH.map((v) => (
<option key={v} value={v}>
{v} - {VERSION_META[v]?.title}
</option>
))}
</select>
</div>
</div>
{/* Results */}
{infoA && infoB && comparison && (
<div className="space-y-8">
{/* Side-by-side version info */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>{metaA?.title || versionA}</CardTitle>
<p className="text-sm text-zinc-500">{metaA?.subtitle}</p>
</CardHeader>
<div className="space-y-2 text-sm text-zinc-600 dark:text-zinc-400">
<p>{infoA.loc} LOC</p>
<p>{infoA.tools.length} tools</p>
{metaA && <LayerBadge layer={metaA.layer}>{metaA.layer}</LayerBadge>}
</div>
</Card>
<Card>
<CardHeader>
<CardTitle>{metaB?.title || versionB}</CardTitle>
<p className="text-sm text-zinc-500">{metaB?.subtitle}</p>
</CardHeader>
<div className="space-y-2 text-sm text-zinc-600 dark:text-zinc-400">
<p>{infoB.loc} LOC</p>
<p>{infoB.tools.length} tools</p>
{metaB && <LayerBadge layer={metaB.layer}>{metaB.layer}</LayerBadge>}
</div>
</Card>
</div>
{/* Side-by-side Architecture Diagrams */}
<div>
<h2 className="mb-4 text-xl font-semibold">{t("architecture")}</h2>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<div>
<h3 className="mb-3 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{metaA?.title || versionA}
</h3>
<ArchDiagram version={versionA} />
</div>
<div>
<h3 className="mb-3 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{metaB?.title || versionB}
</h3>
<ArchDiagram version={versionB} />
</div>
</div>
</div>
{/* Structural diff */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<FileCode size={16} />
<span className="text-sm">{t("loc_delta")}</span>
</div>
</CardHeader>
<CardTitle>
<span className={comparison.locDelta >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
{comparison.locDelta >= 0 ? "+" : ""}{comparison.locDelta}
</span>
<span className="ml-2 text-sm font-normal text-zinc-500">{t("lines")}</span>
</CardTitle>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<Wrench size={16} />
<span className="text-sm">{t("new_tools_in_b")}</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-blue-600 dark:text-blue-400">{comparison.toolsOnlyB.length}</span>
</CardTitle>
{comparison.toolsOnlyB.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{comparison.toolsOnlyB.map((tool) => (
<span key={tool} className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
{tool}
</span>
))}
</div>
)}
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<Box size={16} />
<span className="text-sm">{t("new_classes_in_b")}</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-purple-600 dark:text-purple-400">{comparison.newClasses.length}</span>
</CardTitle>
{comparison.newClasses.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{comparison.newClasses.map((cls) => (
<span key={cls} className="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
{cls}
</span>
))}
</div>
)}
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<FunctionSquare size={16} />
<span className="text-sm">{t("new_functions_in_b")}</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-amber-600 dark:text-amber-400">{comparison.newFunctions.length}</span>
</CardTitle>
{comparison.newFunctions.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{comparison.newFunctions.map((fn) => (
<span key={fn} className="rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
{fn}
</span>
))}
</div>
)}
</Card>
</div>
{/* Tool comparison */}
<Card>
<CardHeader>
<CardTitle>{t("tool_comparison")}</CardTitle>
</CardHeader>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
<div>
<h4 className="mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400">
{t("only_in")} {metaA?.title || versionA}
</h4>
{comparison.toolsOnlyA.length === 0 ? (
<p className="text-xs text-zinc-400">{t("none")}</p>
) : (
<div className="flex flex-wrap gap-1">
{comparison.toolsOnlyA.map((tool) => (
<span key={tool} className="rounded bg-red-100 px-1.5 py-0.5 text-xs text-red-700 dark:bg-red-900/30 dark:text-red-300">
{tool}
</span>
))}
</div>
)}
</div>
<div>
<h4 className="mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400">
{t("shared")}
</h4>
{comparison.toolsShared.length === 0 ? (
<p className="text-xs text-zinc-400">{t("none")}</p>
) : (
<div className="flex flex-wrap gap-1">
{comparison.toolsShared.map((tool) => (
<span key={tool} className="rounded bg-zinc-100 px-1.5 py-0.5 text-xs text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
{tool}
</span>
))}
</div>
)}
</div>
<div>
<h4 className="mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400">
{t("only_in")} {metaB?.title || versionB}
</h4>
{comparison.toolsOnlyB.length === 0 ? (
<p className="text-xs text-zinc-400">{t("none")}</p>
) : (
<div className="flex flex-wrap gap-1">
{comparison.toolsOnlyB.map((tool) => (
<span key={tool} className="rounded bg-green-100 px-1.5 py-0.5 text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300">
{tool}
</span>
))}
</div>
)}
</div>
</div>
</Card>
{/* Code Diff */}
<div>
<h2 className="mb-4 text-xl font-semibold">{t("source_diff")}</h2>
<CodeDiff
oldSource={infoA.source}
newSource={infoB.source}
oldLabel={`${infoA.id} (${infoA.filename})`}
newLabel={`${infoB.id} (${infoB.filename})`}
/>
</div>
</div>
)}
{/* Empty state */}
{(!versionA || !versionB) && (
<div className="rounded-lg border border-dashed border-zinc-300 p-12 text-center dark:border-zinc-700">
<p className="text-zinc-400">{t("empty_hint")}</p>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,133 @@
"use client";
import Link from "next/link";
import { useTranslations, useLocale } from "@/lib/i18n";
import { LAYERS, VERSION_META } from "@/lib/constants";
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import { LayerBadge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { ChevronRight } from "lucide-react";
import type { VersionIndex } from "@/types/agent-data";
import versionData from "@/data/generated/versions.json";
const data = versionData as VersionIndex;
const LAYER_BORDER_CLASSES: Record<string, string> = {
tools: "border-l-blue-500",
planning: "border-l-emerald-500",
memory: "border-l-purple-500",
concurrency: "border-l-amber-500",
collaboration: "border-l-red-500",
};
const LAYER_HEADER_BG: Record<string, string> = {
tools: "bg-blue-500",
planning: "bg-emerald-500",
memory: "bg-purple-500",
concurrency: "bg-amber-500",
collaboration: "bg-red-500",
};
export default function LayersPage() {
const t = useTranslations("layers");
const locale = useLocale();
return (
<div className="py-4">
<div className="mb-10">
<h1 className="text-3xl font-bold">{t("title")}</h1>
<p className="mt-2 text-zinc-500 dark:text-zinc-400">{t("subtitle")}</p>
</div>
<div className="space-y-6">
{LAYERS.map((layer, index) => {
const versionInfos = layer.versions.map((vId) => {
const info = data.versions.find((v) => v.id === vId);
const meta = VERSION_META[vId];
return { id: vId, info, meta };
});
return (
<div
key={layer.id}
className={cn(
"overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800",
"border-l-4",
LAYER_BORDER_CLASSES[layer.id]
)}
>
{/* Layer header */}
<div className="flex items-center gap-3 px-6 py-4">
<div className={cn("h-3 w-3 rounded-full", LAYER_HEADER_BG[layer.id])} />
<div>
<h2 className="text-xl font-bold">
<span className="text-zinc-400 dark:text-zinc-600">L{index + 1}</span>
{" "}
{layer.label}
</h2>
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
{t(layer.id)}
</p>
</div>
</div>
{/* Version cards within this layer */}
<div className="border-t border-zinc-200 bg-zinc-50/50 px-6 py-4 dark:border-zinc-800 dark:bg-zinc-900/50">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{versionInfos.map(({ id, info, meta }) => (
<Link
key={id}
href={`/${locale}/${id}`}
className="group"
>
<Card className="transition-shadow hover:shadow-md">
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-zinc-400">{id}</span>
<LayerBadge layer={layer.id}>{layer.id}</LayerBadge>
</div>
<h3 className="mt-1 font-semibold text-zinc-900 dark:text-zinc-100">
{meta?.title || id}
</h3>
{meta?.subtitle && (
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
{meta.subtitle}
</p>
)}
</div>
<ChevronRight
size={16}
className="mt-1 shrink-0 text-zinc-300 transition-colors group-hover:text-zinc-600 dark:text-zinc-600 dark:group-hover:text-zinc-300"
/>
</div>
<div className="mt-3 flex items-center gap-4 text-xs text-zinc-500 dark:text-zinc-400">
<span>{info?.loc ?? "?"} LOC</span>
<span>{info?.tools.length ?? "?"} tools</span>
</div>
{meta?.keyInsight && (
<p className="mt-2 text-xs leading-relaxed text-zinc-500 dark:text-zinc-400 line-clamp-2">
{meta.keyInsight}
</p>
)}
</Card>
</Link>
))}
</div>
</div>
{/* Composition indicator */}
{index < LAYERS.length - 1 && (
<div className="flex items-center justify-center py-1 text-zinc-300 dark:text-zinc-700">
<svg width="20" height="12" viewBox="0 0 20 12" fill="none" className="text-current">
<path d="M10 0 L10 12 M5 7 L10 12 L15 7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
)}
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { Sidebar } from "@/components/layout/sidebar";
export default function LearnLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex gap-8">
<Sidebar />
<div className="min-w-0 flex-1">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,20 @@
"use client";
import { useTranslations } from "@/lib/i18n";
import { Timeline } from "@/components/timeline/timeline";
export default function TimelinePage() {
const t = useTranslations("timeline");
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold">{t("title")}</h1>
<p className="mt-2 text-[var(--color-text-secondary)]">
{t("subtitle")}
</p>
</div>
<Timeline />
</div>
);
}

View File

@@ -0,0 +1,60 @@
import type { Metadata } from "next";
import { I18nProvider } from "@/lib/i18n";
import { Header } from "@/components/layout/header";
import en from "@/i18n/messages/en.json";
import zh from "@/i18n/messages/zh.json";
import ja from "@/i18n/messages/ja.json";
import "../globals.css";
const locales = ["en", "zh", "ja"];
const metaMessages: Record<string, typeof en> = { en, zh, ja };
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const messages = metaMessages[locale] || metaMessages.en;
return {
title: messages.meta?.title || "Learn Claude Code",
description: messages.meta?.description || "Build an AI coding agent from scratch, one concept at a time",
};
}
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return (
<html lang={locale} suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: `
(function() {
var theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
`}} />
</head>
<body className="min-h-screen bg-[var(--color-bg)] text-[var(--color-text)] antialiased">
<I18nProvider locale={locale}>
<Header />
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
{children}
</main>
</I18nProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,234 @@
"use client";
import Link from "next/link";
import { useTranslations, useLocale } from "@/lib/i18n";
import { LEARNING_PATH, VERSION_META, LAYERS } from "@/lib/constants";
import { LayerBadge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import versionsData from "@/data/generated/versions.json";
import { MessageFlow } from "@/components/architecture/message-flow";
const LAYER_DOT_COLORS: Record<string, string> = {
tools: "bg-blue-500",
planning: "bg-emerald-500",
memory: "bg-purple-500",
concurrency: "bg-amber-500",
collaboration: "bg-red-500",
};
const LAYER_BORDER_COLORS: Record<string, string> = {
tools: "border-blue-500/30 hover:border-blue-500/60",
planning: "border-emerald-500/30 hover:border-emerald-500/60",
memory: "border-purple-500/30 hover:border-purple-500/60",
concurrency: "border-amber-500/30 hover:border-amber-500/60",
collaboration: "border-red-500/30 hover:border-red-500/60",
};
const LAYER_BAR_COLORS: Record<string, string> = {
tools: "bg-blue-500",
planning: "bg-emerald-500",
memory: "bg-purple-500",
concurrency: "bg-amber-500",
collaboration: "bg-red-500",
};
function getVersionData(id: string) {
return versionsData.versions.find((v) => v.id === id);
}
export default function HomePage() {
const t = useTranslations("home");
const locale = useLocale();
return (
<div className="flex flex-col gap-20 pb-16">
{/* Hero Section */}
<section className="flex flex-col items-center px-2 pt-8 text-center sm:pt-20">
<h1 className="text-3xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
{t("hero_title")}
</h1>
<p className="mt-4 max-w-2xl text-base text-[var(--color-text-secondary)] sm:text-xl">
{t("hero_subtitle")}
</p>
<div className="mt-8">
<Link
href={`/${locale}/timeline`}
className="inline-flex min-h-[44px] items-center gap-2 rounded-lg bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{t("start")}
<span aria-hidden="true">&rarr;</span>
</Link>
</div>
</section>
{/* Core Pattern Section */}
<section>
<div className="mb-6 text-center">
<h2 className="text-2xl font-bold sm:text-3xl">{t("core_pattern")}</h2>
<p className="mt-2 text-[var(--color-text-secondary)]">
{t("core_pattern_desc")}
</p>
</div>
<div className="mx-auto max-w-2xl overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950">
<div className="flex items-center gap-2 border-b border-zinc-800 px-4 py-2.5">
<span className="h-3 w-3 rounded-full bg-red-500/70" />
<span className="h-3 w-3 rounded-full bg-yellow-500/70" />
<span className="h-3 w-3 rounded-full bg-green-500/70" />
<span className="ml-3 text-xs text-zinc-500">agent_loop.py</span>
</div>
<pre className="overflow-x-auto p-4 text-sm leading-relaxed">
<code>
<span className="text-purple-400">while</span>
<span className="text-zinc-300"> </span>
<span className="text-orange-300">True</span>
<span className="text-zinc-500">:</span>
{"\n"}
<span className="text-zinc-300">{" "}response = client.messages.</span>
<span className="text-blue-400">create</span>
<span className="text-zinc-500">(</span>
<span className="text-zinc-300">messages=</span>
<span className="text-zinc-300">messages</span>
<span className="text-zinc-500">,</span>
<span className="text-zinc-300"> tools=</span>
<span className="text-zinc-300">tools</span>
<span className="text-zinc-500">)</span>
{"\n"}
<span className="text-purple-400">{" "}if</span>
<span className="text-zinc-300"> response.stop_reason != </span>
<span className="text-green-400">&quot;tool_use&quot;</span>
<span className="text-zinc-500">:</span>
{"\n"}
<span className="text-purple-400">{" "}break</span>
{"\n"}
<span className="text-purple-400">{" "}for</span>
<span className="text-zinc-300"> tool_call </span>
<span className="text-purple-400">in</span>
<span className="text-zinc-300"> response.content</span>
<span className="text-zinc-500">:</span>
{"\n"}
<span className="text-zinc-300">{" "}result = </span>
<span className="text-blue-400">execute_tool</span>
<span className="text-zinc-500">(</span>
<span className="text-zinc-300">tool_call.name</span>
<span className="text-zinc-500">,</span>
<span className="text-zinc-300"> tool_call.input</span>
<span className="text-zinc-500">)</span>
{"\n"}
<span className="text-zinc-300">{" "}messages.</span>
<span className="text-blue-400">append</span>
<span className="text-zinc-500">(</span>
<span className="text-zinc-300">result</span>
<span className="text-zinc-500">)</span>
</code>
</pre>
</div>
</section>
{/* Message Flow Visualization */}
<section>
<div className="mb-6 text-center">
<h2 className="text-2xl font-bold sm:text-3xl">{t("message_flow")}</h2>
<p className="mt-2 text-[var(--color-text-secondary)]">
{t("message_flow_desc")}
</p>
</div>
<div className="mx-auto max-w-2xl">
<MessageFlow />
</div>
</section>
{/* Learning Path Preview */}
<section>
<div className="mb-6 text-center">
<h2 className="text-2xl font-bold sm:text-3xl">{t("learning_path")}</h2>
<p className="mt-2 text-[var(--color-text-secondary)]">
{t("learning_path_desc")}
</p>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{LEARNING_PATH.map((versionId) => {
const meta = VERSION_META[versionId];
const data = getVersionData(versionId);
if (!meta || !data) return null;
return (
<Link
key={versionId}
href={`/${locale}/${versionId}`}
className="group block"
>
<Card
className={cn(
"h-full border transition-all duration-200",
LAYER_BORDER_COLORS[meta.layer]
)}
>
<div className="flex items-start justify-between gap-2">
<LayerBadge layer={meta.layer}>{versionId}</LayerBadge>
<span className="text-xs tabular-nums text-[var(--color-text-secondary)]">
{data.loc} {t("loc")}
</span>
</div>
<h3 className="mt-3 text-sm font-semibold group-hover:underline">
{meta.title}
</h3>
<p className="mt-1 text-xs text-[var(--color-text-secondary)]">
{meta.keyInsight}
</p>
</Card>
</Link>
);
})}
</div>
</section>
{/* Layer Overview */}
<section>
<div className="mb-6 text-center">
<h2 className="text-2xl font-bold sm:text-3xl">{t("layers_title")}</h2>
<p className="mt-2 text-[var(--color-text-secondary)]">
{t("layers_desc")}
</p>
</div>
<div className="flex flex-col gap-3">
{LAYERS.map((layer) => (
<div
key={layer.id}
className="flex items-center gap-4 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4"
>
<div
className={cn(
"h-full w-1.5 self-stretch rounded-full",
LAYER_BAR_COLORS[layer.id]
)}
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold">{layer.label}</h3>
<span className="text-xs text-[var(--color-text-secondary)]">
{layer.versions.length} {t("versions_in_layer")}
</span>
</div>
<div className="mt-2 flex flex-wrap gap-1.5">
{layer.versions.map((vid) => {
const meta = VERSION_META[vid];
return (
<Link key={vid} href={`/${locale}/${vid}`}>
<LayerBadge
layer={layer.id}
className="cursor-pointer transition-opacity hover:opacity-80"
>
{vid}: {meta?.title}
</LayerBadge>
</Link>
);
})}
</div>
</div>
</div>
))}
</div>
</section>
</div>
);
}

BIN
web/src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

555
web/src/app/globals.css Normal file
View File

@@ -0,0 +1,555 @@
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
:root {
--color-layer-tools: #3B82F6;
--color-layer-planning: #10B981;
--color-layer-memory: #8B5CF6;
--color-layer-concurrency: #F59E0B;
--color-layer-collaboration: #EF4444;
--color-bg: #ffffff;
--color-bg-secondary: #f4f4f5;
--color-text: #09090b;
--color-text-secondary: #71717a;
--color-border: #e4e4e7;
}
.dark {
--color-bg: #09090b;
--color-bg-secondary: #18181b;
--color-text: #fafafa;
--color-text-secondary: #a1a1aa;
--color-border: #27272a;
}
body {
background: var(--color-bg);
color: var(--color-text);
}
@media (max-width: 640px) {
pre, code {
font-size: 11px;
}
}
* {
-webkit-tap-highlight-color: transparent;
}
/* =====================================================
PROSE-CUSTOM: Premium documentation rendering
===================================================== */
/* -- Headings -- */
.prose-custom h1 {
margin-top: 2.5rem;
margin-bottom: 1rem;
font-size: 1.5rem;
line-height: 2rem;
font-weight: 800;
letter-spacing: -0.02em;
color: #09090b;
}
.dark .prose-custom h1 {
color: #fafafa;
}
.prose-custom h2 {
margin-top: 2.5rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 700;
letter-spacing: -0.01em;
color: #09090b;
border-bottom: 1px solid #e4e4e7;
}
.dark .prose-custom h2 {
color: #fafafa;
border-bottom-color: #27272a;
}
.prose-custom h3 {
margin-top: 2rem;
margin-bottom: 0.75rem;
font-size: 1.0625rem;
line-height: 1.5rem;
font-weight: 600;
color: #18181b;
}
.dark .prose-custom h3 {
color: #e4e4e7;
}
.prose-custom h4 {
margin-top: 1.5rem;
margin-bottom: 0.5rem;
font-size: 0.9375rem;
line-height: 1.5rem;
font-weight: 600;
color: #27272a;
}
.dark .prose-custom h4 {
color: #d4d4d8;
}
/* -- Paragraphs -- */
.prose-custom p {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
font-size: 0.9rem;
line-height: 1.7;
color: #3f3f46;
}
.dark .prose-custom p {
color: #d4d4d8;
}
/* -- Hero callout (first blockquote) -- */
.prose-custom blockquote.hero-callout {
position: relative;
margin-top: 0;
margin-bottom: 1.5rem;
border-left: none;
border-radius: 0.75rem;
background: linear-gradient(135deg, #eff6ff 0%, #f0fdf4 100%);
padding: 1.25rem 1.5rem 1.25rem 1.75rem;
font-style: normal;
overflow: hidden;
}
.prose-custom blockquote.hero-callout::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(to bottom, #3b82f6, #10b981);
border-radius: 4px 0 0 4px;
}
.prose-custom blockquote.hero-callout p {
font-size: 0.95rem;
line-height: 1.65;
font-weight: 500;
color: #1e40af;
margin: 0;
}
.dark .prose-custom blockquote.hero-callout {
background: linear-gradient(135deg, #172554 0%, #052e16 100%);
}
.dark .prose-custom blockquote.hero-callout p {
color: #93c5fd;
}
/* -- Regular blockquotes -- */
.prose-custom blockquote {
margin-top: 1rem;
margin-bottom: 1rem;
border-left: 3px solid #a5b4fc;
border-radius: 0 0.5rem 0.5rem 0;
background-color: #eef2ff;
padding: 0.75rem 1rem;
font-style: normal;
}
.prose-custom blockquote p {
color: #4338ca;
font-size: 0.875rem;
margin: 0;
}
.dark .prose-custom blockquote {
border-left-color: #6366f1;
background-color: rgba(99, 102, 241, 0.1);
}
.dark .prose-custom blockquote p {
color: #c7d2fe;
}
/* -- Code blocks with language label -- */
.prose-custom pre {
position: relative;
overflow-x: auto;
margin-top: 1rem;
margin-bottom: 1rem;
border-radius: 0.75rem;
border: 1px solid #1e293b;
background-color: #0f172a;
padding: 1.25rem;
font-size: 0.8125rem;
line-height: 1.6;
color: #e2e8f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.prose-custom pre.code-block {
padding-top: 2.25rem;
}
.prose-custom pre.code-block::before {
content: attr(data-language);
position: absolute;
top: 0;
right: 0.75rem;
padding: 0.125rem 0.625rem 0.25rem;
background: #3b82f6;
color: #ffffff;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
border-radius: 0 0 0.375rem 0.375rem;
font-family: system-ui, -apple-system, sans-serif;
}
.prose-custom pre.code-block[data-language="sh"]::before {
background: #22c55e;
content: "terminal";
}
/* -- ASCII diagram containers -- */
.prose-custom pre.ascii-diagram {
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
border: 1px solid #cbd5e1;
color: #334155;
text-align: center;
font-size: 0.75rem;
line-height: 1.35;
padding: 1.5rem 1rem;
}
.dark .prose-custom pre.ascii-diagram {
background: linear-gradient(135deg, #1e1b4b, #172554);
border-color: #312e81;
color: #c7d2fe;
}
/* -- Inline code -- */
.prose-custom :not(pre) > code {
border-radius: 0.375rem;
background-color: #f1f5f9;
border: 1px solid #e2e8f0;
padding: 0.125rem 0.425rem;
font-size: 0.8125rem;
font-weight: 500;
color: #be185d;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.dark .prose-custom :not(pre) > code {
background-color: #27272a;
border-color: #3f3f46;
color: #f9a8d4;
}
/* -- Links -- */
.prose-custom a {
color: #2563eb;
font-weight: 500;
text-decoration: underline;
text-decoration-color: #93c5fd;
text-underline-offset: 2px;
transition: text-decoration-color 0.15s;
}
.prose-custom a:hover {
text-decoration-color: #2563eb;
}
.dark .prose-custom a {
color: #60a5fa;
text-decoration-color: #1e40af;
}
.dark .prose-custom a:hover {
text-decoration-color: #60a5fa;
}
/* -- Lists -- */
.prose-custom ul {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
padding-left: 1.5rem;
font-size: 0.9rem;
line-height: 1.7;
color: #3f3f46;
}
.dark .prose-custom ul {
color: #d4d4d8;
}
.prose-custom ul > li {
margin-top: 0.375rem;
margin-bottom: 0.375rem;
position: relative;
}
.prose-custom ul > li::marker {
color: #3b82f6;
}
.prose-custom ol {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
padding-left: 0;
list-style: none;
counter-reset: step-counter;
font-size: 0.9rem;
line-height: 1.7;
color: #3f3f46;
}
.dark .prose-custom ol {
color: #d4d4d8;
}
.prose-custom ol > li {
counter-increment: step-counter;
margin-top: 0.75rem;
margin-bottom: 0.75rem;
padding-left: 2.75rem;
position: relative;
}
.prose-custom ol > li::before {
content: counter(step-counter);
position: absolute;
left: 0;
top: 0;
width: 1.75rem;
height: 1.75rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.5rem;
background: linear-gradient(135deg, #3b82f6, #6366f1);
color: #ffffff;
font-size: 0.75rem;
font-weight: 700;
font-family: ui-monospace, SFMono-Regular, monospace;
flex-shrink: 0;
}
/* Reset nested lists inside ol to normal style */
.prose-custom ol > li > ul {
padding-left: 1.25rem;
}
.prose-custom ol > li > ul > li {
padding-left: 0;
}
.prose-custom ol > li > ul > li::before {
display: none;
}
/* -- Tables -- */
.prose-custom table {
width: 100%;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
border-collapse: separate;
border-spacing: 0;
font-size: 0.8125rem;
line-height: 1.5;
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid #e2e8f0;
}
.dark .prose-custom table {
border-color: #27272a;
}
.prose-custom thead {
border-bottom: none;
}
.prose-custom th {
padding: 0.625rem 1rem;
text-align: left;
font-weight: 600;
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
background-color: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.dark .prose-custom th {
color: #94a3b8;
background-color: #18181b;
border-bottom-color: #27272a;
}
.prose-custom td {
padding: 0.625rem 1rem;
border-bottom: 1px solid #f1f5f9;
color: #475569;
}
.prose-custom td code {
font-size: 0.75rem;
}
.dark .prose-custom td {
border-bottom-color: #1e1e22;
color: #cbd5e1;
}
.prose-custom tbody tr:last-child td {
border-bottom: none;
}
.prose-custom tbody tr:hover {
background-color: #f8fafc;
}
.dark .prose-custom tbody tr:hover {
background-color: #111113;
}
/* -- Horizontal rules -- */
.prose-custom hr {
margin-top: 2rem;
margin-bottom: 2rem;
border: none;
height: 1px;
background: linear-gradient(to right, transparent, #d4d4d8, transparent);
}
.dark .prose-custom hr {
background: linear-gradient(to right, transparent, #3f3f46, transparent);
}
/* -- Strong / Em -- */
.prose-custom strong {
font-weight: 700;
color: #09090b;
}
.dark .prose-custom strong {
color: #fafafa;
}
.prose-custom em {
font-style: italic;
color: #52525b;
}
.dark .prose-custom em {
color: #a1a1aa;
}
/* =====================================================
HIGHLIGHT.JS TOKEN THEME (code syntax highlighting)
===================================================== */
.hljs {
background: transparent !important;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #c084fc;
font-weight: 500;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet {
color: #fb923c;
}
.hljs-string,
.hljs-doctag,
.hljs-template-variable,
.hljs-variable {
color: #34d399;
}
.hljs-number {
color: #fb923c;
}
.hljs-comment,
.hljs-quote {
color: #64748b;
font-style: italic;
}
.hljs-title,
.hljs-section {
color: #60a5fa;
font-weight: 600;
}
.hljs-title.function_,
.hljs-title.class_ {
color: #60a5fa;
}
.hljs-built_in {
color: #f472b6;
}
.hljs-attr,
.hljs-attribute {
color: #fbbf24;
}
.hljs-params {
color: #e2e8f0;
}
.hljs-meta {
color: #94a3b8;
}
.hljs-name,
.hljs-tag {
color: #f87171;
}
.hljs-selector-class,
.hljs-selector-id {
color: #a78bfa;
}
.hljs-deletion {
color: #fca5a5;
background-color: rgba(239, 68, 68, 0.15);
}
.hljs-addition {
color: #86efac;
background-color: rgba(34, 197, 94, 0.15);
}

View File

@@ -0,0 +1,228 @@
"use client";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
import { LAYERS } from "@/lib/constants";
import versionsData from "@/data/generated/versions.json";
const CLASS_DESCRIPTIONS: Record<string, string> = {
TodoManager: "Visible task planning with constraints",
SkillLoader: "Dynamic knowledge injection from SKILL.md files",
ContextManager: "Three-layer context compression pipeline",
Task: "File-based persistent task with dependencies",
TaskManager: "File-based persistent task CRUD with dependencies",
BackgroundTask: "Single background execution unit",
BackgroundManager: "Non-blocking thread execution + notification queue",
TeammateManager: "Multi-agent team lifecycle and coordination",
Teammate: "Individual agent identity and state tracking",
SharedBoard: "Cross-agent shared state coordination",
};
interface ArchDiagramProps {
version: string;
}
function getLayerColor(versionId: string): string {
const layer = LAYERS.find((l) => (l.versions as readonly string[]).includes(versionId));
return layer?.color ?? "#71717a";
}
function getLayerColorClasses(versionId: string): {
border: string;
bg: string;
} {
const v =
versionsData.versions.find((v) => v.id === versionId) as { layer?: string } | undefined;
const layer = v?.layer;
switch (layer) {
case "tools":
return {
border: "border-blue-500",
bg: "bg-blue-500/10",
};
case "planning":
return {
border: "border-emerald-500",
bg: "bg-emerald-500/10",
};
case "memory":
return {
border: "border-purple-500",
bg: "bg-purple-500/10",
};
case "concurrency":
return {
border: "border-amber-500",
bg: "bg-amber-500/10",
};
case "collaboration":
return {
border: "border-red-500",
bg: "bg-red-500/10",
};
default:
return {
border: "border-zinc-500",
bg: "bg-zinc-500/10",
};
}
}
function collectClassesUpTo(
targetId: string
): { name: string; introducedIn: string }[] {
const { versions, diffs } = versionsData;
const order = versions.map((v) => v.id);
const targetIdx = order.indexOf(targetId);
if (targetIdx < 0) return [];
const result: { name: string; introducedIn: string }[] = [];
const seen = new Set<string>();
for (let i = 0; i <= targetIdx; i++) {
const v = versions[i];
if (!v.classes) continue;
for (const cls of v.classes) {
if (!seen.has(cls.name)) {
seen.add(cls.name);
result.push({ name: cls.name, introducedIn: v.id });
}
}
}
return result;
}
function getNewClassNames(version: string): Set<string> {
const diff = versionsData.diffs.find((d) => d.to === version);
if (!diff) {
const v = versionsData.versions.find((ver) => ver.id === version);
return new Set(v?.classes?.map((c) => c.name) ?? []);
}
return new Set(diff.newClasses ?? []);
}
export function ArchDiagram({ version }: ArchDiagramProps) {
const allClasses = collectClassesUpTo(version);
const newClassNames = getNewClassNames(version);
const versionData = versionsData.versions.find((v) => v.id === version);
const tools = versionData?.tools ?? [];
const reversed = [...allClasses].reverse();
return (
<div className="space-y-3">
{reversed.map((cls, i) => {
const isNew = newClassNames.has(cls.name);
const colorClasses = getLayerColorClasses(cls.introducedIn);
return (
<div key={cls.name}>
{i > 0 && (
<div className="flex justify-center py-1">
<motion.svg
width="24"
height="20"
viewBox="0 0 24 20"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: i * 0.08 + 0.05 }}
>
<motion.line
x1={12}
y1={0}
x2={12}
y2={14}
stroke="var(--color-text-secondary)"
strokeWidth={1.5}
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.3, delay: i * 0.08 }}
/>
<motion.polygon
points="7,12 12,19 17,12"
fill="var(--color-text-secondary)"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: i * 0.08 + 0.2 }}
/>
</motion.svg>
</div>
)}
<motion.div
key={cls.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.08, duration: 0.3 }}
className={cn(
"rounded-lg border-2 px-4 py-3 transition-colors",
isNew
? cn(colorClasses.border, colorClasses.bg)
: "border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800/50"
)}
>
<div className="flex items-center justify-between">
<div>
<span
className={cn(
"font-mono text-sm font-semibold",
isNew
? "text-zinc-900 dark:text-white"
: "text-zinc-400 dark:text-zinc-500"
)}
>
{cls.name}
</span>
<p
className={cn(
"mt-0.5 text-xs",
isNew
? "text-zinc-600 dark:text-zinc-300"
: "text-zinc-400 dark:text-zinc-500"
)}
>
{CLASS_DESCRIPTIONS[cls.name] || ""}
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-400 dark:text-zinc-500">
{cls.introducedIn}
</span>
{isNew && (
<span className="rounded-full bg-zinc-900 px-2 py-0.5 text-[10px] font-bold uppercase text-white dark:bg-white dark:text-zinc-900">
NEW
</span>
)}
</div>
</div>
</motion.div>
</div>
);
})}
{allClasses.length === 0 && (
<div className="rounded-lg border border-dashed border-zinc-300 px-4 py-6 text-center text-sm text-zinc-400 dark:border-zinc-600">
No classes in this version (functions only)
</div>
)}
{tools.length > 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: reversed.length * 0.08 + 0.1 }}
className="flex flex-wrap gap-1.5 pt-2"
>
{tools.map((tool) => (
<span
key={tool}
className="rounded-md bg-zinc-100 px-2 py-1 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400"
>
{tool}
</span>
))}
</motion.div>
)}
</div>
);
}

View File

@@ -0,0 +1,145 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useTranslations, useLocale } from "@/lib/i18n";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
import s01Annotations from "@/data/annotations/s01.json";
import s02Annotations from "@/data/annotations/s02.json";
import s03Annotations from "@/data/annotations/s03.json";
import s04Annotations from "@/data/annotations/s04.json";
import s05Annotations from "@/data/annotations/s05.json";
import s06Annotations from "@/data/annotations/s06.json";
import s07Annotations from "@/data/annotations/s07.json";
import s08Annotations from "@/data/annotations/s08.json";
import s09Annotations from "@/data/annotations/s09.json";
import s10Annotations from "@/data/annotations/s10.json";
import s11Annotations from "@/data/annotations/s11.json";
interface Decision {
id: string;
title: string;
description: string;
alternatives: string;
zh?: { title: string; description: string };
ja?: { title: string; description: string };
}
interface AnnotationFile {
version: string;
decisions: Decision[];
}
const ANNOTATIONS: Record<string, AnnotationFile> = {
s01: s01Annotations as AnnotationFile,
s02: s02Annotations as AnnotationFile,
s03: s03Annotations as AnnotationFile,
s04: s04Annotations as AnnotationFile,
s05: s05Annotations as AnnotationFile,
s06: s06Annotations as AnnotationFile,
s07: s07Annotations as AnnotationFile,
s08: s08Annotations as AnnotationFile,
s09: s09Annotations as AnnotationFile,
s10: s10Annotations as AnnotationFile,
s11: s11Annotations as AnnotationFile,
};
interface DesignDecisionsProps {
version: string;
}
function DecisionCard({
decision,
locale,
}: {
decision: Decision;
locale: string;
}) {
const [open, setOpen] = useState(false);
const t = useTranslations("version");
const localized =
locale !== "en" ? (decision as unknown as Record<string, unknown>)[locale] as { title?: string; description?: string } | undefined : undefined;
const title = localized?.title || decision.title;
const description = localized?.description || decision.description;
return (
<div className="rounded-lg border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
<button
onClick={() => setOpen(!open)}
className="flex w-full items-center justify-between px-4 py-3 text-left"
>
<span className="pr-4 text-sm font-semibold text-zinc-900 dark:text-white">
{title}
</span>
<ChevronDown
size={16}
className={cn(
"shrink-0 text-zinc-400 transition-transform duration-200",
open && "rotate-180"
)}
/>
</button>
<AnimatePresence>
{open && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="border-t border-zinc-100 px-4 py-3 dark:border-zinc-800">
<p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-300">
{description}
</p>
{decision.alternatives && (
<div className="mt-3">
<h4 className="text-xs font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
{t("alternatives")}
</h4>
<p className="mt-1 text-sm leading-relaxed text-zinc-500 dark:text-zinc-400">
{decision.alternatives}
</p>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export function DesignDecisions({ version }: DesignDecisionsProps) {
const t = useTranslations("version");
const locale = useLocale();
const annotations = ANNOTATIONS[version];
if (!annotations || annotations.decisions.length === 0) {
return null;
}
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">{t("design_decisions")}</h2>
<div className="space-y-2">
{annotations.decisions.map((decision, i) => (
<motion.div
key={decision.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.05 }}
>
<DecisionCard decision={decision} locale={locale} />
</motion.div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,243 @@
"use client";
import { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { useTranslations } from "@/lib/i18n";
import { getFlowForVersion } from "@/data/execution-flows";
import type { FlowNode, FlowEdge } from "@/types/agent-data";
const NODE_WIDTH = 140;
const NODE_HEIGHT = 40;
const DIAMOND_SIZE = 50;
const LAYER_COLORS: Record<string, string> = {
start: "#3B82F6",
process: "#10B981",
decision: "#F59E0B",
subprocess: "#8B5CF6",
end: "#EF4444",
};
function getNodeCenter(node: FlowNode): { cx: number; cy: number } {
return { cx: node.x, cy: node.y };
}
function getEdgePath(from: FlowNode, to: FlowNode): string {
const { cx: x1, cy: y1 } = getNodeCenter(from);
const { cx: x2, cy: y2 } = getNodeCenter(to);
const halfH = from.type === "decision" ? DIAMOND_SIZE / 2 : NODE_HEIGHT / 2;
const halfHTo = to.type === "decision" ? DIAMOND_SIZE / 2 : NODE_HEIGHT / 2;
if (Math.abs(x1 - x2) < 10) {
const startY = y1 + halfH;
const endY = y2 - halfHTo;
return `M ${x1} ${startY} L ${x2} ${endY}`;
}
const startY = y1 + halfH;
const endY = y2 - halfHTo;
const midY = (startY + endY) / 2;
return `M ${x1} ${startY} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${endY}`;
}
function NodeShape({ node }: { node: FlowNode }) {
const color = LAYER_COLORS[node.type];
const lines = node.label.split("\n");
if (node.type === "decision") {
const half = DIAMOND_SIZE / 2;
return (
<g>
<polygon
points={`${node.x},${node.y - half} ${node.x + half},${node.y} ${node.x},${node.y + half} ${node.x - half},${node.y}`}
fill="none"
stroke={color}
strokeWidth={2}
/>
{lines.map((line, i) => (
<text
key={i}
x={node.x}
y={node.y + (i - (lines.length - 1) / 2) * 12}
textAnchor="middle"
dominantBaseline="central"
fontSize={10}
fontFamily="monospace"
fill="currentColor"
>
{line}
</text>
))}
</g>
);
}
if (node.type === "start" || node.type === "end") {
return (
<g>
<rect
x={node.x - NODE_WIDTH / 2}
y={node.y - NODE_HEIGHT / 2}
width={NODE_WIDTH}
height={NODE_HEIGHT}
rx={NODE_HEIGHT / 2}
fill="none"
stroke={color}
strokeWidth={2}
/>
<text
x={node.x}
y={node.y}
textAnchor="middle"
dominantBaseline="central"
fontSize={12}
fontWeight={600}
fontFamily="monospace"
fill="currentColor"
>
{node.label}
</text>
</g>
);
}
const isSubprocess = node.type === "subprocess";
return (
<g>
<rect
x={node.x - NODE_WIDTH / 2}
y={node.y - NODE_HEIGHT / 2}
width={NODE_WIDTH}
height={NODE_HEIGHT}
rx={4}
fill="none"
stroke={color}
strokeWidth={2}
strokeDasharray={isSubprocess ? "6 3" : undefined}
/>
{lines.map((line, i) => (
<text
key={i}
x={node.x}
y={node.y + (i - (lines.length - 1) / 2) * 13}
textAnchor="middle"
dominantBaseline="central"
fontSize={11}
fontFamily="monospace"
fill="currentColor"
>
{line}
</text>
))}
</g>
);
}
function EdgePath({
edge,
nodes,
index,
}: {
edge: FlowEdge;
nodes: FlowNode[];
index: number;
}) {
const from = nodes.find((n) => n.id === edge.from);
const to = nodes.find((n) => n.id === edge.to);
if (!from || !to) return null;
const d = getEdgePath(from, to);
const midX = (from.x + to.x) / 2;
const midY = (from.y + to.y) / 2;
return (
<g>
<motion.path
d={d}
fill="none"
stroke="var(--color-text-secondary)"
strokeWidth={1.5}
markerEnd="url(#arrowhead)"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 0.5, delay: index * 0.12 }}
/>
{edge.label && (
<motion.text
x={midX + 8}
y={midY - 4}
fontSize={10}
fill="var(--color-text-secondary)"
fontFamily="monospace"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: index * 0.12 + 0.3 }}
>
{edge.label}
</motion.text>
)}
</g>
);
}
interface ExecutionFlowProps {
version: string;
}
export function ExecutionFlow({ version }: ExecutionFlowProps) {
const t = useTranslations("version");
const [flow, setFlow] = useState<ReturnType<typeof getFlowForVersion>>(null);
useEffect(() => {
setFlow(getFlowForVersion(version));
}, [version]);
if (!flow) return null;
const maxY = Math.max(...flow.nodes.map((n) => n.y)) + 50;
return (
<section>
<h2 className="mb-4 text-xl font-semibold">{t("execution_flow")}</h2>
<div className="overflow-x-auto rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4">
<svg
viewBox={`0 0 600 ${maxY}`}
className="mx-auto w-full max-w-[600px]"
style={{ minHeight: 300 }}
>
<defs>
<marker
id="arrowhead"
markerWidth={8}
markerHeight={6}
refX={8}
refY={3}
orient="auto"
>
<polygon
points="0 0, 8 3, 0 6"
fill="var(--color-text-secondary)"
/>
</marker>
</defs>
{flow.edges.map((edge, i) => (
<EdgePath key={`${edge.from}-${edge.to}`} edge={edge} nodes={flow.nodes} index={i} />
))}
{flow.nodes.map((node, i) => (
<motion.g
key={node.id}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.06, duration: 0.3 }}
>
<NodeShape node={node} />
</motion.g>
))}
</svg>
</div>
</section>
);
}

View File

@@ -0,0 +1,70 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
const FLOW_STEPS = [
{ role: "user", label: "user", color: "bg-blue-500" },
{ role: "assistant", label: "assistant", color: "bg-zinc-600" },
{ role: "tool_call", label: "tool_call", color: "bg-amber-500" },
{ role: "tool_result", label: "tool_result", color: "bg-emerald-500" },
{ role: "assistant", label: "assistant", color: "bg-zinc-600" },
{ role: "tool_call", label: "tool_call", color: "bg-amber-500" },
{ role: "tool_result", label: "tool_result", color: "bg-emerald-500" },
{ role: "assistant", label: "assistant (final)", color: "bg-zinc-600" },
];
export function MessageFlow() {
const [count, setCount] = useState(0);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount((prev) => {
if (prev >= FLOW_STEPS.length) {
setTimeout(() => setCount(0), 1500);
return prev;
}
return prev + 1;
});
}, 800);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, []);
return (
<div className="overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4">
<div className="mb-3 flex items-center gap-2">
<span className="font-mono text-xs text-[var(--color-text-secondary)]">
messages[]
</span>
<span className="ml-auto rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-xs tabular-nums dark:bg-zinc-800">
len={count}
</span>
</div>
<div className="flex gap-1.5 overflow-x-auto pb-1">
<AnimatePresence>
{FLOW_STEPS.slice(0, count).map((step, i) => (
<motion.div
key={i}
initial={{ opacity: 0, scale: 0.7, width: 0 }}
animate={{ opacity: 1, scale: 1, width: "auto" }}
transition={{ duration: 0.25 }}
className={`flex shrink-0 items-center rounded-md px-2.5 py-1.5 ${step.color}`}
>
<span className="whitespace-nowrap font-mono text-[10px] font-medium text-white">
{step.label}
</span>
</motion.div>
))}
</AnimatePresence>
{count === 0 && (
<div className="flex h-7 items-center text-xs text-[var(--color-text-secondary)]">
[]
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,102 @@
"use client";
import { useMemo } from "react";
interface SourceViewerProps {
source: string;
filename: string;
}
function highlightLine(line: string): React.ReactNode[] {
const trimmed = line.trimStart();
if (trimmed.startsWith("#")) {
return [
<span key={0} className="text-zinc-400 italic">
{line}
</span>,
];
}
if (trimmed.startsWith("@")) {
return [
<span key={0} className="text-amber-400">
{line}
</span>,
];
}
if (trimmed.startsWith('"""') || trimmed.startsWith("'''")) {
return [
<span key={0} className="text-emerald-500">
{line}
</span>,
];
}
const keywordSet = new Set([
"def", "class", "import", "from", "return", "if", "elif", "else",
"while", "for", "in", "not", "and", "or", "is", "None", "True",
"False", "try", "except", "raise", "with", "as", "yield", "break",
"continue", "pass", "global", "lambda", "async", "await",
]);
const parts = line.split(
/(\b(?:def|class|import|from|return|if|elif|else|while|for|in|not|and|or|is|None|True|False|try|except|raise|with|as|yield|break|continue|pass|global|lambda|async|await|self)\b|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|f"(?:[^"\\]|\\.)*"|f'(?:[^'\\]|\\.)*'|#.*$|\b\d+(?:\.\d+)?\b)/
);
return parts.map((part, idx) => {
if (!part) return null;
if (keywordSet.has(part)) {
return <span key={idx} className="text-blue-400 font-medium">{part}</span>;
}
if (part === "self") {
return <span key={idx} className="text-purple-400">{part}</span>;
}
if (part.startsWith("#")) {
return <span key={idx} className="text-zinc-400 italic">{part}</span>;
}
if (
(part.startsWith('"') && part.endsWith('"')) ||
(part.startsWith("'") && part.endsWith("'")) ||
(part.startsWith('f"') && part.endsWith('"')) ||
(part.startsWith("f'") && part.endsWith("'"))
) {
return <span key={idx} className="text-emerald-500">{part}</span>;
}
if (/^\d+(?:\.\d+)?$/.test(part)) {
return <span key={idx} className="text-orange-400">{part}</span>;
}
return <span key={idx}>{part}</span>;
});
}
export function SourceViewer({ source, filename }: SourceViewerProps) {
const lines = useMemo(() => source.split("\n"), [source]);
return (
<div className="rounded-lg border border-zinc-200 dark:border-zinc-700">
<div className="flex items-center gap-2 border-b border-zinc-200 px-4 py-2 dark:border-zinc-700">
<div className="flex gap-1.5">
<span className="h-3 w-3 rounded-full bg-red-400" />
<span className="h-3 w-3 rounded-full bg-yellow-400" />
<span className="h-3 w-3 rounded-full bg-green-400" />
</div>
<span className="font-mono text-xs text-zinc-400">{filename}</span>
</div>
<div className="overflow-x-auto bg-zinc-950">
<pre className="p-2 text-[10px] leading-4 sm:p-4 sm:text-xs sm:leading-5">
<code>
{lines.map((line, i) => (
<div key={i} className="flex">
<span className="mr-2 inline-block w-6 shrink-0 select-none text-right text-zinc-600 sm:mr-4 sm:w-8">
{i + 1}
</span>
<span className="text-zinc-200">
{highlightLine(line)}
</span>
</div>
))}
</code>
</pre>
</div>
</div>
);
}

View File

@@ -0,0 +1,205 @@
"use client";
import { useState, useMemo } from "react";
import { diffLines, Change } from "diff";
import { cn } from "@/lib/utils";
interface CodeDiffProps {
oldSource: string;
newSource: string;
oldLabel: string;
newLabel: string;
}
export function CodeDiff({ oldSource, newSource, oldLabel, newLabel }: CodeDiffProps) {
const [viewMode, setViewMode] = useState<"unified" | "split">("unified");
const changes = useMemo(() => diffLines(oldSource, newSource), [oldSource, newSource]);
return (
<div>
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 truncate text-sm text-zinc-500 dark:text-zinc-400">
<span className="font-medium text-zinc-700 dark:text-zinc-300">{oldLabel}</span>
{" -> "}
<span className="font-medium text-zinc-700 dark:text-zinc-300">{newLabel}</span>
</div>
<div className="flex shrink-0 rounded-lg border border-zinc-200 dark:border-zinc-700">
<button
onClick={() => setViewMode("unified")}
className={cn(
"min-h-[36px] px-3 text-xs font-medium transition-colors",
viewMode === "unified"
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900"
: "text-zinc-500 hover:text-zinc-700 dark:text-zinc-400"
)}
>
Unified
</button>
<button
onClick={() => setViewMode("split")}
className={cn(
"min-h-[36px] px-3 text-xs font-medium transition-colors sm:inline-flex hidden",
viewMode === "split"
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900"
: "text-zinc-500 hover:text-zinc-700 dark:text-zinc-400"
)}
>
Split
</button>
</div>
</div>
{viewMode === "unified" ? (
<UnifiedView changes={changes} />
) : (
<SplitView changes={changes} />
)}
</div>
);
}
function UnifiedView({ changes }: { changes: Change[] }) {
let oldLine = 1;
let newLine = 1;
const rows: { oldNum: number | null; newNum: number | null; type: "add" | "remove" | "context"; text: string }[] = [];
for (const change of changes) {
const lines = change.value.replace(/\n$/, "").split("\n");
for (const line of lines) {
if (change.added) {
rows.push({ oldNum: null, newNum: newLine++, type: "add", text: line });
} else if (change.removed) {
rows.push({ oldNum: oldLine++, newNum: null, type: "remove", text: line });
} else {
rows.push({ oldNum: oldLine++, newNum: newLine++, type: "context", text: line });
}
}
}
return (
<div className="overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-700">
<table className="w-full border-collapse font-mono text-xs leading-5">
<tbody>
{rows.map((row, i) => (
<tr
key={i}
className={cn(
row.type === "add" && "bg-green-50 dark:bg-green-950/30",
row.type === "remove" && "bg-red-50 dark:bg-red-950/30"
)}
>
<td className="w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600">
{row.oldNum ?? ""}
</td>
<td className="w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600">
{row.newNum ?? ""}
</td>
<td className="w-4 select-none px-1 text-center">
{row.type === "add" && <span className="text-green-600 dark:text-green-400">+</span>}
{row.type === "remove" && <span className="text-red-600 dark:text-red-400">-</span>}
</td>
<td className="whitespace-pre px-2">
<span
className={cn(
row.type === "add" && "text-green-800 dark:text-green-300",
row.type === "remove" && "text-red-800 dark:text-red-300",
row.type === "context" && "text-zinc-700 dark:text-zinc-300"
)}
>
{row.text}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function SplitView({ changes }: { changes: Change[] }) {
let oldLine = 1;
let newLine = 1;
type SplitRow = {
left: { num: number | null; text: string; type: "remove" | "context" | "empty" };
right: { num: number | null; text: string; type: "add" | "context" | "empty" };
};
const rows: SplitRow[] = [];
for (const change of changes) {
const lines = change.value.replace(/\n$/, "").split("\n");
if (change.removed) {
for (const line of lines) {
rows.push({
left: { num: oldLine++, text: line, type: "remove" },
right: { num: null, text: "", type: "empty" },
});
}
} else if (change.added) {
let filled = 0;
for (const line of lines) {
// Try to fill in empty right-side slots from preceding removes
const lastUnfilled = rows.length - lines.length + filled;
if (
lastUnfilled >= 0 &&
lastUnfilled < rows.length &&
rows[lastUnfilled].right.type === "empty" &&
rows[lastUnfilled].left.type === "remove"
) {
rows[lastUnfilled].right = { num: newLine++, text: line, type: "add" };
} else {
rows.push({
left: { num: null, text: "", type: "empty" },
right: { num: newLine++, text: line, type: "add" },
});
}
filled++;
}
} else {
for (const line of lines) {
rows.push({
left: { num: oldLine++, text: line, type: "context" },
right: { num: newLine++, text: line, type: "context" },
});
}
}
}
const cellClass = (type: string) =>
cn(
"whitespace-pre px-2",
type === "add" && "bg-green-50 text-green-800 dark:bg-green-950/30 dark:text-green-300",
type === "remove" && "bg-red-50 text-red-800 dark:bg-red-950/30 dark:text-red-300",
type === "context" && "text-zinc-700 dark:text-zinc-300",
type === "empty" && "bg-zinc-50 dark:bg-zinc-900"
);
return (
<div className="overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-700">
<table className="w-full border-collapse font-mono text-xs leading-5">
<tbody>
{rows.map((row, i) => (
<tr key={i}>
<td className="w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600">
{row.left.num ?? ""}
</td>
<td className={cn("w-1/2 border-r border-zinc-200 dark:border-zinc-700", cellClass(row.left.type))}>
{row.left.text}
</td>
<td className="w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600">
{row.right.num ?? ""}
</td>
<td className={cn("w-1/2", cellClass(row.right.type))}>
{row.right.text}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -0,0 +1,134 @@
"use client";
import { motion } from "framer-motion";
import { useTranslations } from "@/lib/i18n";
import { Card } from "@/components/ui/card";
interface WhatsNewProps {
diff: {
from: string;
to: string;
newClasses: string[];
newFunctions: string[];
newTools: string[];
locDelta: number;
} | null;
}
export function WhatsNew({ diff }: WhatsNewProps) {
const t = useTranslations("version");
const td = useTranslations("diff");
if (!diff) {
return null;
}
const hasContent =
diff.newClasses.length > 0 ||
diff.newTools.length > 0 ||
diff.newFunctions.length > 0 ||
diff.locDelta !== 0;
if (!hasContent) {
return null;
}
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">{t("whats_new")}</h2>
<div className="grid gap-4 sm:grid-cols-2">
{diff.newClasses.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Card className="h-full">
<h3 className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{td("new_classes")}
</h3>
<div className="space-y-1.5">
{diff.newClasses.map((cls) => (
<div
key={cls}
className="rounded-md bg-emerald-50 px-3 py-1.5 font-mono text-sm font-medium text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-300"
>
{cls}
</div>
))}
</div>
</Card>
</motion.div>
)}
{diff.newTools.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
>
<Card className="h-full">
<h3 className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{td("new_tools")}
</h3>
<div className="flex flex-wrap gap-1.5">
{diff.newTools.map((tool) => (
<span
key={tool}
className="rounded-full bg-blue-50 px-3 py-1 font-mono text-xs font-medium text-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
>
{tool}
</span>
))}
</div>
</Card>
</motion.div>
)}
{diff.newFunctions.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="h-full">
<h3 className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{td("new_functions")}
</h3>
<ul className="space-y-1 text-sm text-zinc-700 dark:text-zinc-300">
{diff.newFunctions.map((fn) => (
<li key={fn} className="font-mono">
<span className="text-zinc-400 dark:text-zinc-500">
def{" "}
</span>
{fn}()
</li>
))}
</ul>
</Card>
</motion.div>
)}
{diff.locDelta !== 0 && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.25 }}
>
<Card className="flex h-full items-center">
<div>
<h3 className="mb-1 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{td("loc_delta")}
</h3>
<p className="text-2xl font-bold text-emerald-600 dark:text-emerald-400">
+{diff.locDelta} lines
</p>
</div>
</Card>
</motion.div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,91 @@
"use client";
import { useMemo } from "react";
import { useLocale } from "@/lib/i18n";
import docsData from "@/data/generated/docs.json";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import remarkRehype from "remark-rehype";
import rehypeRaw from "rehype-raw";
import rehypeHighlight from "rehype-highlight";
import rehypeStringify from "rehype-stringify";
interface DocRendererProps {
version: string;
}
function renderMarkdown(md: string): string {
const result = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeHighlight, { detect: false, ignoreMissing: true })
.use(rehypeStringify)
.processSync(md);
return String(result);
}
function postProcessHtml(html: string): string {
// Add language labels to highlighted code blocks
html = html.replace(
/<pre><code class="hljs language-(\w+)">/g,
'<pre class="code-block" data-language="$1"><code class="hljs language-$1">'
);
// Wrap plain pre>code (ASCII art / diagrams) in diagram container
html = html.replace(
/<pre><code(?! class="hljs)([^>]*)>/g,
'<pre class="ascii-diagram"><code$1>'
);
// Mark the first blockquote as hero callout
html = html.replace(
/<blockquote>/,
'<blockquote class="hero-callout">'
);
// Remove the h1 (it's redundant with the page header)
html = html.replace(/<h1>.*?<\/h1>\n?/, "");
// Fix ordered list counter for interrupted lists (ol start="N")
html = html.replace(
/<ol start="(\d+)">/g,
(_, start) => `<ol style="counter-reset:step-counter ${parseInt(start) - 1}">`
);
return html;
}
export function DocRenderer({ version }: DocRendererProps) {
const locale = useLocale();
const doc = useMemo(() => {
const match = docsData.find(
(d: { version: string; locale: string }) =>
d.version === version && d.locale === locale
);
if (match) return match;
return docsData.find(
(d: { version: string; locale: string }) =>
d.version === version && d.locale === "en"
);
}, [version, locale]);
if (!doc) return null;
const html = useMemo(() => {
const raw = renderMarkdown(doc.content);
return postProcessHtml(raw);
}, [doc.content]);
return (
<div className="py-4">
<div
className="prose-custom"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
}

View File

@@ -0,0 +1,167 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslations, useLocale } from "@/lib/i18n";
import { Github, Menu, X, Sun, Moon } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
const NAV_ITEMS = [
{ key: "timeline", href: "/timeline" },
{ key: "compare", href: "/compare" },
{ key: "layers", href: "/layers" },
] as const;
const LOCALES = [
{ code: "en", label: "EN" },
{ code: "zh", label: "中文" },
{ code: "ja", label: "日本語" },
];
export function Header() {
const t = useTranslations("nav");
const pathname = usePathname();
const locale = useLocale();
const [mobileOpen, setMobileOpen] = useState(false);
const [dark, setDark] = useState(() => {
if (typeof window !== "undefined") {
const stored = localStorage.getItem("theme");
if (stored) return stored === "dark";
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
return false;
});
function toggleDark() {
const next = !dark;
setDark(next);
document.documentElement.classList.toggle("dark", next);
localStorage.setItem("theme", next ? "dark" : "light");
}
function switchLocale(newLocale: string) {
const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
window.location.href = newPath;
}
return (
<header className="sticky top-0 z-50 border-b border-[var(--color-border)] bg-[var(--color-bg)]/80 backdrop-blur-sm">
<div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
<Link href={`/${locale}`} className="text-lg font-bold">
Learn Claude Code
</Link>
{/* Desktop nav */}
<nav className="hidden items-center gap-6 md:flex">
{NAV_ITEMS.map((item) => (
<Link
key={item.key}
href={`/${locale}${item.href}`}
className={cn(
"text-sm font-medium transition-colors hover:text-zinc-900 dark:hover:text-white",
pathname.includes(item.href)
? "text-zinc-900 dark:text-white"
: "text-zinc-500 dark:text-zinc-400"
)}
>
{t(item.key)}
</Link>
))}
{/* Locale switcher */}
<div className="flex items-center gap-1 rounded-lg border border-[var(--color-border)] p-0.5">
{LOCALES.map((l) => (
<button
key={l.code}
onClick={() => switchLocale(l.code)}
className={cn(
"rounded-md px-2 py-1 text-xs font-medium transition-colors",
locale === l.code
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900"
: "text-zinc-500 hover:text-zinc-700 dark:text-zinc-400"
)}
>
{l.label}
</button>
))}
</div>
<button
onClick={toggleDark}
className="rounded-md p-1.5 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-white"
>
{dark ? <Sun size={16} /> : <Moon size={16} />}
</button>
<a
href="https://github.com/shareAI-lab/learn-claude-code"
target="_blank"
rel="noopener"
className="text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-white"
>
<Github size={18} />
</a>
</nav>
{/* Mobile hamburger */}
<button
onClick={() => setMobileOpen(!mobileOpen)}
className="flex min-h-[44px] min-w-[44px] items-center justify-center md:hidden"
>
{mobileOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
{/* Mobile menu */}
{mobileOpen && (
<div className="border-t border-[var(--color-border)] bg-[var(--color-bg)] p-4 md:hidden">
{NAV_ITEMS.map((item) => (
<Link
key={item.key}
href={`/${locale}${item.href}`}
className="flex min-h-[44px] items-center text-sm"
onClick={() => setMobileOpen(false)}
>
{t(item.key)}
</Link>
))}
<div className="mt-3 flex items-center justify-between border-t border-[var(--color-border)] pt-3">
<div className="flex gap-2">
{LOCALES.map((l) => (
<button
key={l.code}
onClick={() => switchLocale(l.code)}
className={cn(
"min-h-[44px] min-w-[44px] rounded-md px-3 text-xs font-medium",
locale === l.code
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900"
: "border border-[var(--color-border)]"
)}
>
{l.label}
</button>
))}
</div>
<div className="flex items-center gap-2">
<button
onClick={toggleDark}
className="flex min-h-[44px] min-w-[44px] items-center justify-center rounded-md text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-white"
>
{dark ? <Sun size={18} /> : <Moon size={18} />}
</button>
<a
href="https://github.com/shareAI-lab/learn-claude-code"
target="_blank"
rel="noopener"
className="flex min-h-[44px] min-w-[44px] items-center justify-center text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-white"
>
<Github size={18} />
</a>
</div>
</div>
</div>
)}
</header>
);
}

View File

@@ -0,0 +1,66 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { LAYERS, VERSION_META } from "@/lib/constants";
import { useTranslations } from "@/lib/i18n";
import { cn } from "@/lib/utils";
const LAYER_DOT_BG: Record<string, string> = {
tools: "bg-blue-500",
planning: "bg-emerald-500",
memory: "bg-purple-500",
concurrency: "bg-amber-500",
collaboration: "bg-red-500",
};
export function Sidebar() {
const pathname = usePathname();
const locale = pathname.split("/")[1] || "en";
const t = useTranslations("sessions");
const tLayer = useTranslations("layer_labels");
return (
<nav className="hidden w-56 shrink-0 md:block">
<div className="sticky top-[calc(3.5rem+2rem)] space-y-5">
{LAYERS.map((layer) => (
<div key={layer.id}>
<div className="flex items-center gap-1.5 pb-1.5">
<span className={cn("h-2 w-2 rounded-full", LAYER_DOT_BG[layer.id])} />
<span className="text-[11px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500">
{tLayer(layer.id)}
</span>
</div>
<ul className="space-y-0.5">
{layer.versions.map((vId) => {
const meta = VERSION_META[vId];
const href = `/${locale}/${vId}`;
const isActive =
pathname === href ||
pathname === `${href}/` ||
pathname.startsWith(`${href}/diff`);
return (
<li key={vId}>
<Link
href={href}
className={cn(
"block rounded-md px-2.5 py-1.5 text-sm transition-colors",
isActive
? "bg-zinc-100 font-medium text-zinc-900 dark:bg-zinc-800 dark:text-white"
: "text-zinc-500 hover:bg-zinc-50 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800/50 dark:hover:text-zinc-300"
)}
>
<span className="font-mono text-xs">{vId}</span>
<span className="ml-1.5">{t(vId) || meta?.title}</span>
</Link>
</li>
);
})}
</ul>
</div>
))}
</div>
</nav>
);
}

View File

@@ -0,0 +1,95 @@
"use client";
import { useRef, useEffect, useState } from "react";
import { AnimatePresence } from "framer-motion";
import { useTranslations } from "@/lib/i18n";
import { useSimulator } from "@/hooks/useSimulator";
import { SimulatorControls } from "./simulator-controls";
import { SimulatorMessage } from "./simulator-message";
import type { Scenario } from "@/types/agent-data";
const scenarioModules: Record<string, () => Promise<{ default: Scenario }>> = {
s01: () => import("@/data/scenarios/s01.json") as Promise<{ default: Scenario }>,
s02: () => import("@/data/scenarios/s02.json") as Promise<{ default: Scenario }>,
s03: () => import("@/data/scenarios/s03.json") as Promise<{ default: Scenario }>,
s04: () => import("@/data/scenarios/s04.json") as Promise<{ default: Scenario }>,
s05: () => import("@/data/scenarios/s05.json") as Promise<{ default: Scenario }>,
s06: () => import("@/data/scenarios/s06.json") as Promise<{ default: Scenario }>,
s07: () => import("@/data/scenarios/s07.json") as Promise<{ default: Scenario }>,
s08: () => import("@/data/scenarios/s08.json") as Promise<{ default: Scenario }>,
s09: () => import("@/data/scenarios/s09.json") as Promise<{ default: Scenario }>,
s10: () => import("@/data/scenarios/s10.json") as Promise<{ default: Scenario }>,
s11: () => import("@/data/scenarios/s11.json") as Promise<{ default: Scenario }>,
};
interface AgentLoopSimulatorProps {
version: string;
}
export function AgentLoopSimulator({ version }: AgentLoopSimulatorProps) {
const t = useTranslations("version");
const [scenario, setScenario] = useState<Scenario | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const loader = scenarioModules[version];
if (loader) {
loader().then((mod) => setScenario(mod.default));
}
}, [version]);
const sim = useSimulator(scenario?.steps ?? []);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "smooth",
});
}
}, [sim.visibleSteps.length]);
if (!scenario) return null;
return (
<section>
<h2 className="mb-2 text-xl font-semibold">{t("simulator")}</h2>
<p className="mb-4 text-sm text-[var(--color-text-secondary)]">
{scenario.description}
</p>
<div className="overflow-hidden rounded-xl border border-[var(--color-border)]">
<div className="border-b border-[var(--color-border)] bg-zinc-50 px-4 py-3 dark:bg-zinc-900">
<SimulatorControls
isPlaying={sim.isPlaying}
isComplete={sim.isComplete}
currentIndex={sim.currentIndex}
totalSteps={sim.totalSteps}
speed={sim.speed}
onPlay={sim.play}
onPause={sim.pause}
onStep={sim.stepForward}
onReset={sim.reset}
onSpeedChange={sim.setSpeed}
/>
</div>
<div
ref={scrollRef}
className="flex max-h-[500px] min-h-[200px] flex-col gap-3 overflow-y-auto p-4"
>
{sim.visibleSteps.length === 0 && (
<div className="flex flex-1 items-center justify-center text-sm text-[var(--color-text-secondary)]">
Press Play or Step to begin
</div>
)}
<AnimatePresence mode="popLayout">
{sim.visibleSteps.map((step, i) => (
<SimulatorMessage key={i} step={step} index={i} />
))}
</AnimatePresence>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,99 @@
"use client";
import { useTranslations } from "@/lib/i18n";
import { Play, Pause, SkipForward, RotateCcw } from "lucide-react";
import { cn } from "@/lib/utils";
interface SimulatorControlsProps {
isPlaying: boolean;
isComplete: boolean;
currentIndex: number;
totalSteps: number;
speed: number;
onPlay: () => void;
onPause: () => void;
onStep: () => void;
onReset: () => void;
onSpeedChange: (speed: number) => void;
}
const SPEEDS = [0.5, 1, 2, 4];
export function SimulatorControls({
isPlaying,
isComplete,
currentIndex,
totalSteps,
speed,
onPlay,
onPause,
onStep,
onReset,
onSpeedChange,
}: SimulatorControlsProps) {
const t = useTranslations("sim");
return (
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-1.5">
{isPlaying ? (
<button
onClick={onPause}
className="flex h-9 w-9 items-center justify-center rounded-lg bg-zinc-900 text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
title={t("pause")}
>
<Pause size={16} />
</button>
) : (
<button
onClick={onPlay}
disabled={isComplete}
className="flex h-9 w-9 items-center justify-center rounded-lg bg-zinc-900 text-white transition-colors hover:bg-zinc-700 disabled:opacity-40 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
title={t("play")}
>
<Play size={16} />
</button>
)}
<button
onClick={onStep}
disabled={isComplete}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--color-border)] transition-colors hover:bg-zinc-100 disabled:opacity-40 dark:hover:bg-zinc-800"
title={t("step")}
>
<SkipForward size={16} />
</button>
<button
onClick={onReset}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--color-border)] transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
title={t("reset")}
>
<RotateCcw size={16} />
</button>
</div>
<div className="flex items-center gap-1.5">
<span className="text-xs text-[var(--color-text-secondary)]">
{t("speed")}:
</span>
{SPEEDS.map((s) => (
<button
key={s}
onClick={() => onSpeedChange(s)}
className={cn(
"rounded px-2 py-1 text-xs font-medium transition-colors",
speed === s
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900"
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text)]"
)}
>
{s}x
</button>
))}
</div>
<span className="ml-auto text-xs tabular-nums text-[var(--color-text-secondary)]">
{Math.max(0, currentIndex + 1)} {t("step_of")} {totalSteps}
</span>
</div>
);
}

View File

@@ -0,0 +1,93 @@
"use client";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
import type { SimStep } from "@/types/agent-data";
import { User, Bot, Terminal, ArrowRight, AlertCircle } from "lucide-react";
interface SimulatorMessageProps {
step: SimStep;
index: number;
}
const TYPE_CONFIG: Record<
string,
{ icon: typeof User; label: string; bgClass: string; borderClass: string }
> = {
user_message: {
icon: User,
label: "User",
bgClass: "bg-blue-50 dark:bg-blue-950/30",
borderClass: "border-blue-200 dark:border-blue-800",
},
assistant_text: {
icon: Bot,
label: "Assistant",
bgClass: "bg-zinc-50 dark:bg-zinc-900",
borderClass: "border-zinc-200 dark:border-zinc-700",
},
tool_call: {
icon: Terminal,
label: "Tool Call",
bgClass: "bg-amber-50 dark:bg-amber-950/30",
borderClass: "border-amber-200 dark:border-amber-800",
},
tool_result: {
icon: ArrowRight,
label: "Tool Result",
bgClass: "bg-emerald-50 dark:bg-emerald-950/30",
borderClass: "border-emerald-200 dark:border-emerald-800",
},
system_event: {
icon: AlertCircle,
label: "System",
bgClass: "bg-purple-50 dark:bg-purple-950/30",
borderClass: "border-purple-200 dark:border-purple-800",
},
};
export function SimulatorMessage({ step, index }: SimulatorMessageProps) {
const config = TYPE_CONFIG[step.type] || TYPE_CONFIG.assistant_text;
const Icon = config.icon;
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
className={cn(
"rounded-lg border p-3",
config.bgClass,
config.borderClass
)}
>
<div className="mb-1.5 flex items-center gap-2">
<Icon size={14} className="shrink-0 text-[var(--color-text-secondary)]" />
<span className="text-xs font-medium text-[var(--color-text-secondary)]">
{config.label}
{step.toolName && (
<span className="ml-1.5 font-mono text-[var(--color-text)]">
{step.toolName}
</span>
)}
</span>
</div>
{step.type === "tool_call" || step.type === "tool_result" ? (
<pre className="overflow-x-auto whitespace-pre-wrap rounded bg-zinc-900 p-2.5 font-mono text-xs leading-relaxed text-zinc-100 dark:bg-zinc-950">
{step.content || "(empty)"}
</pre>
) : step.type === "system_event" ? (
<pre className="overflow-x-auto whitespace-pre-wrap rounded bg-purple-900/80 p-2.5 font-mono text-xs leading-relaxed text-purple-100 dark:bg-purple-950">
{step.content}
</pre>
) : (
<p className="text-sm leading-relaxed">{step.content}</p>
)}
<p className="mt-2 text-xs italic text-[var(--color-text-secondary)]">
{step.annotation}
</p>
</motion.div>
);
}

View File

@@ -0,0 +1,215 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { useTranslations, useLocale } from "@/lib/i18n";
import { LEARNING_PATH, VERSION_META, LAYERS } from "@/lib/constants";
import { LayerBadge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import versionsData from "@/data/generated/versions.json";
const LAYER_DOT_BG: Record<string, string> = {
tools: "bg-blue-500",
planning: "bg-emerald-500",
memory: "bg-purple-500",
concurrency: "bg-amber-500",
collaboration: "bg-red-500",
};
const LAYER_LINE_BG: Record<string, string> = {
tools: "bg-blue-500/30",
planning: "bg-emerald-500/30",
memory: "bg-purple-500/30",
concurrency: "bg-amber-500/30",
collaboration: "bg-red-500/30",
};
const LAYER_BAR_BG: Record<string, string> = {
tools: "bg-blue-500",
planning: "bg-emerald-500",
memory: "bg-purple-500",
concurrency: "bg-amber-500",
collaboration: "bg-red-500",
};
function getVersionData(id: string) {
return versionsData.versions.find((v) => v.id === id);
}
const MAX_LOC = Math.max(
...versionsData.versions
.filter((v) => LEARNING_PATH.includes(v.id as (typeof LEARNING_PATH)[number]))
.map((v) => v.loc)
);
export function Timeline() {
const t = useTranslations("timeline");
const tv = useTranslations("version");
const locale = useLocale();
return (
<div className="flex flex-col gap-12">
{/* Layer Legend */}
<div>
<h3 className="mb-3 text-sm font-medium text-[var(--color-text-secondary)]">
{t("layer_legend")}
</h3>
<div className="flex flex-wrap gap-2">
{LAYERS.map((layer) => (
<div key={layer.id} className="flex items-center gap-1.5">
<span
className={cn("h-3 w-3 rounded-full", LAYER_DOT_BG[layer.id])}
/>
<span className="text-xs font-medium">{layer.label}</span>
</div>
))}
</div>
</div>
{/* Vertical Timeline */}
<div className="relative">
{LEARNING_PATH.map((versionId, index) => {
const meta = VERSION_META[versionId];
const data = getVersionData(versionId);
if (!meta || !data) return null;
const isLast = index === LEARNING_PATH.length - 1;
const locPercent = Math.round((data.loc / MAX_LOC) * 100);
return (
<div key={versionId} className="relative flex gap-4 pb-8 sm:gap-6">
{/* Timeline line + dot */}
<div className="flex flex-col items-center">
<div
className={cn(
"z-10 flex h-8 w-8 shrink-0 items-center justify-center rounded-full ring-4 ring-[var(--color-bg)] sm:h-10 sm:w-10",
LAYER_DOT_BG[meta.layer]
)}
>
<span className="text-[10px] font-bold text-white sm:text-xs">
{versionId.replace("s", "").replace("_mini", "m")}
</span>
</div>
{!isLast && (
<div
className={cn(
"w-0.5 flex-1",
LAYER_LINE_BG[
VERSION_META[LEARNING_PATH[index + 1]]?.layer || meta.layer
]
)}
/>
)}
</div>
{/* Content card */}
<div className="flex-1 pb-2">
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.4, delay: 0.1 }}
className="rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4 transition-colors hover:border-[var(--color-text-secondary)]/30 sm:p-5"
>
<div className="flex flex-wrap items-start gap-2">
<LayerBadge layer={meta.layer}>{versionId}</LayerBadge>
<span className="text-xs text-[var(--color-text-secondary)]">
{meta.coreAddition}
</span>
</div>
<h3 className="mt-2 text-base font-semibold sm:text-lg">
{meta.title}
<span className="ml-2 text-sm font-normal text-[var(--color-text-secondary)]">
{meta.subtitle}
</span>
</h3>
{/* Stats row */}
<div className="mt-3 flex flex-wrap items-center gap-4 text-xs text-[var(--color-text-secondary)]">
<span className="tabular-nums">
{data.loc} {tv("loc")}
</span>
<span className="tabular-nums">
{data.tools.length} {tv("tools")}
</span>
</div>
{/* LOC bar */}
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
<div
className={cn(
"h-full rounded-full transition-all",
LAYER_BAR_BG[meta.layer]
)}
style={{ width: `${locPercent}%` }}
/>
</div>
{/* Key insight */}
{meta.keyInsight && (
<p className="mt-3 text-sm italic text-[var(--color-text-secondary)]">
&ldquo;{meta.keyInsight}&rdquo;
</p>
)}
{/* Link */}
<Link
href={`/${locale}/${versionId}`}
className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-zinc-900 hover:underline dark:text-zinc-100"
>
{t("learn_more")}
<span aria-hidden="true">&rarr;</span>
</Link>
</motion.div>
</div>
</div>
);
})}
</div>
{/* LOC Growth Chart */}
<div>
<h3 className="mb-4 text-lg font-semibold">{t("loc_growth")}</h3>
<div className="flex flex-col gap-2">
{LEARNING_PATH.map((versionId) => {
const meta = VERSION_META[versionId];
const data = getVersionData(versionId);
if (!meta || !data) return null;
const widthPercent = Math.max(
2,
Math.round((data.loc / MAX_LOC) * 100)
);
return (
<div key={versionId} className="flex items-center gap-3">
<span className="w-8 shrink-0 text-right text-xs font-medium tabular-nums">
{versionId}
</span>
<div className="flex-1">
<div className="h-5 w-full overflow-hidden rounded bg-zinc-100 dark:bg-zinc-800">
<motion.div
initial={{ width: 0 }}
whileInView={{ width: `${widthPercent}%` }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.05 * LEARNING_PATH.indexOf(versionId) }}
className={cn(
"flex h-full items-center rounded px-2",
LAYER_BAR_BG[meta.layer]
)}
>
<span className="text-[10px] font-medium text-white">
{data.loc}
</span>
</motion.div>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { cn } from "@/lib/utils";
const LAYER_COLORS = {
tools:
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300",
planning:
"bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300",
memory:
"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300",
concurrency:
"bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300",
collaboration:
"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300",
} as const;
interface BadgeProps {
layer: keyof typeof LAYER_COLORS;
children: React.ReactNode;
className?: string;
}
export function LayerBadge({ layer, children, className }: BadgeProps) {
return (
<span
className={cn(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
LAYER_COLORS[layer],
className
)}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,39 @@
import { cn } from "@/lib/utils";
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export function Card({ className, children, ...props }: CardProps) {
return (
<div
className={cn(
"rounded-xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900",
className
)}
{...props}
>
{children}
</div>
);
}
export function CardHeader({ className, children, ...props }: CardProps) {
return (
<div className={cn("mb-4", className)} {...props}>
{children}
</div>
);
}
export function CardTitle({
className,
children,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h3 className={cn("text-lg font-semibold", className)} {...props}>
{children}
</h3>
);
}

View File

@@ -0,0 +1,37 @@
"use client";
import { useState } from "react";
import { cn } from "@/lib/utils";
interface TabsProps {
tabs: { id: string; label: string }[];
defaultTab?: string;
children: (activeTab: string) => React.ReactNode;
className?: string;
}
export function Tabs({ tabs, defaultTab, children, className }: TabsProps) {
const [active, setActive] = useState(defaultTab || tabs[0]?.id || "");
return (
<div className={className}>
<div className="flex border-b border-zinc-200 dark:border-zinc-700">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActive(tab.id)}
className={cn(
"px-4 py-2 text-sm font-medium transition-colors",
active === tab.id
? "border-b-2 border-zinc-900 text-zinc-900 dark:border-white dark:text-white"
: "text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200"
)}
>
{tab.label}
</button>
))}
</div>
<div className="mt-4">{children(active)}</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
import { lazy, Suspense } from "react";
import { useTranslations } from "@/lib/i18n";
const visualizations: Record<
string,
React.LazyExoticComponent<React.ComponentType<{ title?: string }>>
> = {
s01: lazy(() => import("./s01-agent-loop")),
s02: lazy(() => import("./s02-tool-dispatch")),
s03: lazy(() => import("./s03-todo-write")),
s04: lazy(() => import("./s04-subagent")),
s05: lazy(() => import("./s05-skill-loading")),
s06: lazy(() => import("./s06-context-compact")),
s07: lazy(() => import("./s07-task-system")),
s08: lazy(() => import("./s08-background-tasks")),
s09: lazy(() => import("./s09-agent-teams")),
s10: lazy(() => import("./s10-team-protocols")),
s11: lazy(() => import("./s11-autonomous-agents")),
};
export function SessionVisualization({ version }: { version: string }) {
const t = useTranslations("viz");
const Component = visualizations[version];
if (!Component) return null;
return (
<Suspense
fallback={
<div className="min-h-[500px] animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800" />
}
>
<div className="min-h-[500px]">
<Component title={t(version)} />
</div>
</Suspense>
);
}

View File

@@ -0,0 +1,416 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
import { useSvgPalette } from "@/hooks/useDarkMode";
// -- Flowchart node definitions --
interface FlowNode {
id: string;
label: string;
x: number;
y: number;
w: number;
h: number;
type: "rect" | "diamond";
}
const NODES: FlowNode[] = [
{ id: "start", label: "Start", x: 160, y: 30, w: 120, h: 40, type: "rect" },
{ id: "api_call", label: "API Call", x: 160, y: 110, w: 120, h: 40, type: "rect" },
{ id: "check", label: "stop_reason?", x: 160, y: 200, w: 140, h: 50, type: "diamond" },
{ id: "execute", label: "Execute Tool", x: 160, y: 300, w: 120, h: 40, type: "rect" },
{ id: "append", label: "Append Result", x: 160, y: 380, w: 120, h: 40, type: "rect" },
{ id: "end", label: "Break / Done", x: 380, y: 200, w: 120, h: 40, type: "rect" },
];
// Edges between nodes (SVG path data computed inline)
interface FlowEdge {
from: string;
to: string;
label?: string;
}
const EDGES: FlowEdge[] = [
{ from: "start", to: "api_call" },
{ from: "api_call", to: "check" },
{ from: "check", to: "execute", label: "tool_use" },
{ from: "execute", to: "append" },
{ from: "append", to: "api_call" },
{ from: "check", to: "end", label: "end_turn" },
];
// Which nodes light up at each step
const ACTIVE_NODES_PER_STEP: string[][] = [
[],
["start"],
["api_call"],
["check", "execute"],
["execute", "append"],
["api_call", "check", "execute", "append"],
["check", "end"],
];
// Which edges highlight at each step
const ACTIVE_EDGES_PER_STEP: string[][] = [
[],
[],
["start->api_call"],
["api_call->check", "check->execute"],
["execute->append"],
["append->api_call", "api_call->check", "check->execute", "execute->append"],
["api_call->check", "check->end"],
];
// -- Message blocks --
interface MessageBlock {
role: string;
detail: string;
colorClass: string;
}
const MESSAGES_PER_STEP: (MessageBlock | null)[][] = [
[],
[{ role: "user", detail: "Fix the login bug", colorClass: "bg-blue-500 dark:bg-blue-600" }],
[],
[{ role: "assistant", detail: "tool_use: read_file", colorClass: "bg-zinc-600 dark:bg-zinc-500" }],
[{ role: "tool_result", detail: "auth.ts contents...", colorClass: "bg-emerald-500 dark:bg-emerald-600" }],
[
{ role: "assistant", detail: "tool_use: edit_file", colorClass: "bg-zinc-600 dark:bg-zinc-500" },
{ role: "tool_result", detail: "file updated", colorClass: "bg-emerald-500 dark:bg-emerald-600" },
],
[{ role: "assistant", detail: "end_turn: Done!", colorClass: "bg-purple-500 dark:bg-purple-600" }],
];
// -- Step annotations --
const STEP_INFO = [
{ title: "The While Loop", desc: "Every agent is a while loop that keeps calling the model until it says 'stop'." },
{ title: "User Input", desc: "The loop starts when the user sends a message." },
{ title: "Call the Model", desc: "Send all messages to the LLM. It sees everything and decides what to do." },
{ title: "stop_reason: tool_use", desc: "The model wants to use a tool. The loop continues." },
{ title: "Execute & Append", desc: "Run the tool, append the result to messages[]. Feed it back." },
{ title: "Loop Again", desc: "Same code path, second iteration. The model decides to edit a file." },
{ title: "stop_reason: end_turn", desc: "The model is done. Loop exits. That's the entire agent." },
];
// -- Helpers --
function getNode(id: string): FlowNode {
return NODES.find((n) => n.id === id)!;
}
function edgePath(fromId: string, toId: string): string {
const from = getNode(fromId);
const to = getNode(toId);
// Loop-back: append -> api_call (goes to the left side and back up)
if (fromId === "append" && toId === "api_call") {
const startX = from.x - from.w / 2;
const startY = from.y;
const endX = to.x - to.w / 2;
const endY = to.y;
return `M ${startX} ${startY} L ${startX - 50} ${startY} L ${endX - 50} ${endY} L ${endX} ${endY}`;
}
// Horizontal: check -> end
if (fromId === "check" && toId === "end") {
const startX = from.x + from.w / 2;
const startY = from.y;
const endX = to.x - to.w / 2;
const endY = to.y;
return `M ${startX} ${startY} L ${endX} ${endY}`;
}
// Vertical (default)
const startX = from.x;
const startY = from.y + from.h / 2;
const endX = to.x;
const endY = to.y - to.h / 2;
return `M ${startX} ${startY} L ${endX} ${endY}`;
}
// -- Component --
export default function AgentLoop({ title }: { title?: string }) {
const {
currentStep,
totalSteps,
next,
prev,
reset,
isPlaying,
toggleAutoPlay,
} = useSteppedVisualization({ totalSteps: 7, autoPlayInterval: 2500 });
const palette = useSvgPalette();
const activeNodes = ACTIVE_NODES_PER_STEP[currentStep];
const activeEdges = ACTIVE_EDGES_PER_STEP[currentStep];
// Build accumulated messages up to the current step
const visibleMessages: MessageBlock[] = [];
for (let s = 0; s <= currentStep; s++) {
for (const msg of MESSAGES_PER_STEP[s]) {
if (msg) visibleMessages.push(msg);
}
}
const stepInfo = STEP_INFO[currentStep];
return (
<section className="min-h-[500px] space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "The Agent While-Loop"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<div className="flex flex-col gap-4 lg:flex-row">
{/* Left panel: SVG Flowchart (60%) */}
<div className="w-full lg:w-[60%]">
<div className="mb-2 font-mono text-xs text-zinc-400 dark:text-zinc-500">
while (stop_reason === "tool_use")
</div>
<svg
viewBox="0 0 500 440"
className="w-full rounded-md border border-zinc-100 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-950"
style={{ minHeight: 300 }}
>
<defs>
<filter id="glow-blue">
<feDropShadow dx="0" dy="0" stdDeviation="4" floodColor="#3b82f6" floodOpacity="0.7" />
</filter>
<filter id="glow-purple">
<feDropShadow dx="0" dy="0" stdDeviation="4" floodColor="#a855f7" floodOpacity="0.7" />
</filter>
<marker
id="arrowhead"
markerWidth="8"
markerHeight="6"
refX="8"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill={palette.arrowFill} />
</marker>
<marker
id="arrowhead-active"
markerWidth="8"
markerHeight="6"
refX="8"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill={palette.activeEdgeStroke} />
</marker>
</defs>
{/* Edges */}
{EDGES.map((edge) => {
const key = `${edge.from}->${edge.to}`;
const isActive = activeEdges.includes(key);
const d = edgePath(edge.from, edge.to);
return (
<g key={key}>
<motion.path
d={d}
fill="none"
stroke={isActive ? palette.activeEdgeStroke : palette.edgeStroke}
strokeWidth={isActive ? 2.5 : 1.5}
strokeDasharray={isActive ? "none" : "none"}
markerEnd={isActive ? "url(#arrowhead-active)" : "url(#arrowhead)"}
animate={{
stroke: isActive ? palette.activeEdgeStroke : palette.edgeStroke,
strokeWidth: isActive ? 2.5 : 1.5,
}}
transition={{ duration: 0.4 }}
/>
{edge.label && (
<text
x={
edge.from === "check" && edge.to === "end"
? (getNode("check").x + getNode("end").x) / 2
: getNode(edge.from).x + 75
}
y={
edge.from === "check" && edge.to === "end"
? getNode("check").y - 10
: (getNode(edge.from).y + getNode(edge.to).y) / 2
}
textAnchor="middle"
className="fill-zinc-400 text-[10px] dark:fill-zinc-500"
>
{edge.label}
</text>
)}
</g>
);
})}
{/* Nodes */}
{NODES.map((node) => {
const isActive = activeNodes.includes(node.id);
const isEnd = node.id === "end";
const filterAttr = isActive
? isEnd
? "url(#glow-purple)"
: "url(#glow-blue)"
: "none";
if (node.type === "diamond") {
// Diamond shape for decision node
const cx = node.x;
const cy = node.y;
const hw = node.w / 2;
const hh = node.h / 2;
const points = `${cx},${cy - hh} ${cx + hw},${cy} ${cx},${cy + hh} ${cx - hw},${cy}`;
return (
<g key={node.id}>
<motion.polygon
points={points}
rx={6}
fill={isActive ? palette.activeNodeFill : palette.nodeFill}
stroke={isActive ? palette.activeNodeStroke : palette.nodeStroke}
strokeWidth={1.5}
filter={filterAttr}
animate={{
fill: isActive ? palette.activeNodeFill : palette.nodeFill,
stroke: isActive ? palette.activeNodeStroke : palette.nodeStroke,
}}
transition={{ duration: 0.4 }}
/>
<motion.text
x={cx}
y={cy + 4}
textAnchor="middle"
fontSize={11}
fontWeight={600}
fontFamily="monospace"
animate={{ fill: isActive ? palette.activeNodeText : palette.nodeText }}
transition={{ duration: 0.4 }}
>
{node.label}
</motion.text>
</g>
);
}
return (
<g key={node.id}>
<motion.rect
x={node.x - node.w / 2}
y={node.y - node.h / 2}
width={node.w}
height={node.h}
rx={8}
fill={isActive ? (isEnd ? palette.endNodeFill : palette.activeNodeFill) : palette.nodeFill}
stroke={isActive ? (isEnd ? palette.endNodeStroke : palette.activeNodeStroke) : palette.nodeStroke}
strokeWidth={1.5}
filter={filterAttr}
animate={{
fill: isActive ? (isEnd ? palette.endNodeFill : palette.activeNodeFill) : palette.nodeFill,
stroke: isActive ? (isEnd ? palette.endNodeStroke : palette.activeNodeStroke) : palette.nodeStroke,
}}
transition={{ duration: 0.4 }}
/>
<motion.text
x={node.x}
y={node.y + 4}
textAnchor="middle"
fontSize={12}
fontWeight={600}
fontFamily="monospace"
animate={{ fill: isActive ? palette.activeNodeText : palette.nodeText }}
transition={{ duration: 0.4 }}
>
{node.label}
</motion.text>
</g>
);
})}
{/* Iteration counter */}
{currentStep >= 5 && (
<motion.text
x={60}
y={130}
textAnchor="middle"
fontSize={10}
fontFamily="monospace"
fill="#3b82f6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
iter #2
</motion.text>
)}
</svg>
</div>
{/* Right panel: messages[] array (40%) */}
<div className="w-full lg:w-[40%]">
<div className="mb-2 font-mono text-xs text-zinc-400 dark:text-zinc-500">
messages[]
</div>
<div className="min-h-[300px] space-y-2 rounded-md border border-zinc-100 bg-zinc-50 p-3 dark:border-zinc-800 dark:bg-zinc-950">
<AnimatePresence mode="popLayout">
{visibleMessages.length === 0 && (
<motion.div
key="empty"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="py-8 text-center text-xs text-zinc-400 dark:text-zinc-600"
>
[ empty ]
</motion.div>
)}
{visibleMessages.map((msg, i) => (
<motion.div
key={`${msg.role}-${msg.detail}-${i}`}
initial={{ opacity: 0, y: 12, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.35, type: "spring", bounce: 0.3 }}
className={`rounded-md px-3 py-2 ${msg.colorClass}`}
>
<div className="font-mono text-[11px] font-semibold text-white">
{msg.role}
</div>
<div className="mt-0.5 text-[10px] text-white/80">
{msg.detail}
</div>
</motion.div>
))}
</AnimatePresence>
{/* Array index markers */}
{visibleMessages.length > 0 && (
<div className="mt-3 border-t border-zinc-200 pt-2 dark:border-zinc-700">
<span className="font-mono text-[10px] text-zinc-400">
length: {visibleMessages.length}
</span>
</div>
)}
</div>
</div>
</div>
</div>
<StepControls
currentStep={currentStep}
totalSteps={totalSteps}
onPrev={prev}
onNext={next}
onReset={reset}
isPlaying={isPlaying}
onToggleAutoPlay={toggleAutoPlay}
stepTitle={stepInfo.title}
stepDescription={stepInfo.desc}
/>
</section>
);
}

View File

@@ -0,0 +1,380 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
import { useSvgPalette } from "@/hooks/useDarkMode";
// -- Tool definitions --
interface ToolDef {
name: string;
desc: string;
color: string;
activeColor: string;
darkColor: string;
darkActiveColor: string;
}
const TOOLS: ToolDef[] = [
{
name: "bash",
desc: "Execute shell commands",
color: "border-orange-300 bg-orange-50",
activeColor: "border-orange-500 bg-orange-100 ring-2 ring-orange-400",
darkColor: "dark:border-zinc-700 dark:bg-zinc-800/50",
darkActiveColor: "dark:border-orange-500 dark:bg-orange-950/40 dark:ring-orange-500",
},
{
name: "read_file",
desc: "Read file contents",
color: "border-sky-300 bg-sky-50",
activeColor: "border-sky-500 bg-sky-100 ring-2 ring-sky-400",
darkColor: "dark:border-zinc-700 dark:bg-zinc-800/50",
darkActiveColor: "dark:border-sky-500 dark:bg-sky-950/40 dark:ring-sky-500",
},
{
name: "write_file",
desc: "Create or overwrite a file",
color: "border-emerald-300 bg-emerald-50",
activeColor: "border-emerald-500 bg-emerald-100 ring-2 ring-emerald-400",
darkColor: "dark:border-zinc-700 dark:bg-zinc-800/50",
darkActiveColor: "dark:border-emerald-500 dark:bg-emerald-950/40 dark:ring-emerald-500",
},
{
name: "edit_file",
desc: "Apply targeted edits",
color: "border-violet-300 bg-violet-50",
activeColor: "border-violet-500 bg-violet-100 ring-2 ring-violet-400",
darkColor: "dark:border-zinc-700 dark:bg-zinc-800/50",
darkActiveColor: "dark:border-violet-500 dark:bg-violet-950/40 dark:ring-violet-500",
},
];
// Per-step: which tool index is active (-1 = none, 4 = all)
const ACTIVE_TOOL_PER_STEP: number[] = [-1, 0, 1, 2, 3, 4];
// Incoming request JSON per step
const REQUEST_PER_STEP: (string | null)[] = [
null,
'{ name: "bash", input: { cmd: "ls -la" } }',
'{ name: "read_file", input: { path: "src/auth.ts" } }',
'{ name: "write_file", input: { path: "config.json" } }',
'{ name: "edit_file", input: { path: "index.ts" } }',
null,
];
// Step annotations
const STEP_INFO = [
{ title: "The Dispatch Map", desc: "A dictionary maps tool names to handler functions. The loop code never changes." },
{ title: "Route: bash", desc: "tool_call.name -> handlers['bash'](input). Name-based routing." },
{ title: "Route: read_file", desc: "Same pattern, different handler. Validate input, execute, return result." },
{ title: "Route: write_file", desc: "Every tool returns a tool_result that goes back into messages[]." },
{ title: "Route: edit_file", desc: "Adding a new tool = adding one entry to the dispatch map." },
{ title: "The Key Insight", desc: "The while loop stays the same. You only grow the dispatch map. That's it." },
];
// SVG layout constants
const SVG_WIDTH = 600;
const SVG_HEIGHT = 320;
const DISPATCHER_X = SVG_WIDTH / 2;
const DISPATCHER_Y = 60;
const DISPATCHER_W = 160;
const DISPATCHER_H = 50;
const CARD_Y = 230;
const CARD_W = 110;
const CARD_H = 65;
const CARD_GAP = 20;
function getCardX(index: number): number {
const totalWidth = TOOLS.length * CARD_W + (TOOLS.length - 1) * CARD_GAP;
const startX = (SVG_WIDTH - totalWidth) / 2;
return startX + index * (CARD_W + CARD_GAP) + CARD_W / 2;
}
export default function ToolDispatch({ title }: { title?: string }) {
const {
currentStep,
totalSteps,
next,
prev,
reset,
isPlaying,
toggleAutoPlay,
} = useSteppedVisualization({ totalSteps: 6, autoPlayInterval: 2500 });
const palette = useSvgPalette();
const activeToolIdx = ACTIVE_TOOL_PER_STEP[currentStep];
const request = REQUEST_PER_STEP[currentStep];
const stepInfo = STEP_INFO[currentStep];
const isAllActive = activeToolIdx === 4;
return (
<section className="min-h-[500px] space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "Tool Dispatch Map"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
{/* Incoming request display */}
<div className="mb-4 flex min-h-[32px] items-center gap-2">
<span className="shrink-0 text-xs font-medium text-zinc-500 dark:text-zinc-400">
Incoming:
</span>
<AnimatePresence mode="wait">
{request && (
<motion.code
key={request}
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.3 }}
className="rounded bg-blue-100 px-2.5 py-1 font-mono text-xs font-medium text-blue-800 dark:bg-blue-900/40 dark:text-blue-300"
>
{request}
</motion.code>
)}
{!request && currentStep === 0 && (
<motion.span
key="waiting"
initial={{ opacity: 0 }}
animate={{ opacity: 0.6 }}
className="text-xs text-zinc-400 dark:text-zinc-600"
>
waiting for tool_call...
</motion.span>
)}
{isAllActive && (
<motion.span
key="all-routes"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-xs font-medium text-emerald-600 dark:text-emerald-400"
>
All routes active
</motion.span>
)}
</AnimatePresence>
</div>
{/* SVG dispatch diagram */}
<svg
viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`}
className="w-full rounded-md border border-zinc-100 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-950"
style={{ minHeight: 240 }}
>
<defs>
<filter id="dispatch-glow">
<feDropShadow dx="0" dy="0" stdDeviation="4" floodColor="#3b82f6" floodOpacity="0.6" />
</filter>
<filter id="card-glow-orange">
<feDropShadow dx="0" dy="0" stdDeviation="3" floodColor="#f97316" floodOpacity="0.6" />
</filter>
<filter id="card-glow-sky">
<feDropShadow dx="0" dy="0" stdDeviation="3" floodColor="#0ea5e9" floodOpacity="0.6" />
</filter>
<filter id="card-glow-emerald">
<feDropShadow dx="0" dy="0" stdDeviation="3" floodColor="#10b981" floodOpacity="0.6" />
</filter>
<filter id="card-glow-violet">
<feDropShadow dx="0" dy="0" stdDeviation="3" floodColor="#8b5cf6" floodOpacity="0.6" />
</filter>
<marker
id="dispatch-arrow"
markerWidth="8"
markerHeight="6"
refX="8"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill={palette.activeEdgeStroke} />
</marker>
<marker
id="dispatch-arrow-dim"
markerWidth="8"
markerHeight="6"
refX="8"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill={palette.arrowFill} />
</marker>
</defs>
{/* Dispatcher box */}
<motion.rect
x={DISPATCHER_X - DISPATCHER_W / 2}
y={DISPATCHER_Y - DISPATCHER_H / 2}
width={DISPATCHER_W}
height={DISPATCHER_H}
rx={10}
strokeWidth={2}
animate={{
fill: currentStep > 0 ? palette.activeNodeFill : palette.nodeFill,
stroke: currentStep > 0 ? palette.activeNodeStroke : palette.nodeStroke,
}}
filter={currentStep > 0 ? "url(#dispatch-glow)" : "none"}
transition={{ duration: 0.4 }}
/>
<motion.text
x={DISPATCHER_X}
y={DISPATCHER_Y + 1}
textAnchor="middle"
dominantBaseline="middle"
fontSize={13}
fontWeight={700}
fontFamily="monospace"
animate={{ fill: currentStep > 0 ? palette.activeNodeText : palette.nodeText }}
transition={{ duration: 0.4 }}
>
dispatch(name)
</motion.text>
{/* Connection lines from dispatcher to each tool card */}
{TOOLS.map((tool, i) => {
const cardX = getCardX(i);
const isActive = isAllActive || i === activeToolIdx;
const lineColor = isActive ? palette.activeEdgeStroke : palette.edgeStroke;
return (
<motion.line
key={`line-${tool.name}`}
x1={DISPATCHER_X}
y1={DISPATCHER_Y + DISPATCHER_H / 2}
x2={cardX}
y2={CARD_Y - CARD_H / 2}
strokeWidth={isActive ? 2.5 : 1.5}
markerEnd={isActive ? "url(#dispatch-arrow)" : "url(#dispatch-arrow-dim)"}
animate={{ stroke: lineColor, strokeWidth: isActive ? 2.5 : 1.5 }}
transition={{ duration: 0.4 }}
/>
);
})}
{/* Tool cards */}
{TOOLS.map((tool, i) => {
const cardX = getCardX(i);
const isActive = isAllActive || i === activeToolIdx;
const glowFilters = [
"url(#card-glow-orange)",
"url(#card-glow-sky)",
"url(#card-glow-emerald)",
"url(#card-glow-violet)",
];
const activeColors = ["#f97316", "#0ea5e9", "#10b981", "#8b5cf6"];
const activeBorders = ["#ea580c", "#0284c7", "#059669", "#7c3aed"];
return (
<g key={tool.name}>
<motion.rect
x={cardX - CARD_W / 2}
y={CARD_Y - CARD_H / 2}
width={CARD_W}
height={CARD_H}
rx={8}
strokeWidth={2}
animate={{
fill: isActive ? activeColors[i] : palette.nodeFill,
stroke: isActive ? activeBorders[i] : palette.nodeStroke,
}}
filter={isActive ? glowFilters[i] : "none"}
transition={{ duration: 0.4 }}
/>
<motion.text
x={cardX}
y={CARD_Y - 8}
textAnchor="middle"
dominantBaseline="middle"
fontSize={11}
fontWeight={700}
fontFamily="monospace"
animate={{ fill: isActive ? "#ffffff" : palette.nodeText }}
transition={{ duration: 0.4 }}
>
{tool.name}
</motion.text>
<motion.text
x={cardX}
y={CARD_Y + 12}
textAnchor="middle"
dominantBaseline="middle"
fontSize={8}
fontFamily="sans-serif"
animate={{ fill: isActive ? "rgba(255,255,255,0.8)" : palette.labelFill }}
transition={{ duration: 0.4 }}
>
{tool.desc}
</motion.text>
</g>
);
})}
{/* "+" extensibility indicator on last step */}
{isAllActive && (
<motion.g
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3, duration: 0.4 }}
>
<circle
cx={getCardX(3) + CARD_W / 2 + 30}
cy={CARD_Y}
r={16}
fill="none"
stroke="#3b82f6"
strokeWidth={2}
strokeDasharray="4 3"
/>
<text
x={getCardX(3) + CARD_W / 2 + 30}
y={CARD_Y + 1}
textAnchor="middle"
dominantBaseline="middle"
fontSize={18}
fontWeight={700}
fill="#3b82f6"
>
+
</text>
</motion.g>
)}
</svg>
{/* Code snippet below the diagram */}
<div className="mt-3 rounded-md bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
<code className="block font-mono text-[11px] leading-relaxed text-zinc-600 dark:text-zinc-300">
<span className="text-blue-600 dark:text-blue-400">const</span> handlers = {"{"}
{TOOLS.map((tool, i) => {
const isActive = isAllActive || i === activeToolIdx;
return (
<motion.span
key={tool.name}
animate={{
color: isActive ? "#3b82f6" : undefined,
fontWeight: isActive ? 700 : 400,
}}
className="text-zinc-600 dark:text-zinc-300"
>
{" "}{tool.name},
</motion.span>
);
})}
{" }{"}{"}"};
</code>
</div>
</div>
<StepControls
currentStep={currentStep}
totalSteps={totalSteps}
onPrev={prev}
onNext={next}
onReset={reset}
isPlaying={isPlaying}
onToggleAutoPlay={toggleAutoPlay}
stepTitle={stepInfo.title}
stepDescription={stepInfo.desc}
/>
</section>
);
}

View File

@@ -0,0 +1,323 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
// -- Task definitions --
type TaskStatus = "pending" | "in_progress" | "done";
interface Task {
id: number;
label: string;
status: TaskStatus;
}
// Snapshot of all 4 tasks at each step
const TASK_STATES: Task[][] = [
// Step 0: all pending
[
{ id: 1, label: "Write auth tests", status: "pending" },
{ id: 2, label: "Fix mobile layout", status: "pending" },
{ id: 3, label: "Add error handling", status: "pending" },
{ id: 4, label: "Update config loader", status: "pending" },
],
// Step 1: still all pending (idle round 1)
[
{ id: 1, label: "Write auth tests", status: "pending" },
{ id: 2, label: "Fix mobile layout", status: "pending" },
{ id: 3, label: "Add error handling", status: "pending" },
{ id: 4, label: "Update config loader", status: "pending" },
],
// Step 2: still all pending (idle round 2)
[
{ id: 1, label: "Write auth tests", status: "pending" },
{ id: 2, label: "Fix mobile layout", status: "pending" },
{ id: 3, label: "Add error handling", status: "pending" },
{ id: 4, label: "Update config loader", status: "pending" },
],
// Step 3: NAG fires, task 1 moves to in_progress
[
{ id: 1, label: "Write auth tests", status: "in_progress" },
{ id: 2, label: "Fix mobile layout", status: "pending" },
{ id: 3, label: "Add error handling", status: "pending" },
{ id: 4, label: "Update config loader", status: "pending" },
],
// Step 4: task 1 done
[
{ id: 1, label: "Write auth tests", status: "done" },
{ id: 2, label: "Fix mobile layout", status: "pending" },
{ id: 3, label: "Add error handling", status: "pending" },
{ id: 4, label: "Update config loader", status: "pending" },
],
// Step 5: task 2 self-directed to in_progress
[
{ id: 1, label: "Write auth tests", status: "done" },
{ id: 2, label: "Fix mobile layout", status: "in_progress" },
{ id: 3, label: "Add error handling", status: "pending" },
{ id: 4, label: "Update config loader", status: "pending" },
],
// Step 6: tasks 2,3 done, task 4 in_progress
[
{ id: 1, label: "Write auth tests", status: "done" },
{ id: 2, label: "Fix mobile layout", status: "done" },
{ id: 3, label: "Add error handling", status: "done" },
{ id: 4, label: "Update config loader", status: "in_progress" },
],
];
// Nag timer value at each step (out of 3)
const NAG_TIMER_PER_STEP = [0, 1, 2, 3, 0, 0, 0];
const NAG_THRESHOLD = 3;
// Whether the nag fires at this step
const NAG_FIRES_PER_STEP = [false, false, false, true, false, false, false];
// Step annotations
const STEP_INFO = [
{ title: "The Plan", desc: "TodoWrite gives the model a visible plan. All tasks start as pending." },
{ title: "Round 1 -- Idle", desc: "The model does work but doesn't touch its todos. The nag counter increments." },
{ title: "Round 2 -- Still Idle", desc: "Two rounds without progress. Pressure builds." },
{ title: "NAG!", desc: "Threshold reached! System message injected: 'You have pending tasks. Pick one up now!'" },
{ title: "Task Complete", desc: "The model completes the task. Timer stays at 0 -- working on todos resets the counter." },
{ title: "Self-Directed", desc: "Once the model learns the pattern, it picks up tasks voluntarily." },
{ title: "Mission Accomplished", desc: "Visible plan + nag pressure = reliable task completion." },
];
// -- Column component --
function KanbanColumn({
title,
tasks,
accentClass,
headerBg,
}: {
title: string;
tasks: Task[];
accentClass: string;
headerBg: string;
}) {
return (
<div className="flex min-h-[280px] flex-1 flex-col rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<div
className={`rounded-t-lg px-3 py-2 text-center text-xs font-bold uppercase tracking-wider ${headerBg}`}
>
{title}
<span className={`ml-1.5 inline-flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-bold ${accentClass}`}>
{tasks.length}
</span>
</div>
<div className="flex flex-1 flex-col gap-2 p-2">
<AnimatePresence mode="popLayout">
{tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}
</AnimatePresence>
{tasks.length === 0 && (
<div className="flex flex-1 items-center justify-center text-xs text-zinc-400 dark:text-zinc-600">
--
</div>
)}
</div>
</div>
);
}
// -- Task card --
function TaskCard({ task }: { task: Task }) {
const statusStyles: Record<TaskStatus, string> = {
pending: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400",
in_progress: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300",
done: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300",
};
const borderStyles: Record<TaskStatus, string> = {
pending: "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-800",
in_progress: "border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-950/30",
done: "border-emerald-300 bg-emerald-50 dark:border-emerald-700 dark:bg-emerald-950/30",
};
return (
<motion.div
layout
layoutId={`task-${task.id}`}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
className={`rounded-md border p-2.5 ${borderStyles[task.status]}`}
>
<div className="mb-1.5 flex items-center justify-between">
<span className="font-mono text-[10px] text-zinc-400 dark:text-zinc-500">
#{task.id}
</span>
<span
className={`rounded-full px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide ${statusStyles[task.status]}`}
>
{task.status.replace("_", " ")}
</span>
</div>
<div className="text-xs font-medium text-zinc-700 dark:text-zinc-300">
{task.label}
</div>
</motion.div>
);
}
// -- Nag gauge --
function NagGauge({ value, max, firing }: { value: number; max: number; firing: boolean }) {
const pct = Math.min((value / max) * 100, 100);
const barColor =
value === 0
? "bg-zinc-300 dark:bg-zinc-600"
: value === 1
? "bg-green-400 dark:bg-green-500"
: value === 2
? "bg-yellow-400 dark:bg-yellow-500"
: "bg-red-500 dark:bg-red-500";
return (
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-zinc-600 dark:text-zinc-300">
Nag Timer
</span>
<span className="font-mono text-xs text-zinc-500 dark:text-zinc-400">
{value}/{max}
</span>
</div>
<div className="relative h-4 w-full overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-700">
<motion.div
className={`absolute inset-y-0 left-0 rounded-full ${barColor}`}
initial={{ width: "0%" }}
animate={{
width: `${pct}%`,
...(firing ? { scale: [1, 1.05, 1] } : {}),
}}
transition={{
width: { duration: 0.5, ease: "easeOut" },
scale: { duration: 0.3, repeat: 2 },
}}
/>
{firing && (
<motion.div
className="absolute inset-0 rounded-full border-2 border-red-500"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 0, 1, 0] }}
transition={{ duration: 1 }}
/>
)}
</div>
</div>
);
}
// -- Main component --
export default function TodoWrite({ title }: { title?: string }) {
const {
currentStep,
totalSteps,
next,
prev,
reset,
isPlaying,
toggleAutoPlay,
} = useSteppedVisualization({ totalSteps: 7, autoPlayInterval: 2500 });
const tasks = TASK_STATES[currentStep];
const nagValue = NAG_TIMER_PER_STEP[currentStep];
const nagFires = NAG_FIRES_PER_STEP[currentStep];
const stepInfo = STEP_INFO[currentStep];
const pendingTasks = tasks.filter((t) => t.status === "pending");
const inProgressTasks = tasks.filter((t) => t.status === "in_progress");
const doneTasks = tasks.filter((t) => t.status === "done");
return (
<section className="min-h-[500px] space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "TodoWrite Nag System"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
{/* Nag gauge + nag message */}
<div className="mb-4 space-y-2">
<NagGauge value={nagValue} max={NAG_THRESHOLD} firing={nagFires} />
<AnimatePresence>
{nagFires && (
<motion.div
initial={{ opacity: 0, y: -8, height: 0 }}
animate={{ opacity: 1, y: 0, height: "auto" }}
exit={{ opacity: 0, y: -8, height: 0 }}
className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-center text-xs font-bold text-red-700 dark:border-red-700 dark:bg-red-950/30 dark:text-red-300"
>
SYSTEM: "You have pending tasks. Pick one up now!"
</motion.div>
)}
</AnimatePresence>
</div>
{/* Kanban board */}
<div className="flex gap-3">
<KanbanColumn
title="Pending"
tasks={pendingTasks}
accentClass="bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300"
headerBg="bg-zinc-200 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"
/>
<KanbanColumn
title="In Progress"
tasks={inProgressTasks}
accentClass="bg-amber-200 text-amber-700 dark:bg-amber-800 dark:text-amber-200"
headerBg="bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300"
/>
<KanbanColumn
title="Done"
tasks={doneTasks}
accentClass="bg-emerald-200 text-emerald-700 dark:bg-emerald-800 dark:text-emerald-200"
headerBg="bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300"
/>
</div>
{/* Progress summary */}
<div className="mt-3 flex items-center justify-between rounded-md bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
<span className="font-mono text-[11px] text-zinc-500 dark:text-zinc-400">
Progress: {doneTasks.length}/{tasks.length} complete
</span>
<div className="flex gap-0.5">
{tasks.map((t) => (
<div
key={t.id}
className={`h-2 w-6 rounded-sm ${
t.status === "done"
? "bg-emerald-500"
: t.status === "in_progress"
? "bg-amber-400"
: "bg-zinc-300 dark:bg-zinc-600"
}`}
/>
))}
</div>
</div>
</div>
<StepControls
currentStep={currentStep}
totalSteps={totalSteps}
onPrev={prev}
onNext={next}
onReset={reset}
isPlaying={isPlaying}
onToggleAutoPlay={toggleAutoPlay}
stepTitle={stepInfo.title}
stepDescription={stepInfo.desc}
/>
</section>
);
}

View File

@@ -0,0 +1,308 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
interface MessageBlock {
id: string;
label: string;
color: string;
}
const PARENT_BASE_MESSAGES: MessageBlock[] = [
{ id: "p1", label: "user: Build login + tests", color: "bg-blue-500" },
{ id: "p2", label: "assistant: Planning approach...", color: "bg-zinc-600" },
{ id: "p3", label: "tool_result: project structure", color: "bg-emerald-500" },
];
const TASK_PROMPT: MessageBlock = {
id: "task",
label: "task: Write unit tests for auth",
color: "bg-purple-500",
};
const CHILD_WORK_MESSAGES: MessageBlock[] = [
{ id: "c1", label: "tool_use: read auth.ts", color: "bg-amber-500" },
{ id: "c2", label: "tool_use: write test.ts", color: "bg-amber-500" },
];
const SUMMARY_BLOCK: MessageBlock = {
id: "summary",
label: "summary: 3 tests written, all passing",
color: "bg-teal-500",
};
const STEPS = [
{
title: "Parent Context",
description:
"The parent agent has accumulated messages from the conversation.",
},
{
title: "Spawn Subagent",
description:
"Task tool creates a child with fresh messages[]. Only the task description is passed.",
},
{
title: "Independent Work",
description:
"The child has its own context. It doesn't see the parent's history.",
},
{
title: "Compress Result",
description:
"The child's full conversation compresses into one summary.",
},
{
title: "Return Summary",
description:
"Only the summary returns. The child's full context is discarded.",
},
{
title: "Clean Context",
description:
"The parent gets a clean summary without context bloat. This is process isolation for LLMs.",
},
];
export default function SubagentIsolation({ title }: { title?: string }) {
const {
currentStep,
totalSteps,
next,
prev,
reset,
isPlaying,
toggleAutoPlay,
} = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });
// Derive what to show in each container based on step
const parentMessages: MessageBlock[] = (() => {
const base = [...PARENT_BASE_MESSAGES];
if (currentStep >= 5) {
base.push(SUMMARY_BLOCK);
}
return base;
})();
const childMessages: MessageBlock[] = (() => {
if (currentStep < 1) return [];
if (currentStep === 1) return [TASK_PROMPT];
if (currentStep === 2) return [TASK_PROMPT, ...CHILD_WORK_MESSAGES];
if (currentStep === 3) return [SUMMARY_BLOCK];
return currentStep >= 4 ? [TASK_PROMPT, ...CHILD_WORK_MESSAGES] : [];
})();
const showChildEmpty = currentStep === 0;
const showArcToChild = currentStep === 1;
const showCompression = currentStep === 3;
const showArcToParent = currentStep === 4;
const childDiscarded = currentStep >= 4;
const childFaded = currentStep >= 4;
return (
<section className="space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "Subagent Context Isolation"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900"
style={{ minHeight: 500 }}
>
{/* Main layout: two containers side by side */}
<div className="relative flex gap-4" style={{ minHeight: 340 }}>
{/* Parent Process Container */}
<div className="flex-1 rounded-xl border-2 border-blue-300 bg-blue-50/50 p-4 dark:border-blue-700 dark:bg-blue-950/20">
<div className="mb-3 flex items-center gap-2">
<div className="h-3 w-3 rounded-full bg-blue-500" />
<span className="text-sm font-bold text-blue-700 dark:text-blue-300">
Parent Process
</span>
</div>
<div className="mb-2 font-mono text-xs text-zinc-400">
messages[]
</div>
<div className="space-y-2">
<AnimatePresence>
{parentMessages.map((msg, i) => (
<motion.div
key={msg.id}
initial={{ opacity: 0, x: -12 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -12 }}
transition={{ duration: 0.4, delay: msg.id === "summary" ? 0.3 : 0 }}
className={`rounded-lg px-3 py-2 text-xs font-medium text-white shadow-sm ${msg.color}`}
>
{msg.label}
</motion.div>
))}
</AnimatePresence>
</div>
{currentStep >= 5 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="mt-3 rounded border border-blue-200 bg-white/60 px-2 py-1 text-center text-xs text-blue-600 dark:border-blue-700 dark:bg-blue-950/30 dark:text-blue-300"
>
3 original + 1 summary = clean context
</motion.div>
)}
</div>
{/* Isolation Wall */}
<div className="flex flex-col items-center justify-center gap-2">
<div className="h-full w-px border-l-2 border-dashed border-zinc-300 dark:border-zinc-600" />
<motion.div
animate={{
opacity: currentStep >= 1 && currentStep <= 4 ? 1 : 0.4,
}}
className="rounded bg-zinc-200 px-2 py-1 text-center font-mono text-[10px] text-zinc-500 dark:bg-zinc-700 dark:text-zinc-400"
style={{ writingMode: "vertical-rl", textOrientation: "mixed" }}
>
ISOLATION
</motion.div>
<div className="h-full w-px border-l-2 border-dashed border-zinc-300 dark:border-zinc-600" />
</div>
{/* Child Process Container */}
<div
className={`flex-1 rounded-xl border-2 p-4 transition-colors duration-300 ${
showChildEmpty
? "border-dashed border-zinc-300 bg-zinc-50/50 dark:border-zinc-600 dark:bg-zinc-800/30"
: childDiscarded
? "border-zinc-300 bg-zinc-100/50 dark:border-zinc-600 dark:bg-zinc-800/40"
: "border-purple-300 bg-purple-50/50 dark:border-purple-700 dark:bg-purple-950/20"
}`}
>
<div className="mb-3 flex items-center gap-2">
<div
className={`h-3 w-3 rounded-full ${
showChildEmpty
? "bg-zinc-300 dark:bg-zinc-600"
: childDiscarded
? "bg-zinc-400 dark:bg-zinc-500"
: "bg-purple-500"
}`}
/>
<span
className={`text-sm font-bold ${
showChildEmpty
? "text-zinc-400 dark:text-zinc-500"
: childDiscarded
? "text-zinc-400 dark:text-zinc-500"
: "text-purple-700 dark:text-purple-300"
}`}
>
Child Process
</span>
</div>
<div className="mb-2 font-mono text-xs text-zinc-400">
messages[] (fresh)
</div>
{showChildEmpty && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex h-24 items-center justify-center rounded-lg border border-dashed border-zinc-200 dark:border-zinc-700"
>
<span className="text-xs text-zinc-400">
not yet spawned
</span>
</motion.div>
)}
<div className="space-y-2">
<AnimatePresence>
{childMessages.map((msg) => (
<motion.div
key={msg.id + "-child"}
initial={{ opacity: 0, x: 12 }}
animate={{ opacity: childFaded ? 0.3 : 1, x: 0 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.4 }}
className={`rounded-lg px-3 py-2 text-xs font-medium text-white shadow-sm ${msg.color}`}
>
{msg.label}
</motion.div>
))}
</AnimatePresence>
</div>
{showCompression && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="mt-3 rounded border border-amber-300 bg-amber-50 px-2 py-1 text-center text-xs text-amber-700 dark:border-amber-600 dark:bg-amber-900/20 dark:text-amber-300"
>
Compressing full context into summary...
</motion.div>
)}
{childDiscarded && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-3 rounded border border-red-200 bg-red-50 px-2 py-1 text-center text-xs text-red-500 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
>
context discarded
</motion.div>
)}
</div>
{/* Animated arcs: task prompt going from parent to child */}
<AnimatePresence>
{showArcToChild && (
<motion.div
initial={{ opacity: 0, x: "20%", y: "-10%" }}
animate={{ opacity: 1, x: "55%", y: "-10%" }}
exit={{ opacity: 0 }}
transition={{ duration: 1.0, ease: "easeInOut" }}
className="pointer-events-none absolute left-0 top-0"
style={{ zIndex: 10 }}
>
<div className="rounded-lg bg-purple-500 px-3 py-1.5 text-xs font-medium text-white shadow-lg">
task prompt
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{showArcToParent && (
<motion.div
initial={{ opacity: 0, x: "75%", y: "60%" }}
animate={{ opacity: 1, x: "15%", y: "60%" }}
exit={{ opacity: 0 }}
transition={{ duration: 1.0, ease: "easeInOut" }}
className="pointer-events-none absolute left-0 top-0"
style={{ zIndex: 10 }}
>
<div className="rounded-lg bg-teal-500 px-3 py-1.5 text-xs font-medium text-white shadow-lg">
summary
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Step Controls */}
<div className="mt-6">
<StepControls
currentStep={currentStep}
totalSteps={totalSteps}
onPrev={prev}
onNext={next}
onReset={reset}
isPlaying={isPlaying}
onToggleAutoPlay={toggleAutoPlay}
stepTitle={STEPS[currentStep].title}
stepDescription={STEPS[currentStep].description}
/>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,407 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
interface SkillEntry {
name: string;
summary: string;
fullTokens: number;
content: string[];
}
const SKILLS: SkillEntry[] = [
{
name: "/commit",
summary: "Create git commits following repo conventions",
fullTokens: 320,
content: [
"1. Run git status + git diff to see changes",
"2. Analyze all staged changes and draft message",
"3. Create commit with Co-Authored-By trailer",
"4. Run git status after commit to verify",
],
},
{
name: "/review-pr",
summary: "Review pull requests for bugs and style",
fullTokens: 480,
content: [
"1. Fetch PR diff via gh pr view",
"2. Analyze changes file by file for issues",
"3. Check for bugs, security, and style problems",
"4. Post review comments with gh pr review",
],
},
{
name: "/test",
summary: "Run and analyze test suites",
fullTokens: 290,
content: [
"1. Detect test framework from package.json",
"2. Run test suite and capture output",
"3. Analyze failures and suggest fixes",
"4. Re-run after applying fixes",
],
},
{
name: "/deploy",
summary: "Deploy application to target environment",
fullTokens: 350,
content: [
"1. Verify all tests pass before deploy",
"2. Build production bundle",
"3. Push to deployment target via CI",
"4. Verify health check on deployed URL",
],
},
];
const TOKEN_STATES = [120, 120, 440, 440, 780, 780];
const MAX_TOKEN_DISPLAY = 1000;
const STEPS = [
{
title: "Layer 1: Compact Summaries",
description:
"All skills are summarized in the system prompt. Compact, always present.",
},
{
title: "Skill Invocation",
description:
'The model recognizes a skill invocation and triggers the Skill tool.',
},
{
title: "Layer 2: Full Injection",
description:
"The full skill instructions are injected as a tool_result, not into the system prompt.",
},
{
title: "In Context Now",
description:
"The detailed instructions appear as if a tool returned them. The model follows them precisely.",
},
{
title: "Stack Skills",
description:
"Multiple skills can be loaded. Only summaries are permanent; full content comes and goes.",
},
{
title: "Two-Layer Architecture",
description:
"Layer 1: always present, tiny. Layer 2: loaded on demand, detailed. Elegant separation.",
},
];
export default function SkillLoading({ title }: { title?: string }) {
const {
currentStep,
totalSteps,
next,
prev,
reset,
isPlaying,
toggleAutoPlay,
} = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });
const tokenCount = TOKEN_STATES[currentStep];
const highlightedSkill = currentStep >= 1 && currentStep <= 3 ? 0 : currentStep >= 4 ? 1 : -1;
const showFirstContent = currentStep >= 2;
const showSecondContent = currentStep >= 4;
const firstContentFaded = currentStep >= 5;
return (
<section className="space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "On-Demand Skill Loading"}
</h2>
<div
className="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900"
style={{ minHeight: 500 }}
>
<div className="flex gap-6">
{/* Main content area */}
<div className="flex-1 space-y-4">
{/* System Prompt Block */}
<div>
<div className="mb-2 flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-zinc-400" />
<span className="text-xs font-semibold text-zinc-600 dark:text-zinc-300">
System Prompt
</span>
<span className="rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-[10px] text-zinc-400 dark:bg-zinc-800">
always present
</span>
</div>
<div className="rounded-lg border border-zinc-300 bg-zinc-900 p-4 dark:border-zinc-600">
<div className="mb-2 font-mono text-[10px] text-zinc-500">
# Available Skills
</div>
<div className="space-y-1.5">
{SKILLS.map((skill, i) => {
const isHighlighted = i === highlightedSkill;
return (
<motion.div
key={skill.name}
animate={{
boxShadow: isHighlighted
? "0 0 12px 2px rgba(59, 130, 246, 0.5)"
: "0 0 0 0px rgba(59, 130, 246, 0)",
}}
transition={{ duration: 0.4 }}
className={`rounded px-3 py-1.5 font-mono text-xs transition-colors ${
isHighlighted
? "bg-blue-900/60 text-blue-300"
: "bg-zinc-800 text-zinc-400"
}`}
>
<span className="font-semibold text-zinc-200">
{skill.name}
</span>
{" - "}
{skill.summary}
</motion.div>
);
})}
</div>
</div>
</div>
{/* User invocation indicator */}
<AnimatePresence>
{currentStep === 1 && (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 dark:border-blue-800 dark:bg-blue-950/30"
>
<span className="text-xs text-blue-600 dark:text-blue-400">
User types:
</span>
<code className="rounded bg-blue-100 px-2 py-0.5 text-xs font-bold text-blue-800 dark:bg-blue-900/50 dark:text-blue-200">
/commit
</code>
</motion.div>
)}
{currentStep === 4 && (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 dark:border-blue-800 dark:bg-blue-950/30"
>
<span className="text-xs text-blue-600 dark:text-blue-400">
User types:
</span>
<code className="rounded bg-blue-100 px-2 py-0.5 text-xs font-bold text-blue-800 dark:bg-blue-900/50 dark:text-blue-200">
/review-pr
</code>
</motion.div>
)}
</AnimatePresence>
{/* Connecting arrow */}
<AnimatePresence>
{(showFirstContent || showSecondContent) && (
<motion.div
initial={{ opacity: 0, scaleY: 0 }}
animate={{ opacity: 1, scaleY: 1 }}
exit={{ opacity: 0 }}
className="flex justify-center"
>
<div className="flex flex-col items-center">
<div className="h-6 w-px bg-blue-400 dark:bg-blue-500" />
<div className="h-0 w-0 border-l-[5px] border-r-[5px] border-t-[6px] border-l-transparent border-r-transparent border-t-blue-400 dark:border-t-blue-500" />
</div>
</motion.div>
)}
</AnimatePresence>
{/* Expanded Skill Content Blocks */}
<div className="space-y-3">
<AnimatePresence>
{showFirstContent && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{
opacity: firstContentFaded ? 0.4 : 1,
height: "auto",
}}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.4 }}
className="overflow-hidden"
>
<div className="rounded-lg border-2 border-blue-300 bg-white p-4 dark:border-blue-700 dark:bg-zinc-800">
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-blue-500" />
<span className="text-xs font-bold text-blue-700 dark:text-blue-300">
SKILL.md: /commit
</span>
</div>
<span className="rounded bg-blue-100 px-1.5 py-0.5 font-mono text-[10px] text-blue-600 dark:bg-blue-900/40 dark:text-blue-300">
tool_result
</span>
</div>
<div className="space-y-1">
{SKILLS[0].content.map((line, i) => (
<motion.div
key={i}
initial={{ opacity: 0, x: -8 }}
animate={{
opacity: firstContentFaded ? 0.5 : 1,
x: 0,
}}
transition={{ delay: i * 0.08 }}
className="font-mono text-xs text-zinc-600 dark:text-zinc-300"
>
{line}
</motion.div>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{showSecondContent && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.4 }}
className="overflow-hidden"
>
<div className="rounded-lg border-2 border-purple-300 bg-white p-4 dark:border-purple-700 dark:bg-zinc-800">
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-purple-500" />
<span className="text-xs font-bold text-purple-700 dark:text-purple-300">
SKILL.md: /review-pr
</span>
</div>
<span className="rounded bg-purple-100 px-1.5 py-0.5 font-mono text-[10px] text-purple-600 dark:bg-purple-900/40 dark:text-purple-300">
tool_result
</span>
</div>
<div className="space-y-1">
{SKILLS[1].content.map((line, i) => (
<motion.div
key={i}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.08 }}
className="font-mono text-xs text-zinc-600 dark:text-zinc-300"
>
{line}
</motion.div>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Mechanism annotation on step 3 */}
<AnimatePresence>
{currentStep === 3 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-700 dark:bg-amber-900/20 dark:text-amber-300"
>
The Skill tool returns content as a tool_result message.
The model sees it in context and follows the instructions.
No system prompt bloat.
</motion.div>
)}
</AnimatePresence>
{/* Final overview label on step 5 */}
<AnimatePresence>
{currentStep === 5 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex gap-3"
>
<div className="flex-1 rounded border border-zinc-200 bg-zinc-50 p-2 text-center dark:border-zinc-700 dark:bg-zinc-800">
<div className="text-[10px] font-semibold text-zinc-500 dark:text-zinc-400">
LAYER 1
</div>
<div className="text-xs text-zinc-600 dark:text-zinc-300">
Always present, ~120 tokens
</div>
</div>
<div className="flex-1 rounded border border-blue-200 bg-blue-50 p-2 text-center dark:border-blue-700 dark:bg-blue-900/20">
<div className="text-[10px] font-semibold text-blue-500 dark:text-blue-400">
LAYER 2
</div>
<div className="text-xs text-blue-600 dark:text-blue-300">
On demand, ~300-500 tokens each
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Token Gauge (vertical bar on the right) */}
<div className="flex w-16 flex-col items-center">
<div className="mb-1 text-center font-mono text-[10px] text-zinc-400">
Tokens
</div>
<div
className="relative w-8 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800"
style={{ height: 300 }}
>
<motion.div
animate={{
height: `${(tokenCount / MAX_TOKEN_DISPLAY) * 100}%`,
}}
transition={{ duration: 0.5 }}
className={`absolute bottom-0 w-full rounded-full ${
tokenCount > 600
? "bg-amber-500"
: tokenCount > 300
? "bg-blue-500"
: "bg-emerald-500"
}`}
/>
</div>
<motion.div
key={tokenCount}
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
className="mt-2 text-center font-mono text-xs font-semibold text-zinc-600 dark:text-zinc-300"
>
{tokenCount}
</motion.div>
</div>
</div>
{/* Step Controls */}
<div className="mt-6">
<StepControls
currentStep={currentStep}
totalSteps={totalSteps}
onPrev={prev}
onNext={next}
onReset={reset}
isPlaying={isPlaying}
onToggleAutoPlay={toggleAutoPlay}
stepTitle={STEPS[currentStep].title}
stepDescription={STEPS[currentStep].description}
/>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,450 @@
"use client";
import { useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
type BlockType = "user" | "assistant" | "tool_result";
interface ContextBlock {
id: string;
type: BlockType;
label: string;
tokens: number;
}
const BLOCK_COLORS: Record<BlockType, string> = {
user: "bg-blue-500",
assistant: "bg-zinc-500 dark:bg-zinc-600",
tool_result: "bg-emerald-500",
};
const BLOCK_LABELS: Record<BlockType, string> = {
user: "USR",
assistant: "AST",
tool_result: "TRL",
};
function generateBlocks(count: number, seed: number): ContextBlock[] {
const types: BlockType[] = ["user", "assistant", "tool_result"];
const blocks: ContextBlock[] = [];
for (let i = 0; i < count; i++) {
const typeIndex = (i + seed) % 3;
const type = types[typeIndex];
const tokens = type === "tool_result" ? 4000 + (i % 3) * 1000 : 1500 + (i % 4) * 500;
blocks.push({
id: `b-${seed}-${i}`,
type,
label: `${BLOCK_LABELS[type]} ${i + 1}`,
tokens,
});
}
return blocks;
}
const MAX_TOKENS = 100000;
const WINDOW_HEIGHT = 350;
interface StepState {
blocks: { id: string; type: BlockType; label: string; heightPx: number; compressed?: boolean }[];
tokenCount: number;
fillPercent: number;
compressionLabel: string | null;
}
function computeStepState(step: number): StepState {
switch (step) {
case 0: {
const raw = generateBlocks(8, 0);
const tokenCount = 30000;
const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);
const blocks = raw.map((b) => ({
...b,
heightPx: Math.max(16, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.3),
}));
return { blocks, tokenCount, fillPercent: 30, compressionLabel: null };
}
case 1: {
const raw = generateBlocks(16, 0);
const tokenCount = 60000;
const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);
const blocks = raw.map((b) => ({
...b,
heightPx: Math.max(12, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.6),
}));
return { blocks, tokenCount, fillPercent: 60, compressionLabel: null };
}
case 2: {
const raw = generateBlocks(20, 0);
const tokenCount = 80000;
const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);
const blocks = raw.map((b) => ({
...b,
heightPx: Math.max(10, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.8),
}));
return { blocks, tokenCount, fillPercent: 80, compressionLabel: null };
}
case 3: {
const raw = generateBlocks(20, 0);
const tokenCount = 60000;
const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);
const blocks = raw.map((b) => ({
...b,
heightPx:
b.type === "tool_result"
? 6
: Math.max(12, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.6),
compressed: b.type === "tool_result",
}));
return {
blocks,
tokenCount,
fillPercent: 60,
compressionLabel: "MICRO-COMPACT",
};
}
case 4: {
const raw = generateBlocks(24, 1);
const tokenCount = 85000;
const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);
const blocks = raw.map((b) => ({
...b,
heightPx: Math.max(10, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.85),
}));
return { blocks, tokenCount, fillPercent: 85, compressionLabel: null };
}
case 5: {
const tokenCount = 25000;
const summaryBlock = {
id: "auto-summary",
type: "assistant" as BlockType,
label: "SUMMARY",
heightPx: 40,
compressed: false,
};
const recentBlocks = generateBlocks(4, 2).map((b) => ({
...b,
heightPx: 20,
}));
return {
blocks: [summaryBlock, ...recentBlocks],
tokenCount,
fillPercent: 25,
compressionLabel: "AUTO-COMPACT",
};
}
case 6: {
const tokenCount = 8000;
const compactBlock = {
id: "compact-summary",
type: "assistant" as BlockType,
label: "COMPACT SUMMARY",
heightPx: 24,
compressed: false,
};
return {
blocks: [compactBlock],
tokenCount,
fillPercent: 8,
compressionLabel: "/compact",
};
}
default:
return { blocks: [], tokenCount: 0, fillPercent: 0, compressionLabel: null };
}
}
const STEPS = [
{
title: "Growing Context",
description:
"The context window holds the conversation. Each API call adds more messages.",
},
{
title: "Context Growing",
description:
"As the agent works, messages accumulate. The context window fills up.",
},
{
title: "Approaching Limit",
description:
"Old tool_results are the biggest consumers. Micro-compact targets these first.",
},
{
title: "Stage 1: Micro-Compact",
description:
"Replace old tool_results with short summaries. Automatic, transparent to the model.",
},
{
title: "Still Growing",
description:
"Work continues. Context grows again toward the threshold...",
},
{
title: "Stage 2: Auto-Compact",
description:
"Entire conversation summarized into a compact block. Triggered at token threshold.",
},
{
title: "Stage 3: /compact",
description:
"User-triggered, most aggressive. Three layers of strategic forgetting enable infinite sessions.",
},
];
export default function ContextCompact({ title }: { title?: string }) {
const {
currentStep,
totalSteps,
next,
prev,
reset,
isPlaying,
toggleAutoPlay,
} = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });
const state = useMemo(() => computeStepState(currentStep), [currentStep]);
const fillColor =
state.fillPercent > 75
? "bg-red-500"
: state.fillPercent > 45
? "bg-amber-500"
: "bg-emerald-500";
const tokenDisplay = `${(state.tokenCount / 1000).toFixed(0)}K`;
return (
<section className="space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "Three-Layer Context Compression"}
</h2>
<div
className="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900"
style={{ minHeight: 500 }}
>
<div className="flex gap-6">
{/* Token Window (tall vertical bar on the left) */}
<div className="flex flex-col items-center">
<div className="mb-2 font-mono text-[10px] font-semibold text-zinc-500 dark:text-zinc-400">
Context Window
</div>
<div
className="relative w-24 overflow-hidden rounded-xl border-2 border-zinc-300 bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800"
style={{ height: WINDOW_HEIGHT }}
>
{/* Blocks stacked from bottom up */}
<div className="absolute bottom-0 left-0 right-0 flex flex-col-reverse gap-px p-1">
<AnimatePresence mode="popLayout">
{state.blocks.map((block) => (
<motion.div
key={block.id}
initial={{ opacity: 0, scaleY: 0 }}
animate={{
opacity: 1,
scaleY: 1,
height: block.heightPx,
}}
exit={{ opacity: 0, scaleY: 0 }}
transition={{ duration: 0.4 }}
className={`flex w-full items-center justify-center rounded-sm ${
block.compressed
? "bg-emerald-300 dark:bg-emerald-700"
: BLOCK_COLORS[block.type]
}`}
style={{ originY: 1 }}
>
{block.heightPx >= 14 && (
<span className="truncate px-1 text-[8px] font-medium text-white">
{block.label}
</span>
)}
</motion.div>
))}
</AnimatePresence>
</div>
{/* Fill level line */}
<motion.div
animate={{ bottom: `${state.fillPercent}%` }}
transition={{ duration: 0.5 }}
className="absolute left-0 right-0 border-t-2 border-dashed border-red-400 dark:border-red-500"
>
<span className="absolute -top-4 right-1 font-mono text-[9px] font-bold text-red-500 dark:text-red-400">
{state.fillPercent}%
</span>
</motion.div>
</div>
{/* Token count */}
<motion.div
key={state.tokenCount}
initial={{ scale: 0.85 }}
animate={{ scale: 1 }}
className="mt-2 font-mono text-sm font-bold text-zinc-700 dark:text-zinc-200"
>
{tokenDisplay}
</motion.div>
<div className="font-mono text-[10px] text-zinc-400">
/ 100K
</div>
</div>
{/* Right side: state display and compression stage */}
<div className="flex flex-1 flex-col justify-between">
{/* Top: horizontal token bar */}
<div>
<div className="mb-1 flex items-center justify-between">
<span className="text-xs text-zinc-500 dark:text-zinc-400">
Token usage
</span>
<span className="font-mono text-xs text-zinc-500">
{state.tokenCount.toLocaleString()} / {MAX_TOKENS.toLocaleString()}
</span>
</div>
<div className="h-3 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
<motion.div
animate={{ width: `${state.fillPercent}%` }}
transition={{ duration: 0.5 }}
className={`h-full rounded-full ${fillColor}`}
/>
</div>
</div>
{/* Message type legend */}
<div className="mt-4 flex items-center gap-4">
<div className="flex items-center gap-1">
<div className="h-3 w-3 rounded bg-blue-500" />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">user</span>
</div>
<div className="flex items-center gap-1">
<div className="h-3 w-3 rounded bg-zinc-500" />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">assistant</span>
</div>
<div className="flex items-center gap-1">
<div className="h-3 w-3 rounded bg-emerald-500" />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">tool_result</span>
</div>
</div>
{/* Highlight old tool_results at step 2 */}
<AnimatePresence>
{currentStep === 2 && (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="mt-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 dark:border-amber-700 dark:bg-amber-900/20"
>
<div className="text-xs font-semibold text-amber-700 dark:text-amber-300">
tool_results are the largest blocks
</div>
<div className="text-[11px] text-amber-600 dark:text-amber-400">
File contents, command outputs, search results -- each one is thousands of tokens.
</div>
</motion.div>
)}
</AnimatePresence>
{/* Compression stage label */}
<AnimatePresence>
{state.compressionLabel && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.4 }}
className="mt-4"
>
<div className={`rounded-lg border-2 p-4 text-center ${
currentStep === 3
? "border-amber-400 bg-amber-50 dark:border-amber-600 dark:bg-amber-900/20"
: currentStep === 5
? "border-blue-400 bg-blue-50 dark:border-blue-600 dark:bg-blue-900/20"
: "border-emerald-400 bg-emerald-50 dark:border-emerald-600 dark:bg-emerald-900/20"
}`}>
<div className={`text-lg font-black ${
currentStep === 3
? "text-amber-600 dark:text-amber-300"
: currentStep === 5
? "text-blue-600 dark:text-blue-300"
: "text-emerald-600 dark:text-emerald-300"
}`}>
{state.compressionLabel}
</div>
<div className={`mt-1 text-xs ${
currentStep === 3
? "text-amber-500 dark:text-amber-400"
: currentStep === 5
? "text-blue-500 dark:text-blue-400"
: "text-emerald-500 dark:text-emerald-400"
}`}>
{currentStep === 3 && "Old tool_results shrunk to tiny summaries"}
{currentStep === 5 && "Full conversation compressed to summary block"}
{currentStep === 6 && "Most aggressive compression -- near-empty context"}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Three stages overview on final step */}
{currentStep === 6 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="mt-4 space-y-2"
>
<div className="flex items-center gap-2 rounded bg-amber-50 px-3 py-1.5 dark:bg-amber-900/10">
<div className="h-2 w-2 rounded-full bg-amber-500" />
<span className="text-xs text-amber-700 dark:text-amber-300">
Stage 1: Micro -- shrink old tool_results
</span>
<span className="ml-auto font-mono text-[10px] text-amber-500">
automatic
</span>
</div>
<div className="flex items-center gap-2 rounded bg-blue-50 px-3 py-1.5 dark:bg-blue-900/10">
<div className="h-2 w-2 rounded-full bg-blue-500" />
<span className="text-xs text-blue-700 dark:text-blue-300">
Stage 2: Auto -- summarize entire conversation
</span>
<span className="ml-auto font-mono text-[10px] text-blue-500">
at threshold
</span>
</div>
<div className="flex items-center gap-2 rounded bg-emerald-50 px-3 py-1.5 dark:bg-emerald-900/10">
<div className="h-2 w-2 rounded-full bg-emerald-500" />
<span className="text-xs text-emerald-700 dark:text-emerald-300">
Stage 3: /compact -- user-triggered, deepest compression
</span>
<span className="ml-auto font-mono text-[10px] text-emerald-500">
manual
</span>
</div>
</motion.div>
)}
</div>
</div>
{/* Step Controls */}
<div className="mt-6">
<StepControls
currentStep={currentStep}
totalSteps={totalSteps}
onPrev={prev}
onNext={next}
onReset={reset}
isPlaying={isPlaying}
onToggleAutoPlay={toggleAutoPlay}
stepTitle={STEPS[currentStep].title}
stepDescription={STEPS[currentStep].description}
/>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,494 @@
"use client";
import { useMemo } from "react";
import { motion } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
import { useDarkMode, useSvgPalette } from "@/hooks/useDarkMode";
type TaskStatus = "pending" | "in_progress" | "completed" | "blocked";
interface TaskNode {
id: string;
label: string;
x: number;
y: number;
deps: string[];
}
interface StepInfo {
title: string;
description: string;
}
const TASKS: TaskNode[] = [
{ id: "T1", label: "T1: Setup DB", x: 80, y: 160, deps: [] },
{ id: "T2", label: "T2: API routes", x: 280, y: 80, deps: ["T1"] },
{ id: "T3", label: "T3: Auth module", x: 280, y: 240, deps: ["T1"] },
{ id: "T4", label: "T4: Integration", x: 480, y: 160, deps: ["T2", "T3"] },
{ id: "T5", label: "T5: Deploy", x: 650, y: 160, deps: ["T4"] },
];
const NODE_W = 140;
const NODE_H = 50;
const STEP_INFO: StepInfo[] = [
{
title: "File-Based Tasks",
description:
"Tasks are stored in JSON files on disk. They survive context compaction -- unlike in-memory state.",
},
{
title: "Start T1",
description:
"Tasks without dependencies can start immediately. T1 has no blockers.",
},
{
title: "T1 Complete",
description: "Completing T1 unblocks its dependents: T2 and T3.",
},
{
title: "Parallel Work",
description:
"T2 and T3 have no dependency on each other. Both can run simultaneously.",
},
{
title: "Partial Unblock",
description:
"T4 depends on BOTH T2 and T3. It waits for all blockers to complete.",
},
{
title: "Fully Unblocked",
description: "All blockers resolved. T4 can now proceed.",
},
{
title: "Graph Resolved",
description:
"The entire dependency graph is resolved. File-based persistence means this works across context compressions.",
},
];
function getTaskStatus(taskId: string, step: number): TaskStatus {
const statusMap: Record<string, TaskStatus[]> = {
T1: [
"pending",
"in_progress",
"completed",
"completed",
"completed",
"completed",
"completed",
],
T2: [
"pending",
"pending",
"pending",
"in_progress",
"completed",
"completed",
"completed",
],
T3: [
"pending",
"pending",
"pending",
"in_progress",
"in_progress",
"completed",
"completed",
],
T4: [
"pending",
"pending",
"pending",
"pending",
"blocked",
"in_progress",
"completed",
],
T5: [
"pending",
"pending",
"pending",
"pending",
"pending",
"pending",
"completed",
],
};
return statusMap[taskId]?.[step] ?? "pending";
}
function isEdgeActive(fromId: string, toId: string, step: number): boolean {
const fromStatus = getTaskStatus(fromId, step);
const toStatus = getTaskStatus(toId, step);
return (
fromStatus === "completed" &&
(toStatus === "in_progress" || toStatus === "completed")
);
}
function getStatusColor(status: TaskStatus) {
switch (status) {
case "pending":
return {
fill: "#e2e8f0",
darkFill: "#27272a",
stroke: "#cbd5e1",
darkStroke: "#3f3f46",
text: "#475569",
darkText: "#d4d4d8",
};
case "in_progress":
return {
fill: "#fef3c7",
darkFill: "#451a0340",
stroke: "#f59e0b",
darkStroke: "#d97706",
text: "#b45309",
darkText: "#fbbf24",
};
case "completed":
return {
fill: "#d1fae5",
darkFill: "#06402740",
stroke: "#10b981",
darkStroke: "#059669",
text: "#047857",
darkText: "#34d399",
};
case "blocked":
return {
fill: "#fecaca",
darkFill: "#45050540",
stroke: "#ef4444",
darkStroke: "#dc2626",
text: "#dc2626",
darkText: "#f87171",
};
}
}
function getStatusLabel(status: TaskStatus): string {
switch (status) {
case "pending":
return "pending";
case "in_progress":
return "in_progress";
case "completed":
return "done";
case "blocked":
return "blocked";
}
}
function buildCurvePath(
x1: number,
y1: number,
x2: number,
y2: number
): string {
const midX = (x1 + x2) / 2;
return `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;
}
export default function TaskSystem({ title }: { title?: string }) {
const {
currentStep,
totalSteps,
next,
prev,
reset,
isPlaying,
toggleAutoPlay,
} = useSteppedVisualization({ totalSteps: 7, autoPlayInterval: 2500 });
const isDark = useDarkMode();
const palette = useSvgPalette();
const edges = useMemo(() => {
const result: {
fromId: string;
toId: string;
x1: number;
y1: number;
x2: number;
y2: number;
}[] = [];
for (const task of TASKS) {
for (const depId of task.deps) {
const dep = TASKS.find((t) => t.id === depId);
if (!dep) continue;
result.push({
fromId: dep.id,
toId: task.id,
x1: dep.x + NODE_W,
y1: dep.y + NODE_H / 2,
x2: task.x,
y2: task.y + NODE_H / 2,
});
}
}
return result;
}, []);
const stepInfo = STEP_INFO[currentStep];
return (
<section className="min-h-[500px] space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "Task Dependency Graph"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<svg viewBox="0 0 800 340" className="w-full" aria-label="Task DAG">
<defs>
<marker
id="arrowGray"
viewBox="0 0 10 10"
refX="9"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill={palette.arrowFill} />
</marker>
<marker
id="arrowGreen"
viewBox="0 0 10 10"
refX="9"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill="#10b981" />
</marker>
<marker
id="arrowRed"
viewBox="0 0 10 10"
refX="9"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill="#ef4444" />
</marker>
<filter id="glowAmber" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feFlood floodColor="#f59e0b" floodOpacity="0.4" result="color" />
<feComposite in="color" in2="blur" operator="in" result="glow" />
<feMerge>
<feMergeNode in="glow" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter
id="glowGreen"
x="-30%"
y="-30%"
width="160%"
height="160%"
>
<feGaussianBlur stdDeviation="3" result="blur" />
<feFlood floodColor="#10b981" floodOpacity="0.3" result="color" />
<feComposite in="color" in2="blur" operator="in" result="glow" />
<feMerge>
<feMergeNode in="glow" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
{/* Dependency edges */}
{edges.map(({ fromId, toId, x1, y1, x2, y2 }) => {
const active = isEdgeActive(fromId, toId, currentStep);
const toStatus = getTaskStatus(toId, currentStep);
const isBlocked = toStatus === "blocked";
let markerEnd = "url(#arrowGray)";
let strokeColor = palette.arrowFill;
if (active) {
markerEnd = "url(#arrowGreen)";
strokeColor = "#10b981";
} else if (isBlocked) {
markerEnd = "url(#arrowRed)";
strokeColor = "#ef4444";
}
return (
<motion.path
key={`${fromId}-${toId}`}
d={buildCurvePath(x1, y1, x2, y2)}
fill="none"
markerEnd={markerEnd}
animate={{
stroke: strokeColor,
strokeWidth: active ? 2.5 : 1.5,
strokeDasharray: isBlocked ? "6 4" : "none",
}}
transition={{ duration: 0.5 }}
/>
);
})}
{/* Task nodes */}
{TASKS.map((task) => {
const status = getTaskStatus(task.id, currentStep);
const colors = getStatusColor(status);
const statusLabel = getStatusLabel(status);
const isActive = status === "in_progress";
const isComplete = status === "completed";
let filterAttr: string | undefined;
if (isActive) filterAttr = "url(#glowAmber)";
else if (isComplete) filterAttr = "url(#glowGreen)";
return (
<g key={task.id} filter={filterAttr}>
<motion.rect
x={task.x}
y={task.y}
width={NODE_W}
height={NODE_H}
rx={8}
animate={{
fill: isDark ? colors.darkFill : colors.fill,
stroke: isDark ? colors.darkStroke : colors.stroke,
}}
strokeWidth={isActive ? 2 : 1.5}
transition={{ duration: 0.4 }}
/>
<text
x={task.x + NODE_W / 2}
y={task.y + 20}
textAnchor="middle"
dominantBaseline="middle"
fontSize="11"
fontWeight="600"
fill={isDark ? colors.darkText : colors.text}
>
{task.label}
</text>
<text
x={task.x + NODE_W / 2}
y={task.y + 38}
textAnchor="middle"
dominantBaseline="middle"
fontSize="9"
fontFamily="monospace"
fill={isDark ? colors.darkText : colors.text}
opacity={0.8}
>
{statusLabel}
</text>
</g>
);
})}
{/* Blocked annotation for T4 at step 4 */}
{currentStep === 4 && (
<motion.g
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<rect
x={445}
y={118}
width={170}
height={22}
rx={4}
fill={isDark ? "#451a03" : "#fef2f2"}
stroke={isDark ? "#dc2626" : "#fca5a5"}
strokeWidth={1}
/>
<text
x={530}
y={132}
textAnchor="middle"
dominantBaseline="middle"
fontSize="9"
fontFamily="monospace"
fill={isDark ? "#f87171" : "#dc2626"}
>
Blocked: waiting on T3
</text>
</motion.g>
)}
</svg>
{/* File persistence indicator */}
<div className="mt-3 flex items-center gap-2 rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 dark:border-zinc-700 dark:bg-zinc-800/60">
<svg
viewBox="0 0 24 24"
className="h-5 w-5 flex-shrink-0 text-zinc-400 dark:text-zinc-500"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776"
/>
</svg>
<div className="flex flex-col">
<span className="font-mono text-xs font-medium text-zinc-600 dark:text-zinc-300">
.tasks/tasks.json
</span>
<span className="text-[10px] text-zinc-400 dark:text-zinc-500">
Persisted to disk -- survives context compaction
</span>
</div>
<motion.div
className="ml-auto h-2 w-2 rounded-full bg-emerald-500"
animate={{ opacity: [1, 0.3, 1] }}
transition={{ repeat: Infinity, duration: 2 }}
/>
</div>
{/* Legend */}
<div className="mt-3 flex flex-wrap items-center gap-4">
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 rounded bg-zinc-300 dark:bg-zinc-600" />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">
pending
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 rounded bg-amber-400 dark:bg-amber-600" />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">
in_progress
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 rounded bg-emerald-400 dark:bg-emerald-600" />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">
completed
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 rounded bg-red-400 dark:bg-red-600" />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">
blocked
</span>
</div>
</div>
</div>
<StepControls
currentStep={currentStep}
totalSteps={totalSteps}
onPrev={prev}
onNext={next}
onReset={reset}
isPlaying={isPlaying}
onToggleAutoPlay={toggleAutoPlay}
stepTitle={stepInfo.title}
stepDescription={stepInfo.description}
/>
</section>
);
}

View File

@@ -0,0 +1,624 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
import { useDarkMode, useSvgPalette } from "@/hooks/useDarkMode";
interface StepInfo {
title: string;
description: string;
}
const STEP_INFO: StepInfo[] = [
{
title: "Three Lanes",
description:
"The agent has a main thread and can spawn daemon background threads for parallel work.",
},
{
title: "Main Thread Working",
description:
"The main agent loop runs as usual, processing user requests.",
},
{
title: "Spawn Background",
description:
"Background tasks run as daemon threads. The main loop doesn't wait for them.",
},
{
title: "Multiple Backgrounds",
description: "Multiple background tasks can run concurrently.",
},
{
title: "Task Completes",
description:
"Background task finishes. Its result goes to the notification queue.",
},
{
title: "Queue Fills",
description:
"Results accumulate in the queue, invisible to the model during this turn.",
},
{
title: "Drain Queue",
description:
"Just before the next LLM call, all queued notifications are injected as tool_results. Non-blocking, async.",
},
];
const LANE_Y = {
main: 60,
bg1: 140,
bg2: 220,
} as const;
const LANE_HEIGHT = 44;
const TIMELINE_LEFT = 160;
const TIMELINE_RIGHT = 720;
const TIMELINE_WIDTH = TIMELINE_RIGHT - TIMELINE_LEFT;
const QUEUE_Y = 300;
interface WorkBlock {
lane: "main" | "bg1" | "bg2";
startFraction: number;
endFraction: number;
color: string;
label?: string;
appearsAtStep: number;
completesAtStep?: number;
}
const WORK_BLOCKS: WorkBlock[] = [
{
lane: "main",
startFraction: 0,
endFraction: 1,
color: "#8b5cf6",
label: "Main agent loop",
appearsAtStep: 1,
},
{
lane: "bg1",
startFraction: 0.18,
endFraction: 0.75,
color: "#10b981",
label: "Run tests",
appearsAtStep: 2,
completesAtStep: 5,
},
{
lane: "bg2",
startFraction: 0.35,
endFraction: 0.58,
color: "#3b82f6",
label: "Lint code",
appearsAtStep: 3,
completesAtStep: 4,
},
];
interface ForkArrow {
fromFraction: number;
toLane: "bg1" | "bg2";
appearsAtStep: number;
}
const FORK_ARROWS: ForkArrow[] = [
{ fromFraction: 0.18, toLane: "bg1", appearsAtStep: 2 },
{ fromFraction: 0.35, toLane: "bg2", appearsAtStep: 3 },
];
interface QueueCard {
id: string;
label: string;
appearsAtStep: number;
drainsAtStep: number;
}
const QUEUE_CARDS: QueueCard[] = [
{
id: "lint-result",
label: "Lint: 0 errors",
appearsAtStep: 4,
drainsAtStep: 6,
},
{
id: "test-result",
label: "Tests: 42 passed",
appearsAtStep: 5,
drainsAtStep: 6,
},
];
function fractionToX(fraction: number): number {
return TIMELINE_LEFT + fraction * TIMELINE_WIDTH;
}
function getBlockEndFraction(block: WorkBlock, step: number): number {
if (step < block.appearsAtStep) return block.startFraction;
if (block.completesAtStep !== undefined && step >= block.completesAtStep) {
return block.endFraction;
}
const growthSteps = (block.completesAtStep ?? 6) - block.appearsAtStep;
const stepsElapsed = step - block.appearsAtStep;
const progress = Math.min(stepsElapsed / growthSteps, 1);
const range = block.endFraction - block.startFraction;
return block.startFraction + range * progress;
}
export default function BackgroundTasks({ title }: { title?: string }) {
const {
currentStep,
totalSteps,
next,
prev,
reset,
isPlaying,
toggleAutoPlay,
} = useSteppedVisualization({ totalSteps: 7, autoPlayInterval: 2500 });
const isDark = useDarkMode();
const palette = useSvgPalette();
const stepInfo = STEP_INFO[currentStep];
const llmCallFraction = 0.82;
const showLlmMarker = currentStep >= 5;
return (
<section className="min-h-[500px] space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "Background Task Lanes"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<svg viewBox="0 0 780 380" className="w-full" aria-label="Background task lanes">
<defs>
<marker
id="forkArrow"
viewBox="0 0 10 10"
refX="9"
refY="5"
markerWidth="5"
markerHeight="5"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill={palette.arrowFill} />
</marker>
<marker
id="drainArrow"
viewBox="0 0 10 10"
refX="9"
refY="5"
markerWidth="5"
markerHeight="5"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill="#f59e0b" />
</marker>
<filter id="blockGlow" x="-10%" y="-20%" width="120%" height="140%">
<feGaussianBlur stdDeviation="2" result="blur" />
<feFlood floodColor="#8b5cf6" floodOpacity="0.2" result="color" />
<feComposite in="color" in2="blur" operator="in" result="glow" />
<feMerge>
<feMergeNode in="glow" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
{/* Timeline axis */}
<line
x1={TIMELINE_LEFT}
y1={30}
x2={TIMELINE_RIGHT}
y2={30}
stroke={palette.labelFill}
strokeWidth={1}
strokeDasharray="4 3"
opacity={0.5}
/>
<text
x={TIMELINE_LEFT}
y={22}
fontSize="9"
fontFamily="monospace"
fill={palette.labelFill}
>
t=0
</text>
<text
x={TIMELINE_RIGHT}
y={22}
fontSize="9"
fontFamily="monospace"
fill={palette.labelFill}
textAnchor="end"
>
time
</text>
{/* Lane backgrounds and labels */}
{(
[
{ key: "main", y: LANE_Y.main, label: "Main Thread" },
{ key: "bg1", y: LANE_Y.bg1, label: "Background 1" },
{ key: "bg2", y: LANE_Y.bg2, label: "Background 2" },
] as const
).map(({ key, y, label }) => (
<g key={key}>
<rect
x={TIMELINE_LEFT}
y={y}
width={TIMELINE_WIDTH}
height={LANE_HEIGHT}
rx={6}
fill="none"
stroke={palette.nodeStroke}
strokeWidth={1}
strokeDasharray="4 2"
opacity={0.6}
/>
<text
x={TIMELINE_LEFT - 10}
y={y + LANE_HEIGHT / 2 + 1}
textAnchor="end"
dominantBaseline="middle"
fontSize="11"
fontWeight="600"
fill={palette.labelFill}
>
{label}
</text>
</g>
))}
{/* Work blocks */}
{WORK_BLOCKS.map((block) => {
if (currentStep < block.appearsAtStep) return null;
const startX = fractionToX(block.startFraction);
const endFraction = getBlockEndFraction(block, currentStep);
const endX = fractionToX(endFraction);
const width = Math.max(endX - startX, 4);
const y = LANE_Y[block.lane];
const isComplete =
block.completesAtStep !== undefined &&
currentStep >= block.completesAtStep;
return (
<motion.g key={`${block.lane}-block`}>
<motion.rect
x={startX}
y={y + 4}
height={LANE_HEIGHT - 8}
rx={5}
initial={{ width: 4 }}
animate={{
width,
opacity: isComplete ? 0.7 : 1,
}}
transition={{ duration: 0.6, ease: "easeOut" }}
fill={block.color}
filter={
!isComplete && block.lane === "main"
? "url(#blockGlow)"
: undefined
}
/>
{width > 60 && block.label && (
<motion.text
x={startX + width / 2}
y={y + LANE_HEIGHT / 2 + 1}
textAnchor="middle"
dominantBaseline="middle"
fontSize="10"
fontWeight="500"
fill="white"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
>
{block.label}
</motion.text>
)}
{isComplete && (
<motion.text
x={endX + 6}
y={y + LANE_HEIGHT / 2 + 1}
dominantBaseline="middle"
fontSize="9"
fontFamily="monospace"
fill="#10b981"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
done
</motion.text>
)}
</motion.g>
);
})}
{/* Fork arrows from main to background lanes */}
{FORK_ARROWS.map((arrow) => {
if (currentStep < arrow.appearsAtStep) return null;
const x = fractionToX(arrow.fromFraction);
const fromY = LANE_Y.main + LANE_HEIGHT;
const toY = LANE_Y[arrow.toLane];
return (
<motion.line
key={`fork-${arrow.toLane}`}
x1={x}
y1={fromY}
x2={x + 20}
y2={toY}
stroke={palette.arrowFill}
strokeWidth={1.5}
markerEnd="url(#forkArrow)"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4 }}
/>
);
})}
{/* LLM API call marker */}
{showLlmMarker && (
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4 }}
>
<line
x1={fractionToX(llmCallFraction)}
y1={LANE_Y.main}
x2={fractionToX(llmCallFraction)}
y2={LANE_Y.main + LANE_HEIGHT}
stroke="#f59e0b"
strokeWidth={2}
strokeDasharray="3 2"
/>
<rect
x={fractionToX(llmCallFraction) - 36}
y={LANE_Y.main - 16}
width={72}
height={16}
rx={3}
fill="#f59e0b"
/>
<text
x={fractionToX(llmCallFraction)}
y={LANE_Y.main - 6}
textAnchor="middle"
dominantBaseline="middle"
fontSize="8"
fontWeight="600"
fill="white"
>
LLM API call
</text>
</motion.g>
)}
{/* Notification queue area */}
<rect
x={TIMELINE_LEFT}
y={QUEUE_Y}
width={TIMELINE_WIDTH}
height={54}
rx={8}
fill="none"
stroke={palette.nodeStroke}
strokeWidth={1}
/>
<text
x={TIMELINE_LEFT - 10}
y={QUEUE_Y + 18}
textAnchor="end"
fontSize="10"
fontWeight="600"
fill={palette.labelFill}
>
Notification
</text>
<text
x={TIMELINE_LEFT - 10}
y={QUEUE_Y + 32}
textAnchor="end"
fontSize="10"
fontWeight="600"
fill={palette.labelFill}
>
Queue
</text>
{/* Queue cards */}
<AnimatePresence>
{QUEUE_CARDS.map((card, idx) => {
if (currentStep < card.appearsAtStep) return null;
const isDraining = currentStep >= card.drainsAtStep;
const cardX = TIMELINE_LEFT + 20 + idx * 150;
const cardY = QUEUE_Y + 10;
const drainTargetY = LANE_Y.main + LANE_HEIGHT / 2 - 12;
const drainTargetX = fractionToX(llmCallFraction) + 10 + idx * 15;
if (isDraining) {
return (
<motion.g
key={`card-${card.id}-drain`}
initial={{ x: cardX, y: cardY, opacity: 1 }}
animate={{
x: drainTargetX,
y: drainTargetY,
opacity: [1, 1, 0],
}}
transition={{ duration: 0.8, ease: "easeInOut" }}
>
<rect
x={0}
y={0}
width={130}
height={34}
rx={5}
fill={isDark ? "#451a0340" : "#fef3c7"}
stroke="#f59e0b"
strokeWidth={1}
/>
<text
x={65}
y={13}
textAnchor="middle"
dominantBaseline="middle"
fontSize="9"
fontWeight="600"
fill={isDark ? "#fbbf24" : "#b45309"}
>
tool_result
</text>
<text
x={65}
y={26}
textAnchor="middle"
dominantBaseline="middle"
fontSize="8"
fontFamily="monospace"
fill={isDark ? "#f59e0b" : "#92400e"}
>
{card.label}
</text>
</motion.g>
);
}
return (
<motion.g
key={`card-${card.id}`}
initial={{ y: cardY - 40, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
<rect
x={cardX}
y={cardY}
width={130}
height={34}
rx={5}
fill={isDark ? "#06402740" : "#d1fae5"}
stroke="#10b981"
strokeWidth={1}
/>
<text
x={cardX + 65}
y={cardY + 13}
textAnchor="middle"
dominantBaseline="middle"
fontSize="9"
fontWeight="600"
fill={isDark ? "#34d399" : "#047857"}
>
tool_result
</text>
<text
x={cardX + 65}
y={cardY + 26}
textAnchor="middle"
dominantBaseline="middle"
fontSize="8"
fontFamily="monospace"
fill={isDark ? "#10b981" : "#065f46"}
>
{card.label}
</text>
</motion.g>
);
})}
</AnimatePresence>
{/* Drain arrows from queue to main thread at step 6 */}
{currentStep >= 6 && (
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<motion.line
x1={fractionToX(llmCallFraction) + 20}
y1={QUEUE_Y}
x2={fractionToX(llmCallFraction) + 20}
y2={LANE_Y.main + LANE_HEIGHT + 4}
stroke="#f59e0b"
strokeWidth={1.5}
markerEnd="url(#drainArrow)"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.5 }}
/>
</motion.g>
)}
{/* Empty queue label when drained */}
{currentStep >= 6 && (
<motion.text
x={TIMELINE_LEFT + TIMELINE_WIDTH / 2}
y={QUEUE_Y + 30}
textAnchor="middle"
dominantBaseline="middle"
fontSize="10"
fontFamily="monospace"
fill={palette.labelFill}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
>
queue drained -- injected into next LLM call
</motion.text>
)}
</svg>
{/* Legend */}
<div className="mt-3 flex flex-wrap items-center gap-4">
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 rounded" style={{ background: "#8b5cf6" }} />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">
Main thread
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 rounded" style={{ background: "#10b981" }} />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">
Background 1
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 rounded" style={{ background: "#3b82f6" }} />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">
Background 2
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-3 w-3 rounded" style={{ background: "#f59e0b" }} />
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">
LLM boundary
</span>
</div>
</div>
</div>
<StepControls
currentStep={currentStep}
totalSteps={totalSteps}
onPrev={prev}
onNext={next}
onReset={reset}
isPlaying={isPlaying}
onToggleAutoPlay={toggleAutoPlay}
stepTitle={stepInfo.title}
stepDescription={stepInfo.description}
/>
</section>
);
}

View File

@@ -0,0 +1,393 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
import { useSvgPalette } from "@/hooks/useDarkMode";
// -- Layout constants --
const SVG_W = 560;
const SVG_H = 340;
const AGENT_R = 40;
// Agent positions: inverted triangle (Lead top-center, Coder bottom-left, Reviewer bottom-right)
const AGENTS = [
{ id: "lead", label: "Lead", cx: SVG_W / 2, cy: 70, inbox: "lead.jsonl" },
{ id: "coder", label: "Coder", cx: 140, cy: 230, inbox: "coder.jsonl" },
{ id: "reviewer", label: "Reviewer", cx: SVG_W - 140, cy: 230, inbox: "reviewer.jsonl" },
] as const;
// Inbox tray dimensions, positioned below each agent circle
const TRAY_W = 72;
const TRAY_H = 22;
const TRAY_OFFSET_Y = AGENT_R + 14;
// Message block dimensions
const MSG_W = 60;
const MSG_H = 20;
function agentById(id: string) {
return AGENTS.find((a) => a.id === id)!;
}
function trayCenter(id: string) {
const a = agentById(id);
return { x: a.cx, y: a.cy + TRAY_OFFSET_Y + TRAY_H / 2 };
}
// Step configuration
const STEPS = [
{ title: "The Team", desc: "Teams use a leader-worker pattern. Each teammate has a file-based mailbox inbox." },
{ title: "Lead Assigns Work", desc: "Communication is async: write a message to the recipient's .jsonl inbox file." },
{ title: "Read Inbox", desc: "Teammates poll their inbox before each LLM call. New messages become context." },
{ title: "Independent Work", desc: "Each teammate runs its own agent loop independently." },
{ title: "Pass Result", desc: "Results flow through the same mailbox mechanism. All communication is via files." },
{ title: "Feedback Loop", desc: "The mailbox pattern supports any communication topology: linear, broadcast, round-robin." },
{ title: "File-Based Coordination", desc: "No shared memory, no locks. All coordination through append-only files. Simple, robust, debuggable." },
];
// Helper: determine which agent glows at each step
function agentGlows(agentId: string, step: number): boolean {
if (step === 1 && agentId === "lead") return true;
if (step === 2 && agentId === "coder") return true;
if (step === 3 && agentId === "coder") return true;
if (step === 4 && agentId === "coder") return true;
if (step === 5 && agentId === "reviewer") return true;
return false;
}
// Helper: determine which inbox tray has a message sitting in it
function trayHasMessage(agentId: string, step: number): boolean {
if (step === 2 && agentId === "coder") return true;
if (step === 4 && agentId === "reviewer") return false;
if (step === 5 && agentId === "reviewer") return true;
return false;
}
// Animated message that travels from one point to another
function TravelingMessage({
fromX,
fromY,
toX,
toY,
label,
delay = 0,
}: {
fromX: number;
fromY: number;
toX: number;
toY: number;
label: string;
delay?: number;
}) {
return (
<motion.g
initial={{ opacity: 0 }}
animate={{
opacity: [0, 1, 1, 0.8],
x: [fromX - MSG_W / 2, fromX - MSG_W / 2, toX - MSG_W / 2, toX - MSG_W / 2],
y: [fromY - MSG_H / 2, fromY - MSG_H / 2, toY - MSG_H / 2, toY - MSG_H / 2],
}}
transition={{
duration: 1.4,
delay,
times: [0, 0.1, 0.7, 1],
ease: "easeInOut",
}}
>
<rect width={MSG_W} height={MSG_H} rx={4} fill="#f59e0b" />
<text
x={MSG_W / 2}
y={MSG_H / 2 + 1}
textAnchor="middle"
dominantBaseline="middle"
fill="white"
fontSize={8}
fontWeight={600}
>
{label}
</text>
</motion.g>
);
}
// Faded trace line between two agents
function TraceLine({ from, to, strokeColor }: { from: string; to: string; strokeColor: string }) {
const f = trayCenter(from);
const t = trayCenter(to);
return (
<motion.line
x1={f.x}
y1={f.y}
x2={t.x}
y2={t.y}
stroke={strokeColor}
strokeWidth={1.5}
strokeDasharray="6 4"
initial={{ opacity: 0 }}
animate={{ opacity: 0.4 }}
transition={{ duration: 0.6 }}
/>
);
}
export default function AgentTeams({ title }: { title?: string }) {
const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });
const step = vis.currentStep;
const palette = useSvgPalette();
return (
<section className="space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "Agent Team Mailboxes"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900 min-h-[500px]">
<div className="flex flex-col lg:flex-row gap-4">
{/* SVG visualization */}
<div className="flex-1">
<svg viewBox={`0 0 ${SVG_W} ${SVG_H}`} className="w-full">
<defs>
<filter id="agent-glow">
<feGaussianBlur stdDeviation="4" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
{/* Step 6: trace lines */}
{step === 6 && (
<>
<TraceLine from="lead" to="coder" strokeColor={palette.edgeStroke} />
<TraceLine from="coder" to="reviewer" strokeColor={palette.edgeStroke} />
<TraceLine from="reviewer" to="lead" strokeColor={palette.edgeStroke} />
</>
)}
{/* Agent nodes */}
{AGENTS.map((agent) => {
const glowing = agentGlows(agent.id, step);
const pulsing = step === 3 && agent.id === "coder";
return (
<g key={agent.id}>
{/* Agent circle */}
<motion.circle
cx={agent.cx}
cy={agent.cy}
r={AGENT_R}
fill={glowing ? "#3b82f6" : palette.edgeStroke}
stroke={glowing ? "#60a5fa" : palette.labelFill}
strokeWidth={2}
animate={{
scale: pulsing ? [1, 1.08, 1] : 1,
fill: glowing ? "#3b82f6" : palette.edgeStroke,
}}
transition={
pulsing
? { duration: 0.8, repeat: Infinity, ease: "easeInOut" }
: { duration: 0.4 }
}
filter={glowing ? "url(#agent-glow)" : undefined}
/>
{/* Agent label */}
<text
x={agent.cx}
y={agent.cy + 1}
textAnchor="middle"
dominantBaseline="middle"
fill="white"
fontSize={12}
fontWeight={700}
>
{agent.label}
</text>
{/* Inbox tray (file icon style) */}
<rect
x={agent.cx - TRAY_W / 2}
y={agent.cy + TRAY_OFFSET_Y}
width={TRAY_W}
height={TRAY_H}
rx={3}
fill={trayHasMessage(agent.id, step) ? "#fef3c7" : palette.nodeFill}
stroke={trayHasMessage(agent.id, step) ? "#f59e0b" : palette.nodeStroke}
strokeWidth={1}
/>
<text
x={agent.cx}
y={agent.cy + TRAY_OFFSET_Y + TRAY_H / 2 + 1}
textAnchor="middle"
dominantBaseline="middle"
fontSize={8}
fontFamily="monospace"
fill={palette.labelFill}
>
{agent.inbox}
</text>
</g>
);
})}
{/* Step 0: team config card */}
{step === 0 && (
<motion.g
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<rect x={12} y={12} width={100} height={44} rx={4} fill="#f0f9ff" stroke="#bae6fd" strokeWidth={1} />
<text x={20} y={28} fontSize={7} fontFamily="monospace" fill="#0284c7" fontWeight={600}>
team.config
</text>
<text x={20} y={40} fontSize={6} fontFamily="monospace" fill="#0369a1">
workers: [coder, reviewer]
</text>
</motion.g>
)}
{/* Step 1: message from Lead to Coder inbox */}
<AnimatePresence>
{step === 1 && (
<TravelingMessage
key="msg-lead-coder"
fromX={agentById("lead").cx}
fromY={agentById("lead").cy + AGENT_R}
toX={agentById("coder").cx}
toY={agentById("coder").cy + TRAY_OFFSET_Y + TRAY_H / 2}
label="task:login"
/>
)}
</AnimatePresence>
{/* Step 2: message from Coder inbox to Coder circle */}
<AnimatePresence>
{step === 2 && (
<TravelingMessage
key="msg-inbox-coder"
fromX={agentById("coder").cx}
fromY={agentById("coder").cy + TRAY_OFFSET_Y + TRAY_H / 2}
toX={agentById("coder").cx}
toY={agentById("coder").cy}
label="task:login"
/>
)}
</AnimatePresence>
{/* Step 3: Coder working, result appears */}
<AnimatePresence>
{step === 3 && (
<motion.g
key="result-msg"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.8, duration: 0.4 }}
>
<rect
x={agentById("coder").cx + AGENT_R + 8}
y={agentById("coder").cy - MSG_H / 2}
width={MSG_W + 10}
height={MSG_H}
rx={4}
fill="#10b981"
/>
<text
x={agentById("coder").cx + AGENT_R + 8 + (MSG_W + 10) / 2}
y={agentById("coder").cy + 1}
textAnchor="middle"
dominantBaseline="middle"
fill="white"
fontSize={8}
fontWeight={600}
>
result:done
</text>
</motion.g>
)}
</AnimatePresence>
{/* Step 4: Coder result message travels to Reviewer inbox */}
<AnimatePresence>
{step === 4 && (
<TravelingMessage
key="msg-coder-reviewer"
fromX={agentById("coder").cx + AGENT_R + 8 + (MSG_W + 10) / 2}
fromY={agentById("coder").cy}
toX={agentById("reviewer").cx}
toY={agentById("reviewer").cy + TRAY_OFFSET_Y + TRAY_H / 2}
label="result:done"
/>
)}
</AnimatePresence>
{/* Step 5: Reviewer reads inbox, sends feedback to Lead */}
<AnimatePresence>
{step === 5 && (
<>
<TravelingMessage
key="msg-reviewer-read"
fromX={agentById("reviewer").cx}
fromY={agentById("reviewer").cy + TRAY_OFFSET_Y + TRAY_H / 2}
toX={agentById("reviewer").cx}
toY={agentById("reviewer").cy}
label="result:done"
delay={0}
/>
<TravelingMessage
key="msg-reviewer-lead"
fromX={agentById("reviewer").cx}
fromY={agentById("reviewer").cy}
toX={agentById("lead").cx}
toY={agentById("lead").cy + TRAY_OFFSET_Y + TRAY_H / 2}
label="feedback"
delay={1.0}
/>
</>
)}
</AnimatePresence>
{/* Step 6: filesystem tree */}
{step === 6 && (
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}
>
<rect x={SVG_W / 2 - 110} y={SVG_H - 80} width={220} height={68} rx={6} fill={palette.bgSubtle} stroke={palette.nodeStroke} strokeWidth={1} />
<text x={SVG_W / 2 - 96} y={SVG_H - 60} fontSize={8} fontFamily="monospace" fill={palette.labelFill}>
.claude/teams/project/
</text>
<text x={SVG_W / 2 - 82} y={SVG_H - 48} fontSize={8} fontFamily="monospace" fill="#60a5fa">
lead.jsonl
</text>
<text x={SVG_W / 2 - 82} y={SVG_H - 36} fontSize={8} fontFamily="monospace" fill="#60a5fa">
coder.jsonl
</text>
<text x={SVG_W / 2 - 82} y={SVG_H - 24} fontSize={8} fontFamily="monospace" fill="#60a5fa">
reviewer.jsonl
</text>
</motion.g>
)}
</svg>
</div>
</div>
{/* Step controls */}
<div className="mt-4">
<StepControls
currentStep={vis.currentStep}
totalSteps={vis.totalSteps}
onPrev={vis.prev}
onNext={vis.next}
onReset={vis.reset}
isPlaying={vis.isPlaying}
onToggleAutoPlay={vis.toggleAutoPlay}
stepTitle={STEPS[step].title}
stepDescription={STEPS[step].desc}
/>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,497 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
import { useSvgPalette } from "@/hooks/useDarkMode";
type Protocol = "shutdown" | "plan";
// -- Layout constants for the sequence diagram --
const SVG_W = 560;
const SVG_H = 360;
const LIFELINE_LEFT_X = 140;
const LIFELINE_RIGHT_X = 420;
const LIFELINE_TOP = 60;
const LIFELINE_BOTTOM = 330;
const ACTIVATION_W = 12;
const ARROW_Y_START = 110;
const ARROW_Y_GAP = 70;
// Request ID shown on message tags
const REQUEST_ID = "req_abc";
// -- Shutdown protocol step definitions --
const SHUTDOWN_STEPS = [
{ title: "Structured Protocols", desc: "Protocols define structured message exchanges with correlated request IDs." },
{ title: "Shutdown Request", desc: "The leader initiates shutdown. The request_id links the request to its response." },
{ title: "Teammate Decides", desc: "The teammate can accept or reject. It's not a forced kill -- it's a polite request." },
{ title: "Approved", desc: "Same request_id in the response. Teammate exits cleanly." },
];
// -- Plan approval protocol step definitions --
const PLAN_STEPS = [
{ title: "Plan Approval", desc: "Teammates in plan_mode must get approval before implementing changes." },
{ title: "Submit Plan", desc: "The teammate designs a plan and sends it to the leader for review." },
{ title: "Leader Reviews", desc: "Leader reviews and approves or rejects with feedback. Same request-response pattern." },
];
// Horizontal arrow between lifelines
function SequenceArrow({
y,
direction,
label,
tagLabel,
color,
tagBg,
tagStroke,
tagText,
}: {
y: number;
direction: "right" | "left";
label: string;
tagLabel?: string;
color: string;
tagBg?: string;
tagStroke?: string;
tagText?: string;
}) {
const fromX = direction === "right" ? LIFELINE_LEFT_X + ACTIVATION_W / 2 : LIFELINE_RIGHT_X - ACTIVATION_W / 2;
const toX = direction === "right" ? LIFELINE_RIGHT_X - ACTIVATION_W / 2 : LIFELINE_LEFT_X + ACTIVATION_W / 2;
const arrowTip = direction === "right" ? toX - 6 : toX + 6;
const labelX = (fromX + toX) / 2;
return (
<motion.g
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
{/* Arrow line */}
<line
x1={fromX}
y1={y}
x2={toX}
y2={y}
stroke={color}
strokeWidth={2}
/>
{/* Arrow head */}
<polygon
points={
direction === "right"
? `${toX},${y} ${arrowTip},${y - 4} ${arrowTip},${y + 4}`
: `${toX},${y} ${arrowTip},${y - 4} ${arrowTip},${y + 4}`
}
fill={color}
/>
{/* Message label */}
<text
x={labelX}
y={y - 10}
textAnchor="middle"
fontSize={8}
fontFamily="monospace"
fontWeight={600}
fill={color}
>
{label}
</text>
{/* Request ID tag */}
{tagLabel && (
<g>
<rect
x={labelX - 36}
y={y + 4}
width={72}
height={16}
rx={3}
fill={tagBg || "#f5f3ff"}
stroke={tagStroke || "#c4b5fd"}
strokeWidth={0.5}
/>
<text
x={labelX}
y={y + 14}
textAnchor="middle"
fontSize={6}
fontFamily="monospace"
fill={tagText || "#7c3aed"}
>
{tagLabel}
</text>
</g>
)}
</motion.g>
);
}
// Decision diamond on a lifeline
function DecisionBox({ x, y }: { x: number; y: number }) {
const size = 14;
return (
<motion.g
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4 }}
>
<polygon
points={`${x},${y - size} ${x + size},${y} ${x},${y + size} ${x - size},${y}`}
fill="#fef3c7"
stroke="#f59e0b"
strokeWidth={1}
/>
<text x={x} y={y + 1} textAnchor="middle" dominantBaseline="middle" fontSize={7} fontWeight={700} fill="#92400e">
?
</text>
<text x={x + size + 6} y={y - 4} fontSize={6} fontFamily="monospace" fill="#10b981">
approve
</text>
<text x={x + size + 6} y={y + 6} fontSize={6} fontFamily="monospace" fill="#ef4444">
reject
</text>
</motion.g>
);
}
// Activation bar on a lifeline
function ActivationBar({
x,
yStart,
yEnd,
color,
}: {
x: number;
yStart: number;
yEnd: number;
color: string;
}) {
return (
<motion.rect
x={x - ACTIVATION_W / 2}
y={yStart}
width={ACTIVATION_W}
height={yEnd - yStart}
rx={2}
fill={color}
initial={{ opacity: 0 }}
animate={{ opacity: 0.6 }}
transition={{ duration: 0.4 }}
/>
);
}
export default function TeamProtocols({ title }: { title?: string }) {
const [protocol, setProtocol] = useState<Protocol>("shutdown");
const totalSteps = protocol === "shutdown" ? SHUTDOWN_STEPS.length : PLAN_STEPS.length;
const steps = protocol === "shutdown" ? SHUTDOWN_STEPS : PLAN_STEPS;
const vis = useSteppedVisualization({ totalSteps, autoPlayInterval: 2500 });
const step = vis.currentStep;
const palette = useSvgPalette();
const switchProtocol = (p: Protocol) => {
setProtocol(p);
vis.reset();
};
const leftLabel = protocol === "shutdown" ? "Leader" : "Leader";
const rightLabel = protocol === "shutdown" ? "Teammate" : "Teammate";
return (
<section className="space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "FSM Team Protocols"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900 min-h-[500px]">
{/* Protocol toggle */}
<div className="flex justify-center gap-2 mb-4">
<button
onClick={() => switchProtocol("shutdown")}
className={`rounded-md px-4 py-1.5 text-xs font-medium transition-colors ${
protocol === "shutdown"
? "bg-blue-500 text-white"
: "bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700"
}`}
>
Shutdown Protocol
</button>
<button
onClick={() => switchProtocol("plan")}
className={`rounded-md px-4 py-1.5 text-xs font-medium transition-colors ${
protocol === "plan"
? "bg-emerald-500 text-white"
: "bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700"
}`}
>
Plan Approval Protocol
</button>
</div>
{/* Sequence diagram SVG */}
<svg viewBox={`0 0 ${SVG_W} ${SVG_H}`} className="w-full">
<defs>
<marker
id="seq-arrow"
viewBox="0 0 10 10"
refX="9"
refY="5"
markerWidth="5"
markerHeight="5"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill={palette.arrowFill} />
</marker>
</defs>
{/* Lifeline headers */}
<rect x={LIFELINE_LEFT_X - 40} y={20} width={80} height={28} rx={6} fill="#3b82f6" />
<text x={LIFELINE_LEFT_X} y={37} textAnchor="middle" dominantBaseline="middle" fill="white" fontSize={11} fontWeight={700}>
{leftLabel}
</text>
<rect x={LIFELINE_RIGHT_X - 40} y={20} width={80} height={28} rx={6} fill="#8b5cf6" />
<text x={LIFELINE_RIGHT_X} y={37} textAnchor="middle" dominantBaseline="middle" fill="white" fontSize={11} fontWeight={700}>
{rightLabel}
</text>
{/* Lifeline dashed lines */}
<line
x1={LIFELINE_LEFT_X}
y1={LIFELINE_TOP}
x2={LIFELINE_LEFT_X}
y2={LIFELINE_BOTTOM}
stroke={palette.edgeStroke}
strokeWidth={1}
strokeDasharray="6 4"
/>
<line
x1={LIFELINE_RIGHT_X}
y1={LIFELINE_TOP}
x2={LIFELINE_RIGHT_X}
y2={LIFELINE_BOTTOM}
stroke={palette.edgeStroke}
strokeWidth={1}
strokeDasharray="6 4"
/>
<AnimatePresence mode="wait">
{protocol === "shutdown" && (
<g key="shutdown">
{/* Activation bars appear as needed */}
{step >= 1 && (
<ActivationBar
x={LIFELINE_LEFT_X}
yStart={ARROW_Y_START - 10}
yEnd={step >= 3 ? ARROW_Y_START + ARROW_Y_GAP * 2 + 20 : ARROW_Y_START + 30}
color="#3b82f6"
/>
)}
{step >= 1 && (
<ActivationBar
x={LIFELINE_RIGHT_X}
yStart={ARROW_Y_START - 5}
yEnd={step >= 3 ? ARROW_Y_START + ARROW_Y_GAP * 2 + 15 : ARROW_Y_START + ARROW_Y_GAP + 20}
color="#8b5cf6"
/>
)}
{/* Step 1: shutdown_request arrow (Leader -> Teammate) */}
{step >= 1 && (
<SequenceArrow
y={ARROW_Y_START}
direction="right"
label="shutdown_request"
tagLabel={`request_id: ${REQUEST_ID}`}
color="#3b82f6"
tagBg={palette.bgSubtle}
tagStroke={palette.nodeStroke}
tagText={palette.nodeText}
/>
)}
{/* Step 2: decision box on teammate lifeline */}
{step >= 2 && (
<DecisionBox
x={LIFELINE_RIGHT_X + 50}
y={ARROW_Y_START + ARROW_Y_GAP}
/>
)}
{/* Step 3: shutdown_response arrow (Teammate -> Leader) */}
{step >= 3 && (
<SequenceArrow
y={ARROW_Y_START + ARROW_Y_GAP * 2}
direction="left"
label="shutdown_response { approve: true }"
tagLabel={`request_id: ${REQUEST_ID}`}
color="#10b981"
tagBg={palette.bgSubtle}
tagStroke={palette.nodeStroke}
tagText={palette.nodeText}
/>
)}
{/* Step 3: exit annotation */}
{step >= 3 && (
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
>
<line
x1={LIFELINE_RIGHT_X - 10}
y1={ARROW_Y_START + ARROW_Y_GAP * 2 + 20}
x2={LIFELINE_RIGHT_X + 10}
y2={ARROW_Y_START + ARROW_Y_GAP * 2 + 36}
stroke="#ef4444"
strokeWidth={2}
/>
<line
x1={LIFELINE_RIGHT_X + 10}
y1={ARROW_Y_START + ARROW_Y_GAP * 2 + 20}
x2={LIFELINE_RIGHT_X - 10}
y2={ARROW_Y_START + ARROW_Y_GAP * 2 + 36}
stroke="#ef4444"
strokeWidth={2}
/>
<text
x={LIFELINE_RIGHT_X + 24}
y={ARROW_Y_START + ARROW_Y_GAP * 2 + 32}
fontSize={8}
fill="#ef4444"
fontWeight={600}
>
exit
</text>
</motion.g>
)}
</g>
)}
{protocol === "plan" && (
<g key="plan">
{/* Activation bars */}
{step >= 1 && (
<ActivationBar
x={LIFELINE_RIGHT_X}
yStart={ARROW_Y_START - 10}
yEnd={step >= 2 ? ARROW_Y_START + ARROW_Y_GAP * 2 + 15 : ARROW_Y_START + 30}
color="#8b5cf6"
/>
)}
{step >= 1 && (
<ActivationBar
x={LIFELINE_LEFT_X}
yStart={ARROW_Y_START - 5}
yEnd={step >= 2 ? ARROW_Y_START + ARROW_Y_GAP * 2 + 15 : ARROW_Y_START + ARROW_Y_GAP + 10}
color="#3b82f6"
/>
)}
{/* Step 1: plan submission arrow (Teammate -> Leader) */}
{step >= 1 && (
<SequenceArrow
y={ARROW_Y_START}
direction="left"
label="exit_plan_mode { plan }"
tagLabel={`request_id: ${REQUEST_ID}`}
color="#8b5cf6"
tagBg={palette.bgSubtle}
tagStroke={palette.nodeStroke}
tagText={palette.nodeText}
/>
)}
{/* Step 1: plan content box */}
{step >= 1 && (
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
>
<rect
x={20}
y={ARROW_Y_START + 20}
width={95}
height={50}
rx={4}
fill={palette.bgSubtle}
stroke={palette.nodeStroke}
strokeWidth={0.5}
/>
<text x={28} y={ARROW_Y_START + 34} fontSize={6} fontFamily="monospace" fill={palette.nodeText} fontWeight={600}>
Plan:
</text>
<text x={28} y={ARROW_Y_START + 44} fontSize={5.5} fontFamily="monospace" fill={palette.labelFill}>
1. Add error handler
</text>
<text x={28} y={ARROW_Y_START + 54} fontSize={5.5} fontFamily="monospace" fill={palette.labelFill}>
2. Update tests
</text>
<text x={28} y={ARROW_Y_START + 64} fontSize={5.5} fontFamily="monospace" fill={palette.labelFill}>
3. Refactor module
</text>
</motion.g>
)}
{/* Step 2: approval response arrow (Leader -> Teammate) */}
{step >= 2 && (
<SequenceArrow
y={ARROW_Y_START + ARROW_Y_GAP * 2}
direction="right"
label="plan_approval_response { approve: true }"
tagLabel={`request_id: ${REQUEST_ID}`}
color="#10b981"
tagBg={palette.bgSubtle}
tagStroke={palette.nodeStroke}
tagText={palette.nodeText}
/>
)}
{/* Step 2: checkmark */}
{step >= 2 && (
<motion.g
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3 }}
>
<circle cx={LIFELINE_RIGHT_X + 40} cy={ARROW_Y_START + ARROW_Y_GAP * 2} r={10} fill="#10b981" />
<text
x={LIFELINE_RIGHT_X + 40}
y={ARROW_Y_START + ARROW_Y_GAP * 2 + 1}
textAnchor="middle"
dominantBaseline="middle"
fontSize={10}
fill="white"
fontWeight={700}
>
OK
</text>
</motion.g>
)}
</g>
)}
</AnimatePresence>
</svg>
{/* Step controls */}
<div className="mt-4">
<StepControls
currentStep={vis.currentStep}
totalSteps={vis.totalSteps}
onPrev={vis.prev}
onNext={vis.next}
onReset={vis.reset}
isPlaying={vis.isPlaying}
onToggleAutoPlay={vis.toggleAutoPlay}
stepTitle={steps[step].title}
stepDescription={steps[step].desc}
/>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,466 @@
"use client";
import { motion } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
import { useSvgPalette } from "@/hooks/useDarkMode";
// -- FSM states and their layout positions (diamond: idle top, poll right, claim bottom, work left) --
type Phase = "idle" | "poll" | "claim" | "work";
const FSM_CX = 110;
const FSM_CY = 110;
const FSM_R = 65;
const FSM_STATE_R = 22;
const FSM_STATES: { id: Phase; label: string; angle: number }[] = [
{ id: "idle", label: "idle", angle: -Math.PI / 2 },
{ id: "poll", label: "poll", angle: 0 },
{ id: "claim", label: "claim", angle: Math.PI / 2 },
{ id: "work", label: "work", angle: Math.PI },
];
const FSM_TRANSITIONS: { from: Phase; to: Phase }[] = [
{ from: "idle", to: "poll" },
{ from: "poll", to: "claim" },
{ from: "claim", to: "work" },
{ from: "work", to: "idle" },
];
function fsmPos(angle: number) {
return { x: FSM_CX + FSM_R * Math.cos(angle), y: FSM_CY + FSM_R * Math.sin(angle) };
}
const PHASE_COLORS: Record<Phase, string> = {
idle: "#a1a1aa",
poll: "#f59e0b",
claim: "#3b82f6",
work: "#10b981",
};
// -- Task board data --
interface TaskRow {
id: string;
name: string;
status: "unclaimed" | "active" | "complete";
owner: string;
}
const INITIAL_TASKS: TaskRow[] = [
{ id: "T1", name: "Fix auth bug", status: "unclaimed", owner: "-" },
{ id: "T2", name: "Add rate limiter", status: "unclaimed", owner: "-" },
{ id: "T3", name: "Write tests", status: "unclaimed", owner: "-" },
{ id: "T4", name: "Update API docs", status: "unclaimed", owner: "-" },
];
// Agent positions around the task board (left panel)
const BOARD_CX = 140;
const BOARD_CY = 90;
const AGENT_ORBIT = 85;
const AGENT_R = 20;
const AGENT_ANGLES = [-Math.PI / 2, Math.PI / 6, (5 * Math.PI) / 6];
function agentPos(index: number) {
const angle = AGENT_ANGLES[index];
return { x: BOARD_CX + AGENT_ORBIT * Math.cos(angle), y: BOARD_CY + AGENT_ORBIT * Math.sin(angle) };
}
// -- Step definitions --
const STEPS = [
{ title: "Self-Governing Agents", desc: "Autonomous agents need no coordinator. They govern themselves with an idle-poll-claim-work cycle." },
{ title: "Idle Timer", desc: "Each idle agent counts rounds. A timeout triggers self-directed task polling." },
{ title: "Poll Task Board", desc: "Timeout! The agent reads the task board looking for unclaimed work." },
{ title: "Claim Task", desc: "The agent writes its name to the task record. Atomic, no conflicts." },
{ title: "Work", desc: "The agent works on the claimed task using its own agent loop." },
{ title: "Independent Polling", desc: "Multiple agents poll and claim independently. No central coordinator needed." },
{ title: "Complete & Reset", desc: "Task done. Agent returns to idle. The cycle repeats." },
{ title: "Self-Organization", desc: "Three agents, zero coordination overhead. Polling + timeout = emergent organization." },
];
// Per-step state for each agent
interface AgentState {
phase: Phase;
timerFill: number;
color: string;
taskClaim: string | null;
}
function getAgentStates(step: number): AgentState[] {
const idle: AgentState = { phase: "idle", timerFill: 0, color: PHASE_COLORS.idle, taskClaim: null };
switch (step) {
case 0:
return [
{ ...idle },
{ ...idle },
{ ...idle },
];
case 1:
return [
{ phase: "idle", timerFill: 0.6, color: PHASE_COLORS.idle, taskClaim: null },
{ ...idle },
{ ...idle },
];
case 2:
return [
{ phase: "poll", timerFill: 1.0, color: PHASE_COLORS.poll, taskClaim: null },
{ ...idle },
{ ...idle },
];
case 3:
return [
{ phase: "claim", timerFill: 0, color: PHASE_COLORS.claim, taskClaim: "T1" },
{ ...idle },
{ ...idle },
];
case 4:
return [
{ phase: "work", timerFill: 0, color: PHASE_COLORS.work, taskClaim: "T1" },
{ ...idle },
{ ...idle },
];
case 5:
return [
{ phase: "work", timerFill: 0, color: PHASE_COLORS.work, taskClaim: "T1" },
{ phase: "claim", timerFill: 0, color: PHASE_COLORS.claim, taskClaim: "T2" },
{ ...idle },
];
case 6:
return [
{ phase: "idle", timerFill: 0, color: PHASE_COLORS.idle, taskClaim: null },
{ phase: "work", timerFill: 0, color: PHASE_COLORS.work, taskClaim: "T2" },
{ ...idle },
];
case 7:
return [
{ phase: "idle", timerFill: 0, color: PHASE_COLORS.idle, taskClaim: null },
{ phase: "work", timerFill: 0, color: PHASE_COLORS.work, taskClaim: "T2" },
{ phase: "claim", timerFill: 0, color: PHASE_COLORS.claim, taskClaim: "T3" },
];
default:
return [{ ...idle }, { ...idle }, { ...idle }];
}
}
function getTaskStates(step: number): TaskRow[] {
const tasks = INITIAL_TASKS.map((t) => ({ ...t }));
if (step >= 3) { tasks[0].status = "active"; tasks[0].owner = "A"; }
if (step >= 5) { tasks[1].status = "active"; tasks[1].owner = "B"; }
if (step >= 6) { tasks[0].status = "complete"; }
if (step >= 7) { tasks[2].status = "active"; tasks[2].owner = "C"; }
return tasks;
}
function getActivePhase(step: number): Phase {
if (step <= 1) return "idle";
if (step === 2) return "poll";
if (step === 3) return "claim";
if (step === 4 || step === 5) return "work";
if (step === 6) return "idle";
return "claim";
}
// Ring timer around an agent
function TimerRing({ cx, cy, r, fill }: { cx: number; cy: number; r: number; fill: number }) {
if (fill <= 0) return null;
const circumference = 2 * Math.PI * (r + 4);
const offset = circumference * (1 - fill);
return (
<motion.circle
cx={cx}
cy={cy}
r={r + 4}
fill="none"
stroke="#f59e0b"
strokeWidth={3}
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
initial={{ strokeDashoffset: circumference }}
animate={{ strokeDashoffset: offset }}
transition={{ duration: 0.8, ease: "easeOut" }}
style={{ transform: "rotate(-90deg)", transformOrigin: `${cx}px ${cy}px` }}
/>
);
}
// FSM arrow between two states
function FSMArrow({ from, to, active, inactiveStroke }: { from: Phase; to: Phase; active: boolean; inactiveStroke: string }) {
const fState = FSM_STATES.find((s) => s.id === from)!;
const tState = FSM_STATES.find((s) => s.id === to)!;
const fPos = fsmPos(fState.angle);
const tPos = fsmPos(tState.angle);
const dx = tPos.x - fPos.x;
const dy = tPos.y - fPos.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const ux = dx / dist;
const uy = dy / dist;
const x1 = fPos.x + ux * FSM_STATE_R;
const y1 = fPos.y + uy * FSM_STATE_R;
const x2 = tPos.x - ux * (FSM_STATE_R + 6);
const y2 = tPos.y - uy * (FSM_STATE_R + 6);
const perpX = -uy * 12;
const perpY = ux * 12;
const cx = (x1 + x2) / 2 + perpX;
const cy = (y1 + y2) / 2 + perpY;
return (
<g>
<path
d={`M ${x1} ${y1} Q ${cx} ${cy} ${x2} ${y2}`}
fill="none"
stroke={active ? PHASE_COLORS[to] : inactiveStroke}
strokeWidth={active ? 2 : 1}
markerEnd="url(#fsm-arrowhead)"
/>
</g>
);
}
export default function AutonomousAgents({ title }: { title?: string }) {
const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });
const step = vis.currentStep;
const palette = useSvgPalette();
const agentStates = getAgentStates(step);
const tasks = getTaskStates(step);
const activePhase = getActivePhase(step);
const agentNames = ["A", "B", "C"];
return (
<section className="space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "Autonomous Agent Cycle"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900 min-h-[500px]">
<div className="flex flex-col lg:flex-row gap-4">
{/* Left panel: spatial view with agents and task board */}
<div className="flex-1">
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-2">Spatial View</div>
<svg viewBox="0 0 280 240" className="w-full">
{/* Task board (small table in center) */}
<rect x={BOARD_CX - 35} y={BOARD_CY - 20} width={70} height={40} rx={4}
fill={palette.bgSubtle} stroke={palette.nodeStroke} strokeWidth={1}
/>
<text x={BOARD_CX} y={BOARD_CY - 8} textAnchor="middle" fontSize={7} fontWeight={600}
fill={palette.nodeText}
>
Task Board
</text>
<text x={BOARD_CX} y={BOARD_CY + 4} textAnchor="middle" fontSize={6} fontFamily="monospace"
fill={palette.labelFill}
>
{tasks.filter((t) => t.status === "unclaimed").length} unclaimed
</text>
<text x={BOARD_CX} y={BOARD_CY + 14} textAnchor="middle" fontSize={6} fontFamily="monospace"
fill="#10b981"
>
{tasks.filter((t) => t.status === "complete").length} complete
</text>
{/* Agents */}
{agentStates.map((state, i) => {
const pos = agentPos(i);
const isPulsing = state.phase === "work";
const isPolling = state.phase === "poll";
return (
<g key={i}>
{/* Dashed line from agent to board when polling */}
{isPolling && (
<motion.line
x1={pos.x} y1={pos.y} x2={BOARD_CX} y2={BOARD_CY}
stroke="#f59e0b" strokeWidth={1.5} strokeDasharray="4 3"
initial={{ opacity: 0 }} animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
)}
{/* Solid line from agent to board when claiming */}
{state.phase === "claim" && (
<motion.line
x1={pos.x} y1={pos.y} x2={BOARD_CX} y2={BOARD_CY}
stroke="#3b82f6" strokeWidth={2}
initial={{ opacity: 0 }} animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
)}
{/* Timer ring */}
<TimerRing cx={pos.x} cy={pos.y} r={AGENT_R} fill={state.timerFill} />
{/* Agent circle */}
<motion.circle
cx={pos.x} cy={pos.y} r={AGENT_R}
fill={state.color}
stroke={state.phase === "work" ? "#059669" : palette.nodeStroke}
strokeWidth={1.5}
animate={{
scale: isPulsing ? [1, 1.1, 1] : 1,
fill: state.color,
}}
transition={
isPulsing
? { duration: 0.8, repeat: Infinity, ease: "easeInOut" }
: { duration: 0.4 }
}
/>
<text x={pos.x} y={pos.y + 1} textAnchor="middle" dominantBaseline="middle"
fill="white" fontSize={11} fontWeight={700}
>
{agentNames[i]}
</text>
{/* Task label below agent when claiming or working */}
{state.taskClaim && (
<motion.text
x={pos.x} y={pos.y + AGENT_R + 12}
textAnchor="middle" fontSize={7} fontFamily="monospace"
fill={state.phase === "work" ? "#10b981" : "#3b82f6"}
fontWeight={600}
initial={{ opacity: 0 }} animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{state.taskClaim}
</motion.text>
)}
</g>
);
})}
</svg>
{/* Task table below the spatial view */}
<div className="mt-2 border border-zinc-200 rounded dark:border-zinc-700 overflow-hidden">
<table className="w-full text-[10px]">
<thead>
<tr className="bg-zinc-50 dark:bg-zinc-800">
<th className="px-2 py-1 text-left font-medium text-zinc-500 dark:text-zinc-400">Task</th>
<th className="px-2 py-1 text-left font-medium text-zinc-500 dark:text-zinc-400">Status</th>
<th className="px-2 py-1 text-left font-medium text-zinc-500 dark:text-zinc-400">Owner</th>
</tr>
</thead>
<tbody>
{tasks.map((task) => (
<tr key={task.id} className="border-t border-zinc-100 dark:border-zinc-800">
<td className="px-2 py-1 font-mono text-zinc-700 dark:text-zinc-300">{task.name}</td>
<td className="px-2 py-1">
<span className={`inline-block rounded px-1.5 py-0.5 text-[9px] font-medium ${
task.status === "complete"
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300"
: task.status === "active"
? "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300"
: "bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400"
}`}>
{task.status}
</span>
</td>
<td className="px-2 py-1 font-mono text-zinc-600 dark:text-zinc-400">{task.owner}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Right panel: FSM state machine diagram */}
<div className="flex-1">
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-2">FSM Cycle</div>
<svg viewBox="0 0 220 220" className="w-full">
<defs>
<marker
id="fsm-arrowhead"
viewBox="0 0 10 10"
refX="8"
refY="5"
markerWidth="5"
markerHeight="5"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill={palette.arrowFill} />
</marker>
</defs>
{/* Transition arrows */}
{FSM_TRANSITIONS.map((t) => {
const isActive =
(activePhase === t.from) ||
(activePhase === t.to && t.from === FSM_TRANSITIONS.find((tr) => tr.to === activePhase)?.from);
return (
<FSMArrow
key={`${t.from}-${t.to}`}
from={t.from}
to={t.to}
active={isActive}
inactiveStroke={palette.nodeStroke}
/>
);
})}
{/* State circles */}
{FSM_STATES.map((state) => {
const pos = fsmPos(state.angle);
const isActive = state.id === activePhase;
return (
<g key={state.id}>
<motion.circle
cx={pos.x}
cy={pos.y}
r={FSM_STATE_R}
fill={isActive ? PHASE_COLORS[state.id] : palette.nodeFill}
stroke={isActive ? PHASE_COLORS[state.id] : palette.nodeStroke}
strokeWidth={isActive ? 2 : 1}
animate={{
fill: isActive ? PHASE_COLORS[state.id] : palette.nodeFill,
scale: isActive ? 1.1 : 1,
}}
transition={{ duration: 0.4 }}
/>
<text
x={pos.x}
y={pos.y + 1}
textAnchor="middle"
dominantBaseline="middle"
fontSize={9}
fontWeight={600}
fill={isActive ? "white" : palette.nodeText}
>
{state.label}
</text>
</g>
);
})}
</svg>
{/* Legend */}
<div className="mt-2 flex flex-wrap gap-3 justify-center">
{FSM_STATES.map((s) => (
<div key={s.id} className="flex items-center gap-1">
<span className="inline-block h-2.5 w-2.5 rounded-full" style={{ backgroundColor: PHASE_COLORS[s.id] }} />
<span className="text-[10px] font-mono text-zinc-500 dark:text-zinc-400">{s.label}</span>
</div>
))}
</div>
</div>
</div>
{/* Step controls */}
<div className="mt-4">
<StepControls
currentStep={vis.currentStep}
totalSteps={vis.totalSteps}
onPrev={vis.prev}
onNext={vis.next}
onReset={vis.reset}
isPlaying={vis.isPlaying}
onToggleAutoPlay={vis.toggleAutoPlay}
stepTitle={STEPS[step].title}
stepDescription={STEPS[step].desc}
/>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,102 @@
"use client";
import { Play, Pause, SkipBack, SkipForward, RotateCcw } from "lucide-react";
import { cn } from "@/lib/utils";
interface StepControlsProps {
currentStep: number;
totalSteps: number;
onPrev: () => void;
onNext: () => void;
onReset: () => void;
isPlaying: boolean;
onToggleAutoPlay: () => void;
stepTitle: string;
stepDescription: string;
className?: string;
}
export function StepControls({
currentStep,
totalSteps,
onPrev,
onNext,
onReset,
isPlaying,
onToggleAutoPlay,
stepTitle,
stepDescription,
className,
}: StepControlsProps) {
return (
<div className={cn("space-y-3", className)}>
{/* Annotation */}
<div className="rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 dark:border-blue-800 dark:bg-blue-950/40">
<div className="mb-1 text-sm font-semibold text-blue-900 dark:text-blue-200">
{stepTitle}
</div>
<div className="text-sm text-blue-700 dark:text-blue-300">
{stepDescription}
</div>
</div>
{/* Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<button
onClick={onReset}
className="rounded-md p-1.5 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
title="Reset"
>
<RotateCcw size={16} />
</button>
<button
onClick={onPrev}
disabled={currentStep === 0}
className="rounded-md p-1.5 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 disabled:opacity-30 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
title="Previous step"
>
<SkipBack size={16} />
</button>
<button
onClick={onToggleAutoPlay}
className="rounded-md p-1.5 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
title={isPlaying ? "Pause" : "Auto-play"}
>
{isPlaying ? <Pause size={16} /> : <Play size={16} />}
</button>
<button
onClick={onNext}
disabled={currentStep === totalSteps - 1}
className="rounded-md p-1.5 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 disabled:opacity-30 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
title="Next step"
>
<SkipForward size={16} />
</button>
</div>
{/* Step indicator */}
<div className="flex items-center gap-2">
<div className="flex gap-1">
{Array.from({ length: totalSteps }, (_, i) => (
<div
key={i}
className={cn(
"h-1.5 w-1.5 rounded-full transition-colors",
i === currentStep
? "bg-blue-500"
: i < currentStep
? "bg-blue-300 dark:bg-blue-700"
: "bg-zinc-200 dark:bg-zinc-700"
)}
/>
))}
</div>
<span className="font-mono text-xs text-zinc-400">
{currentStep + 1}/{totalSteps}
</span>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,47 @@
{
"version": "s01",
"decisions": [
{
"id": "one-tool-sufficiency",
"title": "Why Bash Alone Is Enough",
"description": "Bash can read files, write files, run arbitrary programs, pipe data between processes, and manage the filesystem. Any additional tool (read_file, write_file, etc.) would be a strict subset of what bash already provides. Adding more tools doesn't unlock new capabilities -- it just adds surface area for confusion. The model has to learn fewer tool schemas, and the implementation stays under 100 lines. This is the minimal viable agent: one tool, one loop.",
"alternatives": "We could have started with a richer toolset (file I/O, HTTP, database), but that would obscure the core insight: an LLM with a shell is already a general-purpose agent. Starting minimal also makes it obvious what each subsequent version actually adds.",
"zh": {
"title": "为什么仅靠 Bash 就够了",
"description": "Bash 能读写文件、运行任意程序、在进程间传递数据、管理文件系统。任何额外的工具read_file、write_file 等)都只是 bash 已有能力的子集。增加工具并不会解锁新能力,只会增加模型需要理解的接口。模型只需学习一个工具的 schema实现代码不超过 100 行。这就是最小可行 agent一个工具一个循环。"
},
"ja": {
"title": "Bash だけで十分な理由",
"description": "Bash はファイルの読み書き、任意のプログラムの実行、プロセス間のデータパイプ、ファイルシステムの管理が可能です。追加のツールread_file、write_file など)は bash が既に提供している機能の部分集合に過ぎません。ツールを増やしても新しい能力は得られず、モデルが理解すべきインターフェースが増えるだけです。モデルが学習するスキーマは1つだけで、実装は100行以内に収まります。これが最小限の実用的エージェント1つのツール、1つのループです。"
}
},
{
"id": "process-as-subagent",
"title": "Recursive Process Spawning as Subagent Mechanism",
"description": "When the agent runs `python v0.py \"subtask\"`, it spawns a completely new process with a fresh LLM context. This child process is effectively a subagent: it has its own system prompt, its own conversation history, and its own task focus. When it finishes, the parent gets the stdout result. This is subagent delegation without any framework -- just Unix process semantics. Each child process naturally isolates concerns because it literally cannot see the parent's context.",
"alternatives": "A framework-level subagent system (like v3's Task tool) gives more control over what tools the subagent can access and how results are returned. But at v0, the point is to show that process spawning is the most primitive form of agent delegation -- no shared memory, no message passing, just stdin/stdout.",
"zh": {
"title": "用递归进程创建实现子代理机制",
"description": "当 agent 执行 `python v0.py \"subtask\"` 时,它会创建一个全新的进程,拥有全新的 LLM 上下文。这个子进程实际上就是一个子代理:有自己的系统提示词、对话历史和任务焦点。子进程完成后,父进程通过 stdout 获取结果。这就是不依赖任何框架的子代理委派——纯粹的 Unix 进程语义。每个子进程天然隔离关注点,因为它根本看不到父进程的上下文。"
},
"ja": {
"title": "再帰プロセス生成によるサブエージェント機構",
"description": "エージェントが `python v0.py \"subtask\"` を実行すると、新しい LLM コンテキストを持つ完全に新しいプロセスが生成されます。この子プロセスは事実上サブエージェントです:独自のシステムプロンプト、会話履歴、タスクフォーカスを持ちます。完了すると、親プロセスは stdout で結果を受け取ります。これはフレームワークなしのサブエージェント委任です——共有メモリもメッセージパッシングもなく、stdin/stdout だけです。各子プロセスは親のコンテキストを参照できないため、関心の分離が自然に実現されます。"
}
},
{
"id": "model-drives-everything",
"title": "No Planning Framework -- The Model Decides",
"description": "There is no planner, no task queue, no state machine. The system prompt tells the model how to approach problems, and the model decides what bash command to run next based on the conversation so far. This is intentional: at this level, adding a planning layer would be premature abstraction. The model's chain-of-thought IS the plan. The agent loop just keeps asking the model what to do until it stops requesting tools.",
"alternatives": "Later versions (v2) add explicit planning via TodoWrite. But v0 proves that implicit planning through the model's reasoning is sufficient for many tasks. The planning framework only becomes necessary when you need external visibility into the agent's intentions.",
"zh": {
"title": "没有规划框架——由模型自行决策",
"description": "没有规划器,没有任务队列,没有状态机。系统提示词告诉模型如何处理问题,模型根据对话历史决定下一步执行什么 bash 命令。这是有意为之的在这个层级添加规划层属于过早抽象。模型的思维链本身就是计划。agent 循环只是不断询问模型下一步做什么,直到模型不再请求工具为止。"
},
"ja": {
"title": "計画フレームワークなし——モデルが全てを決定",
"description": "プランナーもタスクキューも状態マシンもありません。システムプロンプトがモデルに問題の取り組み方を伝え、モデルがこれまでの会話に基づいて次に実行する bash コマンドを決定します。これは意図的な設計です:このレベルでは計画レイヤーの追加は時期尚早な抽象化です。モデルの思考の連鎖そのものが計画です。エージェントループはモデルがツールの呼び出しを止めるまで、次の行動を問い続けるだけです。"
}
}
]
}

View File

@@ -0,0 +1,47 @@
{
"version": "s02",
"decisions": [
{
"id": "four-tools-not-twenty",
"title": "Why Exactly Four Tools",
"description": "The four tools are bash, read_file, write_file, and edit_file. Together they cover roughly 95% of coding tasks. Bash handles execution and arbitrary commands. Read_file provides precise file reading with line numbers. Write_file creates or overwrites files. Edit_file does surgical string replacement. More tools would increase the model's cognitive load -- it has to decide which tool to use, and more options means more chances of picking the wrong one. Fewer tools also means fewer tool schemas to maintain and fewer edge cases to handle.",
"alternatives": "We could add specialized tools (list_directory, search_files, http_request), and later versions do. But at this stage, bash already covers those use cases. The split from v0's single tool to v1's four tools is specifically about giving the model structured I/O for file operations, where bash's quoting and escaping often trips up the model.",
"zh": {
"title": "为什么恰好四个工具",
"description": "四个工具分别是 bash、read_file、write_file 和 edit_file覆盖了大约 95% 的编程任务。Bash 处理执行和任意命令read_file 提供带行号的精确文件读取write_file 创建或覆盖文件edit_file 做精确的字符串替换。工具越多,模型的认知负担越重——它必须在更多选项中做选择,选错的概率也随之增加。更少的工具也意味着更少的 schema 需要维护、更少的边界情况需要处理。"
},
"ja": {
"title": "なぜ正確に4つのツールなのか",
"description": "4つのツールは bash、read_file、write_file、edit_file です。これらでコーディングタスクの約95%をカバーします。Bash は実行と任意のコマンドを処理し、read_file は行番号付きの正確なファイル読み取りを提供し、write_file はファイルの作成・上書きを行い、edit_file は外科的な文字列置換を行います。ツールが増えるとモデルの認知負荷が増大し、どのツールを使うかの判断でミスが増えます。ツールが少ないことは、メンテナンスすべきスキーマとエッジケースの削減も意味します。"
}
},
{
"id": "model-as-agent",
"title": "The Model IS the Agent",
"description": "The core agent loop is trivially simple: while True, call the LLM, if it returns tool_use blocks then execute them and feed results back, if it returns only text then stop. There is no router, no decision tree, no workflow engine. The model itself decides what to do, when to stop, and how to recover from errors. The code is just plumbing that connects the model to tools. This is a philosophical stance: agent behavior emerges from the model, not from the framework.",
"alternatives": "Many agent frameworks add elaborate orchestration layers: ReAct loops with explicit Thought/Action/Observation parsing, LangChain-style chains, AutoGPT-style goal decomposition. These frameworks assume the model needs scaffolding to behave as an agent. Our approach assumes the model already knows how to be an agent -- it just needs tools to act on the world.",
"zh": {
"title": "模型本身就是代理",
"description": "核心 agent 循环极其简单:不断调用 LLM如果返回 tool_use 块就执行并回传结果如果只返回文本就停止。没有路由器没有决策树没有工作流引擎。模型自己决定做什么、何时停止、如何从错误中恢复。代码只是连接模型和工具的管道。这是一种设计哲学agent 行为从模型中涌现,而非由框架定义。"
},
"ja": {
"title": "モデルそのものがエージェント",
"description": "コアのエージェントループは極めてシンプルですLLM を呼び出し続け、tool_use ブロックが返されればそれを実行して結果をフィードバックし、テキストのみが返されれば停止します。ルーターも決定木もワークフローエンジンもありません。モデル自体が何をすべきか、いつ停止するか、エラーからどう回復するかを決定します。コードはモデルとツールを接続する配管に過ぎません。これは設計思想です:エージェントの振る舞いはフレームワークではなくモデルから創発するものです。"
}
},
{
"id": "explicit-tool-schemas",
"title": "JSON Schemas for Every Tool",
"description": "Each tool defines a strict JSON schema for its input parameters. For example, edit_file requires old_string and new_string as exact strings, not regex patterns. This eliminates an entire class of bugs: the model can't pass malformed input because the API validates against the schema before execution. It also makes the model's intent unambiguous -- when it calls edit_file with specific strings, there's no parsing ambiguity about what it wants to change.",
"alternatives": "Some agent systems let the model output free-form text that gets parsed with regex or heuristics (e.g., extracting code from markdown blocks). This is fragile -- the model might format output slightly differently and break the parser. JSON schemas trade flexibility for reliability.",
"zh": {
"title": "每个工具都有 JSON Schema",
"description": "每个工具都为输入参数定义了严格的 JSON schema。例如edit_file 要求 old_string 和 new_string 是精确的字符串,而非正则表达式。这消除了一整类错误:模型无法传递格式错误的输入,因为 API 会在执行前校验 schema。这也使模型的意图变得明确——当它用特定字符串调用 edit_file 时,不存在关于它想修改什么的解析歧义。"
},
"ja": {
"title": "全ツールに JSON Schema を定義",
"description": "各ツールは入力パラメータに対して厳密な JSON Schema を定義しています。例えば edit_file は old_string と new_string を正確な文字列として要求し、正規表現は使いません。これにより一連のバグを排除できますAPI がスキーマに対して実行前にバリデーションを行うため、モデルは不正な入力を渡せません。モデルの意図も明確になります――特定の文字列で edit_file を呼び出す際、何を変更したいかについて解析の曖昧さがありません。"
}
}
]
}

View File

@@ -0,0 +1,47 @@
{
"version": "s03",
"decisions": [
{
"id": "visible-planning",
"title": "Making Plans Visible via TodoWrite",
"description": "Instead of letting the model plan silently in its chain-of-thought, we force plans to be externalized through the TodoWrite tool. Each plan item has a status (pending, in_progress, completed) that gets tracked explicitly. This has three benefits: (1) users can see what the agent intends to do before it does it, (2) developers can debug agent behavior by inspecting the plan state, (3) the agent itself can refer back to its plan in later turns when earlier context has scrolled away.",
"alternatives": "The model could plan internally via chain-of-thought reasoning (as it does in v0/v1). Internal planning works but is invisible and ephemeral -- once the thinking scrolls out of context, the plan is lost. Claude's extended thinking is another option, but it's not inspectable by the user or by downstream tools.",
"zh": {
"title": "通过 TodoWrite 让计划可见",
"description": "我们不让模型在思维链中默默规划,而是强制通过 TodoWrite 工具将计划外化。每个计划项都有可追踪的状态pending、in_progress、completed。这有三个好处(1) 用户可以在执行前看到 agent 打算做什么;(2) 开发者可以通过检查计划状态来调试 agent 行为;(3) agent 自身可以在后续轮次中引用计划,即使早期上下文已经滚出窗口。"
},
"ja": {
"title": "TodoWrite による計画の可視化",
"description": "モデルが思考の連鎖の中で黙って計画するのではなく、TodoWrite ツールを通じて計画を外部化することを強制します。各計画項目には追跡可能なステータスpending、in_progress、completedがあります。利点は3つ(1) ユーザーがエージェントの意図を実行前に確認できる、(2) 開発者が計画状態を検査してデバッグできる、(3) エージェント自身が以前のコンテキストがスクロールアウトした後でも計画を参照できる。"
}
},
{
"id": "single-in-progress",
"title": "Only One Task Can Be In-Progress",
"description": "The TodoWrite tool enforces that at most one task has status 'in_progress' at any time. If the model tries to start a second task, it must first complete or abandon the current one. This constraint prevents a subtle failure mode: models that try to 'multitask' by interleaving work on multiple items tend to lose track of state and produce half-finished results. Sequential focus produces higher quality than parallel thrashing.",
"alternatives": "Allowing multiple in-progress items would let the agent context-switch between tasks, which seems more flexible. In practice, LLMs handle context-switching poorly -- they lose track of which task they were working on and mix up details between tasks. The single-focus constraint is a guardrail that improves output quality.",
"zh": {
"title": "同一时间只允许一个任务进行中",
"description": "TodoWrite 工具强制要求任何时候最多只能有一个任务处于 in_progress 状态。如果模型想开始第二个任务,必须先完成或放弃当前任务。这个约束防止了一种隐蔽的失败模式:试图通过交替处理多个项目来'多任务'的模型,往往会丢失状态并产出半成品。顺序执行的专注度远高于并行切换。"
},
"ja": {
"title": "同時に進行中にできるタスクは1つだけ",
"description": "TodoWrite ツールは、同時に 'in_progress' 状態のタスクを最大1つに制限します。モデルが2つ目のタスクを開始しようとする場合、まず現在のタスクを完了または中断する必要があります。この制約は微妙な失敗モードを防ぎます複数の項目を交互に処理して「マルチタスク」しようとするモデルは、状態を見失い中途半端な結果を生みがちです。逐次的な集中は並行的な切り替えよりも高品質な出力を生み出します。"
}
},
{
"id": "max-twenty-items",
"title": "Maximum of 20 Plan Items",
"description": "TodoWrite caps the plan at 20 items. This is a deliberate constraint against over-planning. Models tend to decompose tasks into increasingly fine-grained steps when unconstrained, producing 50-item plans where each step is trivial. Long plans are fragile: if step 15 fails, the remaining 35 steps may all be invalid. Short plans (under 20 items) stay at the right abstraction level and are easier to adapt when reality diverges from the plan.",
"alternatives": "No cap would give the model full flexibility, but in practice leads to absurdly detailed plans. A dynamic cap (proportional to task complexity) would be smarter but adds complexity. The fixed cap of 20 is a simple heuristic that works well empirically -- most real coding tasks can be expressed in 5-15 meaningful steps.",
"zh": {
"title": "计划项上限为 20 条",
"description": "TodoWrite 将计划项限制在 20 条以内。这是对过度规划的刻意约束。不加限制时,模型倾向于将任务分解成越来越细粒度的步骤,产出 50 条的计划,每一步都微不足道。冗长的计划很脆弱:如果第 15 步失败,剩下的 35 步可能全部作废。20 条以内的短计划保持在正确的抽象层级,更容易在现实偏离计划时做出调整。"
},
"ja": {
"title": "計画項目の上限は20個",
"description": "TodoWrite は計画を20項目に制限します。これは過度な計画に対する意図的な制約です。制約がないとモデルはタスクをどんどん細かいステップに分解し、各ステップが些末な50項目の計画を作りがちです。長い計画は脆弱ですステップ15が失敗すると残りの35ステップは全て無効になりかねません。20項目以内の短い計画は適切な抽象度を保ち、現実が計画から逸脱した際の適応が容易です。"
}
}
]
}

View File

@@ -0,0 +1,47 @@
{
"version": "s04",
"decisions": [
{
"id": "context-isolation",
"title": "Subagents Get Fresh Context, Not Shared History",
"description": "When a parent agent spawns a subagent via the Task tool, the subagent starts with a clean message history containing only the system prompt and the delegated task description. It does NOT inherit the parent's conversation. This is context isolation: the subagent can focus entirely on its specific subtask without being distracted by hundreds of messages from the parent's broader conversation. The result is returned to the parent as a single tool_result, collapsing potentially dozens of subagent turns into one concise answer.",
"alternatives": "Sharing the parent's full context would give the subagent more information, but it would also flood the subagent with irrelevant details. Context window is finite -- filling it with parent history leaves less room for the subagent's own work. Fork-based approaches (copy the parent context) are a middle ground but still waste tokens on irrelevant history.",
"zh": {
"title": "子代理获得全新上下文,而非共享历史",
"description": "当父代理通过 Task 工具创建子代理时,子代理从全新的消息历史开始,只包含系统提示词和委派的任务描述,不继承父代理的对话。这就是上下文隔离:子代理可以完全专注于特定子任务,不会被父代理长达数百条消息的对话干扰。结果作为单条 tool_result 返回给父代理,将子代理可能数十轮的交互压缩为一个简洁的回答。"
},
"ja": {
"title": "サブエージェントは共有履歴ではなく新しいコンテキストを取得",
"description": "親エージェントが Task ツールでサブエージェントを生成すると、サブエージェントはシステムプロンプトと委任されたタスク説明のみを含むクリーンなメッセージ履歴から開始します。親の会話は引き継ぎません。これがコンテキスト分離です:サブエージェントは親の広範な会話の何百ものメッセージに気を取られることなく、特定のサブタスクに完全に集中できます。結果は単一の tool_result として親に返され、サブエージェントの数十ターンが1つの簡潔な回答に凝縮されます。"
}
},
{
"id": "tool-filtering",
"title": "Explore Agents Cannot Write Files",
"description": "When spawning a subagent with the 'Explore' type, it receives only read-only tools: bash (with restrictions), read_file, and search tools. It cannot call write_file or edit_file. This implements the principle of least privilege: an agent tasked with 'find all usages of function X' doesn't need write access. Removing write tools eliminates the risk of accidental file modification during exploration, and it also narrows the tool space so the model makes better decisions with fewer options.",
"alternatives": "Giving all subagents full tool access is simpler to implement but violates least privilege. A permission-request system (subagent asks parent for write access) adds complexity and latency. Static tool filtering by role is the pragmatic middle ground -- simple to implement, effective at preventing accidents.",
"zh": {
"title": "Explore 代理不能写入文件",
"description": "创建 Explore 类型的子代理时它只获得只读工具bash有限制、read_file 和搜索工具,不能调用 write_file 或 edit_file。这实现了最小权限原则一个被委派'查找函数 X 所有使用位置'的代理不需要写权限。移除写工具消除了探索过程中误修改文件的风险,同时缩小了工具空间,让模型在更少的选项中做出更好的决策。"
},
"ja": {
"title": "Explore エージェントはファイルを書き込めない",
"description": "Explore タイプのサブエージェントを生成すると、読み取り専用ツールのみが提供されますbash制限付き、read_file、検索ツール。write_file や edit_file は使えません。これは最小権限の原則の実装です:「関数 X の全使用箇所を見つける」タスクに書き込み権限は不要です。書き込みツールを除外することで探索中の誤ったファイル変更リスクを排除し、ツール空間を狭めてモデルがより良い判断を下せるようにします。"
}
},
{
"id": "no-recursive-task",
"title": "Subagents Cannot Spawn Their Own Subagents",
"description": "The Task tool is not included in the subagent's tool set. A subagent must complete its work directly -- it cannot delegate further. This prevents infinite delegation loops: without this constraint, an agent could spawn a subagent that spawns another subagent, each one re-delegating the same task in slightly different words, consuming tokens without making progress. One level of delegation handles the vast majority of use cases. If a task is too complex for a single subagent, the parent should decompose it differently.",
"alternatives": "Allowing recursive delegation (bounded by depth) would handle deeply nested tasks but adds complexity and the risk of runaway token consumption. In practice, single-level delegation covers most real-world coding tasks. Multi-level delegation is addressed in later versions (v6+) through persistent team structures instead of recursive spawning.",
"zh": {
"title": "子代理不能再创建子代理",
"description": "Task 工具不包含在子代理的工具集中。子代理必须直接完成工作,不能继续委派。这防止了无限委派循环:没有这个约束,一个代理可能创建子代理,子代理又创建子代理,每一层都用略微不同的措辞重新委派同一任务,消耗 token 却毫无进展。一层委派足以处理绝大多数场景。如果任务对单个子代理来说太复杂,应该由父代理重新分解。"
},
"ja": {
"title": "サブエージェントは自身のサブエージェントを生成できない",
"description": "Task ツールはサブエージェントのツールセットに含まれません。サブエージェントは作業を直接完了しなければならず、さらなる委任はできません。これにより無限委任ループを防止します:この制約がなければ、エージェントがサブエージェントを生成し、そのサブエージェントがさらにサブエージェントを生成し、それぞれが微妙に異なる言葉で同じタスクを再委任してトークンを消費するだけで進捗しない可能性があります。一段階の委任で大多数のユースケースに対応できます。"
}
}
]
}

View File

@@ -0,0 +1,47 @@
{
"version": "s05",
"decisions": [
{
"id": "tool-result-injection",
"title": "Skills Inject via tool_result, Not System Prompt",
"description": "When the agent invokes the Skill tool, the skill's content (a SKILL.md file) is returned as a tool_result in a user message, not injected into the system prompt. This is a deliberate caching optimization: the system prompt remains static across turns, which means API providers can cache it (Anthropic's prompt caching, OpenAI's system message caching). If skill content were in the system prompt, it would change every time a new skill is loaded, invalidating the cache. By putting dynamic content in tool_result, we keep the expensive system prompt cacheable while still getting skill knowledge into context.",
"alternatives": "Injecting skills into the system prompt is simpler and gives skills higher priority in the model's attention. But it breaks prompt caching (every skill load creates a new system prompt variant) and bloats the system prompt over time as skills accumulate. The tool_result approach keeps things cache-friendly at the cost of slightly lower attention priority.",
"zh": {
"title": "技能通过 tool_result 注入,而非系统提示词",
"description": "当 agent 调用 Skill 工具时技能内容SKILL.md 文件)作为 tool_result 在用户消息中返回而非注入系统提示词。这是一个刻意的缓存优化系统提示词在各轮次间保持静态API 提供商可以缓存它Anthropic 的 prompt caching、OpenAI 的 system message caching。如果技能内容在系统提示词中每次加载新技能都会使缓存失效。将动态内容放在 tool_result 中,既保持了昂贵的系统提示词可缓存,又让技能知识进入了上下文。"
},
"ja": {
"title": "スキルはシステムプロンプトではなく tool_result で注入",
"description": "エージェントが Skill ツールを呼び出すと、スキルの内容SKILL.md ファイル)はシステムプロンプトへの注入ではなく、ユーザーメッセージ内の tool_result として返されます。これは意図的なキャッシュ最適化ですシステムプロンプトはターン間で静的に保たれるため、API プロバイダーがキャッシュできますAnthropic のプロンプトキャッシュ、OpenAI のシステムメッセージキャッシュ)。スキル内容がシステムプロンプト内にあると、新しいスキルをロードするたびにキャッシュが無効化されます。動的コンテンツを tool_result に配置することで、高コストなシステムプロンプトのキャッシュ可能性を維持しつつ、スキル知識をコンテキストに取り込めます。"
}
},
{
"id": "lazy-loading",
"title": "On-Demand Skill Loading Instead of Upfront",
"description": "Skills are not loaded at startup. The agent starts with only the skill names and descriptions (from frontmatter). When the agent decides it needs a specific skill, it calls the Skill tool, which loads the full SKILL.md body into context. This keeps the initial prompt small and focused. An agent solving a Python bug doesn't need the Kubernetes deployment skill loaded -- that would waste context window space and potentially confuse the model with irrelevant instructions.",
"alternatives": "Loading all skills upfront guarantees the model always has all knowledge available, but wastes tokens on irrelevant skills and may hit context limits. A recommendation system (model suggests skills, human approves) adds latency. Lazy loading lets the model self-serve the knowledge it needs, when it needs it.",
"zh": {
"title": "按需加载技能而非预加载",
"description": "技能不会在启动时加载。Agent 初始只拥有技能名称和描述(来自 frontmatter。当 agent 判断需要特定技能时,调用 Skill 工具将完整的 SKILL.md 内容加载到上下文中。这保持了初始提示词的精简。一个正在修复 Python bug 的 agent 不需要加载 Kubernetes 部署技能——那会浪费上下文窗口空间,还可能用无关指令干扰模型。"
},
"ja": {
"title": "起動時ではなくオンデマンドでスキルを読み込み",
"description": "スキルは起動時に読み込まれません。エージェントは最初、スキルの名前と説明フロントマターからのみを持ちます。エージェントが特定のスキルが必要だと判断すると、Skill ツールを呼び出して完全な SKILL.md の内容をコンテキストに読み込みます。これにより初期プロンプトを小さく保ちます。Python のバグを修正しているエージェントに Kubernetes デプロイのスキルは不要です――コンテキストウィンドウの無駄遣いであり、無関係な指示でモデルを混乱させかねません。"
}
},
{
"id": "frontmatter-body-split",
"title": "YAML Frontmatter + Markdown Body in SKILL.md",
"description": "Each SKILL.md file has two parts: YAML frontmatter (name, description, globs) and a markdown body (the actual instructions). The frontmatter serves as metadata for the skill registry -- it's what gets listed when the agent asks 'what skills are available?' The body is the payload that gets loaded on demand. This separation means you can list 100 skills (reading only frontmatter, a few bytes each) without loading 100 full instruction sets (potentially thousands of tokens each).",
"alternatives": "A separate metadata file (skill.yaml + skill.md) would work but doubles the number of files. Embedding metadata in the markdown (as headings or comments) requires parsing the full file to extract metadata. Frontmatter is a well-established convention (Jekyll, Hugo, Astro) that keeps metadata and content co-located but separately parseable.",
"zh": {
"title": "SKILL.md 采用 YAML Frontmatter + Markdown 正文",
"description": "每个 SKILL.md 文件有两部分YAML frontmatter名称、描述、globs和 markdown 正文实际指令。Frontmatter 作为技能注册表的元数据——当 agent 问'有哪些可用技能'时,展示的就是这些信息。正文是按需加载的有效负载。这种分离意味着可以列出 100 个技能(每个只读几字节的 frontmatter而不必加载 100 套完整指令集(每套可能数千 token。"
},
"ja": {
"title": "SKILL.md で YAML フロントマター + Markdown 本文",
"description": "各 SKILL.md ファイルは2つの部分で構成されますYAML フロントマター名前、説明、globsと Markdown 本文実際の指示。フロントマターはスキルレジストリのメタデータとして機能し、エージェントが「どんなスキルが利用可能か」と問い合わせた際に一覧表示されます。本文はオンデマンドで読み込まれるペイロードです。この分離により、100個のスキル一覧表示各数バイトのフロントマターのみ読み取りが100個の完全な指示セット各数千トークンのロードなしに可能になります。"
}
}
]
}

View File

@@ -0,0 +1,61 @@
{
"version": "s06",
"decisions": [
{
"id": "three-layer-compression",
"title": "Three-Layer Compression Strategy",
"description": "Context management uses three distinct layers, each with different cost/benefit profiles. (1) Microcompact runs every turn and is nearly free: it truncates tool_result blocks from older messages, stripping verbose command output that's no longer needed. (2) Auto_compact triggers when token count exceeds a threshold: it calls the LLM to generate a conversation summary, which is expensive but dramatically reduces context size. (3) Manual compact is user-triggered for explicit 'start fresh' moments. Layering these means the cheap operation runs constantly (keeping context tidy) while the expensive operation runs rarely (only when actually needed).",
"alternatives": "A single compression strategy (e.g., always summarize at 80% capacity) would be simpler but wasteful -- most of the time, microcompact alone keeps things manageable. A sliding window (drop oldest N messages) is cheap but loses important context. The three-layer approach gives the best token efficiency: cheap cleanup constantly, expensive summarization rarely.",
"zh": {
"title": "三层压缩策略",
"description": "上下文管理使用三个独立的层次,各有不同的成本收益比。(1) 微压缩每轮都运行,几乎零成本:它截断旧消息中的 tool_result 块,去除不再需要的冗长命令输出。(2) 自动压缩在 token 数超过阈值时触发:调用 LLM 生成对话摘要,代价高但能大幅缩减上下文。(3) 手动压缩由用户触发,用于明确的'重新开始'场景。分层意味着低成本操作持续运行(保持上下文整洁),而高成本操作很少触发(仅在真正需要时)。"
},
"ja": {
"title": "3層圧縮戦略",
"description": "コンテキスト管理は、異なるコスト・効果プロファイルを持つ3つの層を使用します。(1) マイクロコンパクトは毎ターン実行されほぼ無コスト:古いメッセージの tool_result ブロックを切り詰め、不要な冗長出力を除去します。(2) 自動コンパクトはトークン数が閾値を超えると発動LLM を呼び出して会話の要約を生成し、コストは高いがコンテキストサイズを劇的に削減します。(3) 手動コンパクトはユーザーが明示的に「最初からやり直し」する時に使用します。この階層化により、安価な操作が常に実行され(コンテキストを整頓)、高価な操作はめったに実行されません(本当に必要な時のみ)。"
}
},
{
"id": "min-savings-threshold",
"title": "MIN_SAVINGS = 20,000 Tokens Before Compressing",
"description": "Auto_compact only triggers when the estimated savings (current tokens minus estimated summary size) exceed 20,000 tokens. Compression is not free: the summary itself consumes tokens, plus there's the API call cost to generate it. If the conversation is only 25,000 tokens, compressing might save 5,000 tokens but cost an API call and produce a summary that's less coherent than the original. The 20K threshold ensures compression only happens when the savings meaningfully exceed the overhead.",
"alternatives": "A percentage-based threshold (compress when context is 80% full) adapts to different context window sizes but doesn't account for the fixed cost of generating a summary. A fixed threshold of 10K would compress more aggressively but often isn't worth it. The 20K value was chosen empirically: it's the point where compression savings consistently outweigh the quality loss from summarization.",
"zh": {
"title": "最小节省量 = 20,000 Token 才触发压缩",
"description": "自动压缩仅在估算节省量(当前 token 数减去预估摘要大小)超过 20,000 token 时才触发。压缩不是免费的:摘要本身会消耗 token还有生成摘要的 API 调用成本。如果对话只有 25,000 token压缩可能节省 5,000 token但需要一次 API 调用且产出的摘要可能不如原文连贯。20K 的阈值确保只在节省量明显超过开销时才进行压缩。"
},
"ja": {
"title": "圧縮前に MIN_SAVINGS = 20,000 トークンが必要",
"description": "自動コンパクトは推定節約量現在のトークン数マイナス推定要約サイズが20,000トークンを超えた場合にのみ発動します。圧縮は無料ではありません要約自体がトークンを消費し、さらに生成のための API コール費用がかかります。会話が25,000トークンしかない場合、圧縮で5,000トークン節約できても、API コールが必要で元の会話より一貫性の低い要約になる可能性があります。20K の閾値は、節約量がオーバーヘッドを確実に上回る場合にのみ圧縮を実行することを保証します。"
}
},
{
"id": "summary-replaces-all",
"title": "Summary Replaces ALL Messages, Not Partial History",
"description": "When auto_compact fires, it generates a summary and replaces the ENTIRE message history with that summary. It does not keep the last N messages alongside the summary. This avoids a subtle coherence problem: if you keep recent messages plus a summary of older ones, the model sees two representations of overlapping content. The summary might say 'we decided to use approach X' while a recent message still shows the deliberation process, creating contradictory signals. A clean summary is a single coherent narrative.",
"alternatives": "Keeping the last 5-10 messages alongside the summary preserves recent detail and gives the model more to work with. But it creates the overlap problem described above, and makes the total context size less predictable. Some systems use a 'sliding window + summary' approach which works but requires careful tuning of the overlap region.",
"zh": {
"title": "摘要替换全部消息,而非保留部分历史",
"description": "自动压缩触发时,生成摘要并替换全部消息历史,不会在摘要旁保留最近的 N 条消息。这避免了一个微妙的连贯性问题:如果同时保留近期消息和旧消息的摘要,模型会看到重叠内容的两种表示。摘要可能说'我们决定使用方案 X',而近期消息仍在展示讨论过程,产生矛盾信号。干净的摘要是一个连贯的单一叙述。"
},
"ja": {
"title": "要約が部分的な履歴ではなく全メッセージを置換",
"description": "自動コンパクトが発動すると、要約を生成してメッセージ履歴の全体をその要約で置換します。要約と並べて直近 N 件のメッセージを保持することはしません。これにより微妙な一貫性の問題を回避します直近のメッセージと古いメッセージの要約を併存させると、モデルは重複するコンテンツの2つの表現を見ることになります。要約が「アプローチ X を使うことに決めた」と言う一方で、直近のメッセージにはまだ検討過程が表示されているかもしれず、矛盾するシグナルを生じます。クリーンな要約は単一の一貫した物語です。"
}
},
{
"id": "transcript-archival",
"title": "Full Conversation Archived to JSONL on Disk",
"description": "Even though context is compressed in memory, the full uncompressed conversation is appended to a JSONL file on disk. Every message, every tool call, every result -- nothing is lost. This means compression is a lossy operation on the in-memory context but a lossless operation on the permanent record. Post-hoc analysis (debugging agent behavior, computing token usage, training data extraction) can always work from the complete transcript. The JSONL format is append-only, making it safe for concurrent writes and easy to stream-process.",
"alternatives": "Not archiving saves disk space but makes debugging hard -- when the agent makes a mistake, you can't see what it was 'thinking' 200 messages ago because that context was compressed away. Database storage (SQLite) would provide queryability but adds a dependency. JSONL is the simplest format that supports append-only writes and line-by-line processing.",
"zh": {
"title": "完整对话以 JSONL 格式归档到磁盘",
"description": "尽管上下文在内存中被压缩,完整的未压缩对话仍会追加到磁盘上的 JSONL 文件中。每条消息、每次工具调用、每个结果都不会丢失。压缩对内存上下文是有损操作,但对永久记录是无损的。事后分析(调试 agent 行为、计算 token 用量、提取训练数据始终可以基于完整记录进行。JSONL 格式仅追加写入,对并发写入安全,易于流式处理。"
},
"ja": {
"title": "完全な会話を JSONL としてディスクに保存",
"description": "メモリ上でコンテキストが圧縮されても、完全な非圧縮会話はディスク上の JSONL ファイルに追記されます。全てのメッセージ、全てのツール呼び出し、全ての結果――何も失われません。圧縮はインメモリコンテキストに対しては不可逆ですが、永続記録に対しては可逆です。事後分析エージェントの挙動デバッグ、トークン使用量の計算、学習データの抽出は常に完全な記録から行えます。JSONL フォーマットは追記専用で、並行書き込みに安全であり行単位の処理が容易です。"
}
}
]
}

View File

@@ -0,0 +1,47 @@
{
"version": "s07",
"decisions": [
{
"id": "file-based-persistence",
"title": "Tasks Stored as JSON Files, Not In-Memory",
"description": "Tasks are persisted as JSON files in a .tasks/ directory on the filesystem instead of being held in memory. This has three critical benefits: (1) Tasks survive process crashes -- if the agent dies mid-task, the task board is still on disk when it restarts. (2) Multiple agents can read and write to the same task directory, enabling multi-agent coordination without shared memory. (3) Humans can inspect and manually edit task files for debugging. The filesystem becomes the shared database.",
"alternatives": "In-memory storage (like v2's TodoWrite) is simpler and faster but loses state on crash and doesn't work across multiple agent processes. A proper database (SQLite, Redis) would provide ACID guarantees and better concurrency, but adds a dependency and operational complexity. Files are the zero-dependency persistence layer that works everywhere.",
"zh": {
"title": "任务存储为 JSON 文件,而非内存",
"description": "任务以 JSON 文件形式持久化在 .tasks/ 目录中,而非保存在内存里。这有三个关键好处:(1) 任务在进程崩溃后仍然存在——如果 agent 在任务中途崩溃,重启后任务板仍在磁盘上;(2) 多个 agent 可以读写同一任务目录,无需共享内存即可实现多代理协调;(3) 人类可以查看和手动编辑任务文件来调试。文件系统就是共享数据库。"
},
"ja": {
"title": "タスクをメモリではなく JSON ファイルとして保存",
"description": "タスクはメモリ内ではなく .tasks/ ディレクトリに JSON ファイルとして永続化されます。3つの重要な利点があります(1) プロセスのクラッシュ後もタスクが存続する――エージェントがタスク途中でクラッシュしても、再起動時にタスクボードはディスク上に残っています。(2) 複数のエージェントが同じタスクディレクトリを読み書きでき、共有メモリなしにマルチエージェント連携が可能になります。(3) 人間がデバッグのためにタスクファイルを検査・手動編集できます。ファイルシステムが共有データベースになります。"
}
},
{
"id": "dependency-graph",
"title": "Tasks Have blocks/blockedBy Dependency Fields",
"description": "Each task can declare which other tasks it blocks (downstream dependents) and which tasks block it (upstream dependencies). An agent will not start a task that has unresolved blockedBy dependencies. This is essential for multi-agent coordination: when Agent A is writing the database schema and Agent B needs to write queries against it, Agent B's task is blockedBy Agent A's task. Without dependencies, both agents might start simultaneously and Agent B would work against a schema that doesn't exist yet.",
"alternatives": "Simple priority ordering (high/medium/low) doesn't capture 'task B literally cannot start until task A finishes.' A centralized coordinator that assigns tasks in order would work but creates a single point of failure and bottleneck. Declarative dependencies let each agent independently determine what it can work on by reading the task files.",
"zh": {
"title": "任务具有 blocks/blockedBy 依赖字段",
"description": "每个任务可以声明它阻塞哪些任务下游依赖以及它被哪些任务阻塞上游依赖。Agent 不会开始有未解决 blockedBy 依赖的任务。这对多代理协调至关重要:当 Agent A 在编写数据库 schema、Agent B 需要写查询时Agent B 的任务被 Agent A 的任务阻塞。没有依赖关系,两个 agent 可能同时开始,而 Agent B 会针对一个尚不存在的 schema 工作。"
},
"ja": {
"title": "タスクに blocks/blockedBy 依存関係フィールド",
"description": "各タスクは、自分がブロックするタスク(下流の依存先)と、自分をブロックするタスク(上流の依存元)を宣言できます。エージェントは未解決の blockedBy 依存がある タスクを開始しません。これはマルチエージェント連携に不可欠です:エージェント A がデータベーススキーマを書いていてエージェント B がそれに対するクエリを書く必要がある場合、B のタスクは A のタスクにブロックされます。依存関係がなければ両エージェントが同時に開始し、B はまだ存在しないスキーマに対して作業することになります。"
}
},
{
"id": "task-replaces-todo",
"title": "TaskManager Replaces TodoWrite",
"description": "TaskManager is the multi-agent evolution of TodoWrite. Same core concept (a list of items with statuses) but with critical additions: file persistence (survives crashes), dependency tracking (blocks/blockedBy), ownership (which agent is working on what), and multi-process safety. TodoWrite was designed for a single agent tracking its own work in memory. TaskManager is designed for a team of agents coordinating through the filesystem. The API is intentionally similar so the conceptual upgrade path is clear.",
"alternatives": "Keeping TodoWrite for single-agent use and adding TaskManager only for multi-agent scenarios would avoid breaking the single-agent experience. But maintaining two systems with overlapping functionality increases complexity. TaskManager is a strict superset of TodoWrite -- a single agent using TaskManager just ignores the multi-agent features.",
"zh": {
"title": "TaskManager 取代 TodoWrite",
"description": "TaskManager 是 TodoWrite 的多代理进化版。核心概念相同带状态的项目列表但增加了关键能力文件持久化崩溃后存活、依赖追踪blocks/blockedBy、所有权哪个 agent 在处理什么、以及多进程安全。TodoWrite 为单 agent 在内存中追踪自身工作而设计。TaskManager 为代理团队通过文件系统协调而设计。API 刻意保持相似,使概念升级路径清晰。"
},
"ja": {
"title": "TaskManager が TodoWrite を置き換え",
"description": "TaskManager は TodoWrite のマルチエージェント進化版です。コア概念は同じステータス付きの項目リストですが、重要な追加がありますファイル永続化クラッシュ後も存続、依存関係追跡blocks/blockedBy、所有権どのエージェントが何を担当しているか、マルチプロセス安全性。TodoWrite は単一エージェントがメモリ内で自身の作業を追跡するために設計されました。TaskManager はエージェントチームがファイルシステムを通じて連携するために設計されています。API は意図的に類似させ、概念的なアップグレードパスを明確にしています。"
}
}
]
}

View File

@@ -0,0 +1,47 @@
{
"version": "s08",
"decisions": [
{
"id": "notification-bus",
"title": "threading.Queue as the Notification Bus",
"description": "Background task results are delivered via a threading.Queue instead of direct callbacks. The background thread puts a notification on the queue when its work completes. The main agent loop polls the queue before each LLM call. This decoupling is important: the background thread doesn't need to know anything about the main loop's state or timing. It just drops a message on the queue and moves on. The main loop picks it up at its own pace -- never mid-API-call, never mid-tool-execution. No race conditions, no callback hell.",
"alternatives": "Direct callbacks (background thread calls a function in the main thread) would deliver results faster but create thread-safety issues -- the callback might fire while the main thread is in the middle of building a request. Event-driven systems (asyncio, event emitters) work but add complexity. A queue is the simplest thread-safe communication primitive.",
"zh": {
"title": "用 threading.Queue 作为通知总线",
"description": "后台任务结果通过 threading.Queue 传递,而非直接回调。后台线程在工作完成时向队列放入通知,主 agent 循环在每次 LLM 调用前轮询队列。这种解耦很重要:后台线程无需了解主循环的状态或时序,只需往队列放入消息然后继续。主循环按自己的节奏取出消息——永远不会在 API 调用中途或工具执行中途。没有竞争条件,没有回调地狱。"
},
"ja": {
"title": "threading.Queue を通知バスとして使用",
"description": "バックグラウンドタスクの結果は直接コールバックではなく threading.Queue を通じて配信されます。バックグラウンドスレッドは作業完了時にキューに通知を投入します。メインのエージェントループは各 LLM 呼び出しの前にキューをポーリングします。この疎結合が重要ですバックグラウンドスレッドはメインループの状態やタイミングを一切知る必要がありません。キューにメッセージを入れて先に進むだけです。メインループは自分のペースで取り出します――API 呼び出しの途中でもツール実行の途中でもありません。レースコンディションもコールバック地獄もありません。"
}
},
{
"id": "daemon-threads",
"title": "Background Tasks Run as Daemon Threads",
"description": "Background task threads are created with daemon=True. In Python, daemon threads are killed automatically when the main thread exits. This prevents a common problem: if the main agent completes its work and exits, but a background thread is still running (waiting on a long API call, stuck in a loop), the process would hang indefinitely. With daemon threads, exit is clean -- the main thread finishes, all daemon threads die, process exits. No zombie processes, no cleanup code needed.",
"alternatives": "Non-daemon threads with explicit cleanup (join with timeout, then terminate) give more control over shutdown but require careful lifecycle management. Process-based parallelism (multiprocessing) provides stronger isolation but higher overhead. Daemon threads are the pragmatic choice: minimal code, correct behavior in the common case.",
"zh": {
"title": "后台任务以守护线程运行",
"description": "后台任务线程以 daemon=True 创建。在 Python 中,守护线程在主线程退出时自动被终止。这防止了一个常见问题:如果主 agent 完成工作并退出,但后台线程仍在运行(等待一个长时间 API 调用或陷入循环),进程会无限挂起。使用守护线程,退出是干净的——主线程结束,所有守护线程自动终止,进程退出。没有僵尸进程,不需要清理代码。"
},
"ja": {
"title": "バックグラウンドタスクはデーモンスレッドとして実行",
"description": "バックグラウンドタスクのスレッドは daemon=True で作成されます。Python ではデーモンスレッドはメインスレッドの終了時に自動的に終了されます。これにより一般的な問題を防ぎます:メインエージェントが作業を完了して終了しても、バックグラウンドスレッドがまだ実行中(長い API 呼び出しを待機、ループに陥っている)だとプロセスが無限にハングします。デーモンスレッドならクリーンに終了できます――メインスレッドが終了すると全デーモンスレッドが自動終了し、プロセスが終了します。ゾンビプロセスもクリーンアップコードも不要です。"
}
},
{
"id": "attachment-format",
"title": "Structured Notification Format with Type Tags",
"description": "Notifications from background tasks use a structured format: {\"type\": \"attachment\", \"attachment\": {status, result, ...}} instead of plain text strings. The type tag lets the main loop handle different notification types differently: an 'attachment' might be injected into the conversation as a tool_result, while a 'status_update' might just update a progress indicator. Machine-readable notifications also enable programmatic filtering (show only errors, suppress progress updates) and UI rendering (display status as a progress bar, not raw text).",
"alternatives": "Plain text notifications are simpler but lose structure. The main loop would have to parse free-form text to determine what happened, which is fragile. A class hierarchy (StatusNotification, ResultNotification, ErrorNotification) is more Pythonic but less portable -- JSON structures work the same way regardless of language or serialization format.",
"zh": {
"title": "带类型标签的结构化通知格式",
"description": "后台任务的通知使用结构化格式:{\"type\": \"attachment\", \"attachment\": {status, result, ...}}而非纯文本字符串。类型标签让主循环可以区别处理不同通知类型attachment 可能作为 tool_result 注入对话,而 status_update 可能只更新进度指示器。机器可读的通知还支持程序化过滤(只显示错误、抑制进度更新)和 UI 渲染(将状态显示为进度条而非原始文本)。"
},
"ja": {
"title": "型タグ付き構造化通知フォーマット",
"description": "バックグラウンドタスクからの通知は構造化フォーマットを使用します:プレーンテキストではなく {\"type\": \"attachment\", \"attachment\": {status, result, ...}} です。型タグによりメインループは異なる通知タイプを異なる方法で処理できますattachment は会話に tool_result として注入され、status_update は進捗インジケーターの更新のみを行うかもしれません。機械可読な通知はプログラム的なフィルタリング(エラーのみ表示、進捗更新の抑制)や UI レンダリング(ステータスを生テキストではなくプログレスバーとして表示)も可能にします。"
}
}
]
}

View File

@@ -0,0 +1,47 @@
{
"version": "s09",
"decisions": [
{
"id": "teammate-vs-subagent",
"title": "Persistent Teammates vs One-Shot Subagents",
"description": "In v3, subagents are ephemeral: spawn, do one task, return result, die. Their knowledge dies with them. In v8, teammates are persistent threads with identity (name, role) and config files. A teammate can complete task A, then be assigned task B, carrying forward everything it learned. This is the difference between hiring a contractor for one job and having a team member. Persistent teammates accumulate project knowledge, understand established patterns, and don't need to re-read the same files for every task.",
"alternatives": "One-shot subagents (v3 style) are simpler and provide perfect context isolation -- no risk of one task's context polluting another. But the re-learning cost is high: every new task starts from zero. A middle ground (subagents with shared memory/knowledge base) was considered but adds complexity without the full benefit of persistent identity and state.",
"zh": {
"title": "持久化队友 vs 一次性子代理",
"description": "在 v3 中,子代理是临时的:创建、执行一个任务、返回结果、销毁。它们的知识随之消亡。在 v8 中,队友是具有身份(名称、角色)和配置文件的持久化线程。队友可以完成任务 A然后被分配任务 B并携带之前学到的所有知识。这就是雇一个临时工做一个项目和拥有一个团队成员之间的区别。持久化队友积累项目知识理解已建立的模式不需要为每个任务重新阅读相同的文件。"
},
"ja": {
"title": "永続的なチームメイト vs 使い捨てサブエージェント",
"description": "v3 ではサブエージェントは一時的です生成、1つのタスクを実行、結果を返却、消滅。その知識も一緒に消えます。v8 ではチームメイトはアイデンティティ(名前、役割)と設定ファイルを持つ永続的なスレッドです。チームメイトはタスク A を完了した後、学んだ全てを引き継いでタスク B に割り当てられます。これは1つの仕事のために請負業者を雇うことと、チームメンバーを持つことの違いです。永続的なチームメイトはプロジェクトの知識を蓄積し、確立されたパターンを理解し、タスクごとに同じファイルを再読する必要がありません。"
}
},
{
"id": "file-based-team-config",
"title": "Team Config Persisted to .teams/{name}/config.json",
"description": "Team structure (member names, roles, agent IDs) is stored in a JSON config file, not in any agent's memory. Any agent can discover its teammates by reading the config file -- no need for a discovery service or shared memory. If an agent crashes and restarts, it reads the config to find out who else is on the team. This is consistent with the v6 philosophy: the filesystem is the coordination layer. Config files are also human-readable, making it easy to manually add/remove team members or debug team setup issues.",
"alternatives": "In-memory team registries are faster but don't survive process restarts and require a central process to maintain. Service discovery (like DNS or a discovery server) is more robust at scale but overkill for a local multi-agent system. File-based config is the simplest approach that works across independent processes.",
"zh": {
"title": "团队配置持久化到 .teams/{name}/config.json",
"description": "团队结构成员名称、角色、agent ID存储在 JSON 配置文件中,而非任何 agent 的内存中。任何 agent 都可以通过读取配置文件发现队友——无需发现服务或共享内存。如果 agent 崩溃并重启,它读取配置即可知道团队中还有谁。这与 v6 的理念一致:文件系统就是协调层。配置文件人类可读,便于手动添加或移除团队成员、调试团队配置问题。"
},
"ja": {
"title": "チーム設定を .teams/{name}/config.json に永続化",
"description": "チーム構成(メンバー名、役割、エージェント IDはエージェントのメモリではなく JSON 設定ファイルに保存されます。どのエージェントも設定ファイルを読むことでチームメイトを発見できます――ディスカバリーサービスや共有メモリは不要です。エージェントがクラッシュして再起動した場合、設定を読んで他のチームメンバーを把握します。これは v6 の思想と一貫しています:ファイルシステムが連携レイヤーです。設定ファイルは人間が読めるため、チームメンバーの手動追加・削除やチーム設定問題のデバッグが容易です。"
}
},
{
"id": "tool-filtering-by-role",
"title": "Teammates Get Subset of Tools, Lead Gets All",
"description": "The team lead receives ALL_TOOLS (including TeamCreate, SendMessage, TaskCreate, etc.) while teammates receive TEAMMATE_TOOLS (a reduced set focused on task execution). This enforces a clear separation of concerns: teammates focus on doing work (coding, testing, researching), while the lead focuses on coordination (creating tasks, assigning work, managing communication). Giving teammates coordination tools would let them create their own sub-teams or reassign tasks, undermining the lead's ability to maintain a coherent plan.",
"alternatives": "Giving all agents identical tools is simpler and more egalitarian, but in practice leads to coordination chaos -- multiple agents trying to manage each other, creating conflicting task assignments. A permission system (any agent can request elevation) adds flexibility but also complexity. Static role-based filtering is predictable and easy to reason about.",
"zh": {
"title": "队友获得工具子集,组长获得全部工具",
"description": "团队组长获得 ALL_TOOLS包括 TeamCreate、SendMessage、TaskCreate 等),而队友获得 TEAMMATE_TOOLS专注于任务执行的精简工具集。这强制了清晰的职责分离队友专注于做事编码、测试、研究组长专注于协调创建任务、分配工作、管理沟通。给队友协调工具会让他们创建自己的子团队或重新分配任务破坏组长维持连贯计划的能力。"
},
"ja": {
"title": "チームメイトはツールのサブセット、リーダーは全ツール",
"description": "チームリーダーは ALL_TOOLSTeamCreate、SendMessage、TaskCreate など含む)を受け取り、チームメイトは TEAMMATE_TOOLSタスク実行に特化した縮小セットを受け取ります。これにより明確な関心の分離が強制されますチームメイトは作業コーディング、テスト、調査に集中し、リーダーは調整タスク作成、作業割り当て、コミュニケーション管理に集中します。チームメイトに調整ツールを与えると、独自のサブチーム作成やタスクの再割り当てが可能になり、リーダーの一貫した計画維持能力が損なわれます。"
}
}
]
}

View File

@@ -0,0 +1,47 @@
{
"version": "s10",
"decisions": [
{
"id": "jsonl-inbox",
"title": "JSONL Inbox Files Instead of Shared Memory",
"description": "Each teammate has its own inbox file (a JSONL file in the team directory). Sending a message means appending a JSON line to the recipient's inbox file. Reading messages means reading the inbox file and tracking which line was last read. JSONL is append-only by nature, which means concurrent writers don't corrupt each other's data (appends to different file positions). This works across processes without any shared memory, mutex, or IPC mechanism. It's also crash-safe: if the writer crashes mid-append, the worst case is one partial line that the reader can skip.",
"alternatives": "Shared memory (Python multiprocessing.Queue) would be faster but doesn't work if agents are separate processes launched independently. A message broker (Redis, RabbitMQ) provides robust pub/sub but adds infrastructure dependencies. Unix domain sockets would work but are harder to debug (no human-readable message log). JSONL files are the simplest approach that provides persistence, cross-process communication, and debuggability.",
"zh": {
"title": "JSONL 收件箱文件而非共享内存",
"description": "每个队友都有自己的收件箱文件(团队目录中的 JSONL 文件)。发送消息意味着向接收者的收件箱文件追加一行 JSON。读取消息意味着读取收件箱文件并追踪上次读到的行。JSONL 天然是仅追加的,这意味着并发写入不会破坏彼此的数据(追加到不同的文件位置)。这在无需共享内存、互斥锁或 IPC 机制的情况下跨进程工作。它也是崩溃安全的:如果写入者在追加中途崩溃,最坏情况是一行不完整的数据,读取者可以跳过。"
},
"ja": {
"title": "共有メモリではなく JSONL インボックスファイル",
"description": "各チームメイトはチームディレクトリ内に独自のインボックスファイルJSONL ファイル)を持ちます。メッセージの送信は受信者のインボックスファイルに JSON 行を追記することです。メッセージの読み取りはインボックスファイルを読んで最後に読んだ行を追跡することです。JSONL は本質的に追記専用で、並行ライターが互いのデータを破壊しません異なるファイル位置への追記。共有メモリ、ミューテックス、IPC メカニズムなしにプロセス間で動作します。クラッシュにも安全ですライターが追記途中でクラッシュしても、最悪の場合は不完全な1行だけでリーダーはスキップできます。"
}
},
{
"id": "five-message-types",
"title": "Exactly Five Message Types Cover All Coordination Patterns",
"description": "The messaging system supports exactly five types: (1) 'message' for point-to-point communication between two agents, (2) 'broadcast' for team-wide announcements, (3) 'shutdown_request' for graceful termination, (4) 'shutdown_response' for acknowledging shutdown, (5) 'plan_approval_response' for the lead to approve or reject a teammate's plan. These five types map to the fundamental coordination patterns: direct communication, broadcast, lifecycle management, and approval workflows. Adding more types (e.g., priority_message, status_update) would increase complexity without enabling new coordination patterns.",
"alternatives": "A single generic message type with metadata fields would be more flexible but makes it harder to enforce protocol correctness. Many more types (10+) would provide finer-grained semantics but increase the model's decision burden. Five types is the sweet spot where every type has a clear, distinct purpose and the model can reliably choose the right one.",
"zh": {
"title": "恰好五种消息类型覆盖所有协调模式",
"description": "消息系统恰好支持五种类型:(1) message 用于两个 agent 间的点对点通信;(2) broadcast 用于全团队公告;(3) shutdown_request 用于优雅终止;(4) shutdown_response 用于确认终止;(5) plan_approval_response 用于组长批准或拒绝队友的计划。这五种类型映射到基本协调模式:直接通信、广播、生命周期管理和审批流程。增加更多类型(如 priority_message、status_update只会增加复杂度而不会启用新的协调模式。"
},
"ja": {
"title": "正確に5つのメッセージタイプで全連携パターンをカバー",
"description": "メッセージングシステムは正確に5つのタイプをサポートします(1) message は2つのエージェント間のポイントツーポイント通信、(2) broadcast はチーム全体への通知、(3) shutdown_request はグレースフルな終了要求、(4) shutdown_response はシャットダウンの確認応答、(5) plan_approval_response はリーダーによるチームメイトの計画の承認・却下。これら5タイプは基本的な連携パターンに対応します直接通信、ブロードキャスト、ライフサイクル管理、承認ワークフロー。タイプを増やしてもpriority_message、status_update など)新たな連携パターンは生まれず、複雑さが増すだけです。"
}
},
{
"id": "inbox-before-api-call",
"title": "Check Inbox Before Every LLM Call",
"description": "Teammates check their inbox file at the top of every agent loop iteration, before calling the LLM API. This ensures maximum responsiveness to incoming messages: a shutdown request is seen within one loop iteration (typically seconds), not after the current task completes (potentially minutes). The inbox check is cheap (read a small file, check if new lines exist) compared to the LLM call (seconds of latency, thousands of tokens). This placement also means incoming messages can influence the next LLM call -- a message saying 'stop working on X, switch to Y' takes effect immediately.",
"alternatives": "Checking inbox after each tool execution would be more responsive but adds overhead to every tool call, which is more frequent than LLM calls. A separate watcher thread could monitor the inbox continuously but adds threading complexity. Checking once per LLM call is the pragmatic sweet spot: responsive enough for coordination, cheap enough to not impact performance.",
"zh": {
"title": "每次 LLM 调用前检查收件箱",
"description": "队友在每次 agent 循环迭代的顶部、调用 LLM API 之前检查收件箱文件。这确保了对传入消息的最大响应性:一个终止请求会在一个循环迭代内被看到(通常几秒钟),而非在当前任务完成后(可能数分钟)。收件箱检查成本很低(读取小文件,检查是否有新行),相比 LLM 调用(秒级延迟,数千 token微不足道。这个位置还意味着传入消息可以影响下一次 LLM 调用——一条'停止 X转去做 Y'的消息会立即生效。"
},
"ja": {
"title": "毎回の LLM 呼び出し前にインボックスを確認",
"description": "チームメイトはエージェントループの各イテレーションの冒頭、LLM API を呼び出す前にインボックスファイルを確認します。これにより受信メッセージへの応答性を最大化しますシャットダウンリクエストは1ループイテレーション以内通常数秒で確認され、現在のタスク完了後数分かかる可能性ではありません。インボックスの確認は安価で小さなファイルを読み、新しい行があるか確認、LLM 呼び出し(秒単位のレイテンシ、数千トークン)と比べて微々たるものです。この配置により受信メッセージが次の LLM 呼び出しに影響できます――「X の作業を止めて Y に切り替えて」というメッセージが即座に有効になります。"
}
}
]
}

View File

@@ -0,0 +1,47 @@
{
"version": "s11",
"decisions": [
{
"id": "polling-not-events",
"title": "Polling for Unclaimed Tasks Instead of Event-Driven Notification",
"description": "Autonomous teammates poll the shared task board every ~1 second to find unclaimed tasks, rather than waiting for event-driven notifications. Polling is fundamentally simpler than pub/sub: there's no subscription management, no event routing, no missed-event bugs. With file-based persistence, polling is just 'read the directory listing' -- a cheap operation that works regardless of how many agents are running. The 1-second interval balances responsiveness (new tasks are discovered quickly) against filesystem overhead (not hammering the disk with reads).",
"alternatives": "Event-driven notification (file watchers via inotify/fsevents, or a pub/sub channel) would reduce latency from seconds to milliseconds. But file watchers are platform-specific and unreliable across network filesystems. A message broker would work but adds infrastructure. For a system where tasks take minutes to complete, discovering new tasks in 1 second instead of 10 milliseconds makes no practical difference.",
"zh": {
"title": "轮询未认领任务而非事件驱动通知",
"description": "自主队友每隔约 1 秒轮询共享任务板以寻找未认领的任务,而非等待事件驱动的通知。轮询从根本上比发布/订阅更简单:没有订阅管理、没有事件路由、没有事件丢失的 bug。在基于文件的持久化下轮询就是'读取目录列表'——一个低成本操作,无论有多少 agent 在运行都能正常工作。1 秒的间隔平衡了响应性(新任务被快速发现)和文件系统开销(不会过度读取磁盘)。"
},
"ja": {
"title": "イベント駆動通知ではなくポーリングで未割り当てタスクを発見",
"description": "自律的なチームメイトはイベント駆動の通知を待つのではなく、約1秒ごとに共有タスクボードをポーリングして未割り当てタスクを探します。ポーリングはパブ/サブより根本的にシンプルですサブスクリプション管理、イベントルーティング、イベント欠落バグがありません。ファイルベースの永続化では、ポーリングは「ディレクトリ一覧を読む」だけで、実行中のエージェント数に関係なく動作する安価な操作です。1秒間隔は応答性新タスクの迅速な発見とファイルシステムのオーバーヘッドディスク読み取りの過負荷回避のバランスを取っています。"
}
},
{
"id": "idle-timeout",
"title": "60-Second Idle Timeout Before Self-Termination",
"description": "When an autonomous teammate has no tasks to work on and no messages in its inbox, it waits up to 60 seconds before giving up and shutting down. This prevents zombie teammates that wait forever for work that never comes -- a real problem when the lead forgets to send a shutdown request, or when all remaining tasks are blocked on external events. The 60-second window is long enough that a brief gap between task completions and new task creation won't cause premature shutdown, but short enough that unused teammates don't waste resources.",
"alternatives": "No timeout (wait forever) risks zombie processes. A very short timeout (5s) causes premature exits when the lead is simply thinking or typing. A heartbeat system (lead periodically pings teammates to keep them alive) works but adds protocol complexity. The 60-second fixed timeout is a good default that balances false-positive exits against resource waste.",
"zh": {
"title": "空闲 60 秒后自动终止",
"description": "当自主队友没有任务可做且收件箱中没有消息时,它最多等待 60 秒后放弃并关闭。这防止了永远等待不会到来的工作的僵尸队友——这在组长忘记发送关闭请求、或所有剩余任务都被外部事件阻塞时是真实存在的问题。60 秒窗口足够长,不会因为任务完成到新任务创建之间的短暂间隔而导致过早关闭;又足够短,不会让闲置队友浪费资源。"
},
"ja": {
"title": "60秒のアイドルタイムアウトで自動終了",
"description": "自律的なチームメイトが作業するタスクもインボックスのメッセージもない場合、最大60秒待ってから諦めてシャットダウンします。これにより永遠に来ない仕事を待ち続けるゾンビチームメイトを防ぎます――リーダーがシャットダウンリクエストの送信を忘れたり、残りのタスクが全て外部イベントでブロックされている場合に実際に起こる問題です。60秒のウィンドウはタスク完了から新タスク作成までの短い間隔で早期シャットダウンが起きない十分な長さであり、かつ未使用のチームメイトがリソースを浪費しない十分な短さです。"
}
},
{
"id": "identity-after-compression",
"title": "Re-Inject Teammate Identity After Context Compression",
"description": "When auto_compact compresses the conversation, the resulting summary loses crucial metadata: the teammate's name, which team it belongs to, and its agent_id. Without this information, the teammate can't claim tasks (tasks are owned by name), can't check its inbox (inbox files are keyed by agent_id), and can't identify itself in messages. So after every auto_compact, the system re-injects a structured identity block into the conversation: 'You are [name] on team [team], your agent_id is [id], your inbox is at [path].' This is the minimum context needed for the teammate to remain functional after memory loss.",
"alternatives": "Putting identity in the system prompt (which survives compression) would avoid this problem, but violates the cache-friendly static-system-prompt design from v4. Embedding identity in the summary prompt ('when summarizing, always include your name and team') is unreliable -- the LLM might omit it. Explicit post-compression injection is deterministic and guaranteed to work.",
"zh": {
"title": "上下文压缩后重新注入队友身份",
"description": "自动压缩对话时,生成的摘要会丢失关键元数据:队友的名称、所属团队和 agent_id。没有这些信息队友无法认领任务任务按名称归属、无法检查收件箱收件箱文件以 agent_id 为键)、也无法在消息中表明身份。因此每次自动压缩后,系统会向对话中重新注入一个结构化的身份块:'你是 [team] 团队的 [name],你的 agent_id 是 [id],你的收件箱在 [path]。'这是队友在记忆丢失后保持功能所需的最小上下文。"
},
"ja": {
"title": "コンテキスト圧縮後にチームメイトのアイデンティティを再注入",
"description": "自動コンパクトが会話を圧縮すると、生成された要約は重要なメタデータを失いますチームメイトの名前、所属チーム、agent_id。この情報がなければチームメイトはタスクを申告できずタスクは名前で所有、インボックスを確認できずインボックスファイルは agent_id をキーとする)、メッセージで自分を識別できません。そのため自動コンパクトの後、システムは構造化されたアイデンティティブロックを会話に再注入します:「あなたは [team] チームの [name] です。agent_id は [id]、インボックスは [path] にあります。」これはメモリ喪失後もチームメイトが機能し続けるために必要な最小限のコンテキストです。"
}
}
]
}

View File

@@ -0,0 +1,278 @@
import type { FlowNode, FlowEdge } from "@/types/agent-data";
export interface FlowDefinition {
nodes: FlowNode[];
edges: FlowEdge[];
}
const FLOW_WIDTH = 600;
const COL_CENTER = FLOW_WIDTH / 2;
const COL_LEFT = 140;
const COL_RIGHT = FLOW_WIDTH - 140;
export const EXECUTION_FLOWS: Record<string, FlowDefinition> = {
s01: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 110 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 190 },
{ id: "bash", label: "Execute Bash", type: "subprocess", x: COL_LEFT, y: 280 },
{ id: "append", label: "Append Result", type: "process", x: COL_LEFT, y: 360 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 280 },
],
edges: [
{ from: "start", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "bash", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "bash", to: "append" },
{ from: "append", to: "llm" },
],
},
s02: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 110 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 190 },
{ id: "dispatch", label: "Tool Dispatch", type: "process", x: COL_LEFT, y: 280 },
{ id: "exec", label: "bash / read / write / edit", type: "subprocess", x: COL_LEFT, y: 360 },
{ id: "append", label: "Append Result", type: "process", x: COL_LEFT, y: 440 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 280 },
],
edges: [
{ from: "start", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "dispatch", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "dispatch", to: "exec" },
{ from: "exec", to: "append" },
{ from: "append", to: "llm" },
],
},
s03: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "todo", label: "Create Todos", type: "process", x: COL_CENTER, y: 100 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 180 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 260 },
{ id: "exec", label: "Execute Tool", type: "subprocess", x: COL_LEFT, y: 340 },
{ id: "append", label: "Append Result", type: "process", x: COL_LEFT, y: 410 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 340 },
],
edges: [
{ from: "start", to: "todo" },
{ from: "todo", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "exec", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "exec", to: "append" },
{ from: "append", to: "llm" },
],
},
s04: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 110 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 190 },
{ id: "is_task", label: "task tool?", type: "decision", x: COL_LEFT, y: 280 },
{ id: "spawn", label: "Spawn Subagent\n(fresh messages[])", type: "subprocess", x: 60, y: 380 },
{ id: "sub_loop", label: "Subagent Loop", type: "process", x: 60, y: 460 },
{ id: "exec", label: "Execute Tool", type: "subprocess", x: COL_LEFT + 80, y: 380 },
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 540 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 280 },
],
edges: [
{ from: "start", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "is_task", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "is_task", to: "spawn", label: "task" },
{ from: "is_task", to: "exec", label: "other" },
{ from: "spawn", to: "sub_loop" },
{ from: "sub_loop", to: "append" },
{ from: "exec", to: "append" },
{ from: "append", to: "llm" },
],
},
s05: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 110 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 190 },
{ id: "is_skill", label: "load_skill?", type: "decision", x: COL_LEFT, y: 280 },
{ id: "load", label: "Read SKILL.md", type: "subprocess", x: 60, y: 370 },
{ id: "inject", label: "Inject via\ntool_result", type: "process", x: 60, y: 450 },
{ id: "exec", label: "Execute Tool", type: "subprocess", x: COL_LEFT + 80, y: 370 },
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 530 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 280 },
],
edges: [
{ from: "start", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "is_skill", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "is_skill", to: "load", label: "skill" },
{ from: "is_skill", to: "exec", label: "other" },
{ from: "load", to: "inject" },
{ from: "inject", to: "append" },
{ from: "exec", to: "append" },
{ from: "append", to: "llm" },
],
},
s06: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "compress_check", label: "Over token\nlimit?", type: "decision", x: COL_CENTER, y: 110 },
{ id: "compress", label: "Compress Context", type: "subprocess", x: COL_RIGHT, y: 110 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 200 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 280 },
{ id: "exec", label: "Execute Tool", type: "subprocess", x: COL_LEFT, y: 360 },
{ id: "append", label: "Append Result", type: "process", x: COL_LEFT, y: 430 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 360 },
],
edges: [
{ from: "start", to: "compress_check" },
{ from: "compress_check", to: "compress", label: "yes" },
{ from: "compress_check", to: "llm", label: "no" },
{ from: "compress", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "exec", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "exec", to: "append" },
{ from: "append", to: "compress_check" },
],
},
s07: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 110 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 190 },
{ id: "is_task", label: "task_manager?", type: "decision", x: COL_LEFT, y: 280 },
{ id: "crud", label: "CRUD Task\n(file-based)", type: "subprocess", x: 60, y: 370 },
{ id: "dep_check", label: "Check\nDependencies", type: "process", x: 60, y: 450 },
{ id: "exec", label: "Execute Tool", type: "subprocess", x: COL_LEFT + 80, y: 370 },
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 530 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 280 },
],
edges: [
{ from: "start", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "is_task", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "is_task", to: "crud", label: "task" },
{ from: "is_task", to: "exec", label: "other" },
{ from: "crud", to: "dep_check" },
{ from: "dep_check", to: "append" },
{ from: "exec", to: "append" },
{ from: "append", to: "llm" },
],
},
s08: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 110 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 190 },
{ id: "bg_check", label: "Background?", type: "decision", x: COL_LEFT, y: 280 },
{ id: "bg_spawn", label: "Spawn Thread", type: "subprocess", x: 60, y: 370 },
{ id: "exec", label: "Execute Tool", type: "subprocess", x: COL_LEFT + 80, y: 370 },
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 450 },
{ id: "notify", label: "Notification\nQueue", type: "process", x: 60, y: 450 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 280 },
],
edges: [
{ from: "start", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "bg_check", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "bg_check", to: "bg_spawn", label: "bg" },
{ from: "bg_check", to: "exec", label: "fg" },
{ from: "bg_spawn", to: "notify" },
{ from: "exec", to: "append" },
{ from: "append", to: "llm" },
{ from: "notify", to: "llm" },
],
},
s09: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "llm", label: "LLM Call\n(team lead)", type: "process", x: COL_CENTER, y: 110 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 200 },
{ id: "is_team", label: "Team tool?", type: "decision", x: COL_LEFT, y: 290 },
{ id: "spawn", label: "Spawn\nTeammate", type: "subprocess", x: 60, y: 390 },
{ id: "msg", label: "Send Message\n(JSONL inbox)", type: "subprocess", x: 60, y: 470 },
{ id: "exec", label: "Execute Tool", type: "subprocess", x: COL_LEFT + 80, y: 390 },
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 550 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 290 },
{ id: "teammate", label: "Teammate Agent\n(own loop)", type: "process", x: COL_RIGHT, y: 470 },
],
edges: [
{ from: "start", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "is_team", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "is_team", to: "spawn", label: "spawn" },
{ from: "is_team", to: "exec", label: "other" },
{ from: "spawn", to: "teammate" },
{ from: "spawn", to: "msg" },
{ from: "msg", to: "append" },
{ from: "exec", to: "append" },
{ from: "append", to: "llm" },
],
},
s10: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "llm", label: "LLM Call\n(team lead)", type: "process", x: COL_CENTER, y: 110 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 200 },
{ id: "is_proto", label: "Protocol?", type: "decision", x: COL_LEFT, y: 290 },
{ id: "shutdown", label: "Shutdown\nRequest", type: "subprocess", x: 60, y: 390 },
{ id: "fsm", label: "FSM:\npending->approved", type: "process", x: 60, y: 470 },
{ id: "exec", label: "Execute Tool", type: "subprocess", x: COL_LEFT + 80, y: 390 },
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 550 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 290 },
{ id: "teammate", label: "Teammate\nreceives request_id", type: "process", x: COL_RIGHT, y: 470 },
],
edges: [
{ from: "start", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "is_proto", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "is_proto", to: "shutdown", label: "shutdown" },
{ from: "is_proto", to: "exec", label: "other" },
{ from: "shutdown", to: "fsm" },
{ from: "fsm", to: "teammate" },
{ from: "teammate", to: "append" },
{ from: "exec", to: "append" },
{ from: "append", to: "llm" },
],
},
s11: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "inbox", label: "Check Inbox", type: "process", x: COL_CENTER, y: 100 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 180 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 260 },
{ id: "exec", label: "Execute Tool", type: "subprocess", x: COL_LEFT, y: 340 },
{ id: "append", label: "Append Result", type: "process", x: COL_LEFT, y: 410 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 340 },
{ id: "idle", label: "Idle Cycle", type: "process", x: COL_RIGHT, y: 420 },
{ id: "poll", label: "Poll Tasks\n+ Auto-Claim", type: "subprocess", x: COL_RIGHT, y: 500 },
],
edges: [
{ from: "start", to: "inbox" },
{ from: "inbox", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "exec", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "exec", to: "append" },
{ from: "append", to: "llm" },
{ from: "end", to: "idle" },
{ from: "idle", to: "poll" },
{ from: "poll", to: "inbox" },
],
},
};
export function getFlowForVersion(version: string): FlowDefinition | null {
return EXECUTION_FLOWS[version] ?? null;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,51 @@
{
"version": "s01",
"title": "The Agent Loop",
"description": "A minimal agent that uses only bash to accomplish tasks",
"steps": [
{
"type": "user_message",
"content": "Create a file called hello.py that prints 'Hello, World!'",
"annotation": "User sends a task to the agent"
},
{
"type": "assistant_text",
"content": "I'll create that file for you using bash.",
"annotation": "The model decides to use its only tool: bash"
},
{
"type": "tool_call",
"content": "echo 'print(\"Hello, World!\")' > hello.py",
"toolName": "bash",
"annotation": "Tool call: the model generates a bash command"
},
{
"type": "tool_result",
"content": "",
"toolName": "bash",
"annotation": "Bash returns empty output (success)"
},
{
"type": "assistant_text",
"content": "Let me verify the file was created correctly.",
"annotation": "The loop continues: model sees the result and decides next action"
},
{
"type": "tool_call",
"content": "cat hello.py",
"toolName": "bash",
"annotation": "Another bash call to verify"
},
{
"type": "tool_result",
"content": "print(\"Hello, World!\")",
"toolName": "bash",
"annotation": "File contents confirmed"
},
{
"type": "assistant_text",
"content": "Done! I've created hello.py with the print statement. You can run it with `python hello.py`.",
"annotation": "stop_reason != 'tool_use' -> loop breaks, agent responds to user"
}
]
}

View File

@@ -0,0 +1,46 @@
{
"version": "s02",
"title": "Tools",
"description": "Agent with read, write, edit, and bash tools",
"steps": [
{
"type": "user_message",
"content": "Add a function called greet(name) to hello.py",
"annotation": "User requests a code modification"
},
{
"type": "assistant_text",
"content": "Let me read the current file first.",
"annotation": "Model chooses read_file instead of bash cat"
},
{
"type": "tool_call",
"content": "hello.py",
"toolName": "read_file",
"annotation": "Dedicated read tool provides cleaner file access"
},
{
"type": "tool_result",
"content": "print(\"Hello, World!\")",
"toolName": "read_file",
"annotation": "File contents returned to the model"
},
{
"type": "tool_call",
"content": "hello.py\n---\ndef greet(name):\n print(f\"Hello, {name}!\")\n\ngreet(\"World\")",
"toolName": "write_file",
"annotation": "write_file replaces the entire file content"
},
{
"type": "tool_result",
"content": "File written successfully",
"toolName": "write_file",
"annotation": "Write confirmed"
},
{
"type": "assistant_text",
"content": "I've updated hello.py with a greet(name) function that takes a name parameter.",
"annotation": "4 tools beat 1: each tool has a clear, focused purpose"
}
]
}

View File

@@ -0,0 +1,53 @@
{
"version": "s03",
"title": "TodoWrite",
"description": "Agent creates a visible plan before executing",
"steps": [
{
"type": "user_message",
"content": "Build a CLI calculator that supports add, subtract, multiply, divide",
"annotation": "A multi-step task that benefits from planning"
},
{
"type": "tool_call",
"content": "[\n { \"id\": 1, \"task\": \"Create calc.py with argument parsing\", \"status\": \"in_progress\" },\n { \"id\": 2, \"task\": \"Implement 4 operations\", \"status\": \"pending\" },\n { \"id\": 3, \"task\": \"Add error handling\", \"status\": \"pending\" },\n { \"id\": 4, \"task\": \"Test all operations\", \"status\": \"pending\" }\n]",
"toolName": "todo_write",
"annotation": "TodoManager creates a visible plan BEFORE any code is written"
},
{
"type": "tool_result",
"content": "Todo list created with 4 tasks",
"toolName": "todo_write",
"annotation": "The plan is now visible and trackable"
},
{
"type": "tool_call",
"content": "calc.py\n---\nimport sys\n\ndef calculate(op, a, b):\n ops = {'+': a+b, '-': a-b, '*': a*b, '/': a/b}\n return ops.get(op, 'Unknown operator')\n\nif __name__ == '__main__':\n a, op, b = float(sys.argv[1]), sys.argv[2], float(sys.argv[3])\n print(calculate(op, a, b))",
"toolName": "write_file",
"annotation": "Executing task #1 and #2: create file with operations"
},
{
"type": "tool_result",
"content": "File written successfully",
"toolName": "write_file",
"annotation": "Code written"
},
{
"type": "tool_call",
"content": "python calc.py 10 + 5",
"toolName": "bash",
"annotation": "Executing task #4: testing"
},
{
"type": "tool_result",
"content": "15.0",
"toolName": "bash",
"annotation": "Test passes"
},
{
"type": "assistant_text",
"content": "Calculator built and tested. All 4 operations work correctly.",
"annotation": "Visible plans improve task completion by making progress trackable"
}
]
}

View File

@@ -0,0 +1,51 @@
{
"version": "s04",
"title": "Subagents",
"description": "Agent spawns a subagent for isolated subtask execution",
"steps": [
{
"type": "user_message",
"content": "Refactor the calculator: split into modules and add tests",
"annotation": "Complex task suitable for delegation"
},
{
"type": "assistant_text",
"content": "I'll delegate the test writing to a subagent while I handle the refactoring.",
"annotation": "Agent decides to split work using the Task tool"
},
{
"type": "tool_call",
"content": "Write unit tests for calc.py covering all 4 operations and edge cases like division by zero",
"toolName": "task",
"annotation": "Task tool spawns a subagent with its own isolated context"
},
{
"type": "system_event",
"content": "[Subagent spawned] Context: fresh messages[], same tools, scoped prompt",
"annotation": "Subagent gets clean context -- no confusion from parent's conversation"
},
{
"type": "tool_result",
"content": "Created test_calc.py with 8 test cases. All passing.",
"toolName": "task",
"annotation": "Subagent completes and returns a summary to parent"
},
{
"type": "tool_call",
"content": "calc.py -> calc/operations.py, calc/cli.py",
"toolName": "write_file",
"annotation": "Parent continues with its own task: refactoring"
},
{
"type": "tool_result",
"content": "Files written successfully",
"toolName": "write_file",
"annotation": "Modules split"
},
{
"type": "assistant_text",
"content": "Refactored into calc/operations.py and calc/cli.py. Tests were written by a subagent and are all passing.",
"annotation": "Context isolation prevents confusion between tasks"
}
]
}

View File

@@ -0,0 +1,44 @@
{
"version": "s05",
"title": "Skills",
"description": "Agent loads external knowledge from SKILL.md files",
"steps": [
{
"type": "user_message",
"content": "Set up a new Next.js project with TypeScript",
"annotation": "Task requires framework-specific knowledge"
},
{
"type": "system_event",
"content": "[SkillLoader] Scanning for SKILL.md files... Found: nextjs.SKILL.md",
"annotation": "SkillLoader finds a matching skill file before the loop runs"
},
{
"type": "system_event",
"content": "[Skill injected] nextjs.SKILL.md -> tool_result message appended",
"annotation": "Skill content is injected as a tool_result, not bloating the system prompt"
},
{
"type": "assistant_text",
"content": "I have knowledge about Next.js setup. Let me create the project structure.",
"annotation": "Model now has framework-specific knowledge from the skill file"
},
{
"type": "tool_call",
"content": "npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir",
"toolName": "bash",
"annotation": "Using skill-provided knowledge for correct flags"
},
{
"type": "tool_result",
"content": "Success! Created my-app at /workspace/my-app",
"toolName": "bash",
"annotation": "Project created with correct configuration"
},
{
"type": "assistant_text",
"content": "Next.js project created with TypeScript, Tailwind CSS, ESLint, App Router, and src/ directory.",
"annotation": "Skills inject via tool_result, not system prompt -- keeping context lean"
}
]
}

View File

@@ -0,0 +1,51 @@
{
"version": "s06",
"title": "Compact",
"description": "Agent compresses context when tokens exceed threshold",
"steps": [
{
"type": "user_message",
"content": "Analyze all Python files in this project and summarize each one",
"annotation": "A task that will generate large amounts of context"
},
{
"type": "tool_call",
"content": "find . -name '*.py' -type f",
"toolName": "bash",
"annotation": "Finding all files to analyze"
},
{
"type": "tool_result",
"content": "./src/main.py\n./src/utils.py\n./src/models.py\n./tests/test_main.py\n... (28 more files)",
"toolName": "bash",
"annotation": "32 files found -- reading all of them will generate massive context"
},
{
"type": "assistant_text",
"content": "I'll read and analyze each file systematically.",
"annotation": "Agent begins processing files one by one"
},
{
"type": "tool_call",
"content": "./src/main.py",
"toolName": "read_file",
"annotation": "Reading file 1 of 32..."
},
{
"type": "tool_result",
"content": "[... 450 lines of code ...]",
"toolName": "read_file",
"annotation": "Large file content added to messages[]"
},
{
"type": "system_event",
"content": "[ContextManager] Token count: 148,231 / 150,000 threshold\nTriggering 3-layer compression:\n 1. Drop tool_result content > 1000 chars -> replaced with summary\n 2. Merge sequential assistant turns\n 3. Summarize oldest 50% of conversation\nResult: 148,231 -> 42,087 tokens",
"annotation": "Compression fires automatically when approaching the limit"
},
{
"type": "assistant_text",
"content": "Continuing analysis with compressed context. Files 1-18 have been summarized, proceeding with remaining files.",
"annotation": "Forgetting old tool results enables infinite work -- the agent can process any number of files"
}
]
}

View File

@@ -0,0 +1,53 @@
{
"version": "s07",
"title": "Tasks",
"description": "Agent uses persistent file-based tasks with dependencies",
"steps": [
{
"type": "user_message",
"content": "Build a REST API with user auth, database, and deployment config",
"annotation": "Multi-part task requiring dependency tracking"
},
{
"type": "tool_call",
"content": "create_task(\"Set up database schema\", deps=[])\ncreate_task(\"Implement user model\", deps=[1])\ncreate_task(\"Add auth endpoints\", deps=[2])\ncreate_task(\"Write deployment config\", deps=[3])",
"toolName": "task_manager",
"annotation": "TaskManager creates file-based tasks with explicit dependencies"
},
{
"type": "tool_result",
"content": "Created 4 tasks in .tasks/\n #1 Set up database schema [ready]\n #2 Implement user model [blocked by #1]\n #3 Add auth endpoints [blocked by #2]\n #4 Write deployment config [blocked by #3]",
"toolName": "task_manager",
"annotation": "Tasks persisted to .tasks/ directory as JSON files"
},
{
"type": "tool_call",
"content": "schema.sql\n---\nCREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT UNIQUE, password_hash TEXT);",
"toolName": "write_file",
"annotation": "Working on task #1"
},
{
"type": "tool_result",
"content": "File written successfully",
"toolName": "write_file",
"annotation": "Schema created"
},
{
"type": "tool_call",
"content": "complete_task(1)",
"toolName": "task_manager",
"annotation": "Completing task #1 automatically unblocks task #2"
},
{
"type": "tool_result",
"content": "Task #1 completed. Task #2 is now ready.",
"toolName": "task_manager",
"annotation": "File-based persistence outlives process memory"
},
{
"type": "assistant_text",
"content": "Database schema is done. Moving to user model implementation.",
"annotation": "Dependency graph ensures tasks execute in correct order"
}
]
}

View File

@@ -0,0 +1,56 @@
{
"version": "s08",
"title": "Background Tasks",
"description": "Agent executes long-running tasks in background threads",
"steps": [
{
"type": "user_message",
"content": "Run the test suite and while it's running, fix the linting errors",
"annotation": "Two tasks that can run concurrently"
},
{
"type": "assistant_text",
"content": "I'll start the tests in the background and fix linting in parallel.",
"annotation": "Agent recognizes opportunity for concurrent execution"
},
{
"type": "tool_call",
"content": "pytest tests/ -v --timeout=300",
"toolName": "background_task",
"annotation": "BackgroundManager spawns a thread for long-running test suite"
},
{
"type": "system_event",
"content": "[BackgroundManager] Thread spawned: task_001\nRunning: pytest tests/ -v --timeout=300\nStatus: in_progress",
"annotation": "Thread is running independently -- main loop continues"
},
{
"type": "tool_result",
"content": "Background task started: task_001",
"toolName": "background_task",
"annotation": "Control returns immediately to the agent"
},
{
"type": "tool_call",
"content": "ruff check --fix .",
"toolName": "bash",
"annotation": "Agent works on linting while tests run in background"
},
{
"type": "tool_result",
"content": "Fixed 12 linting errors in 5 files",
"toolName": "bash",
"annotation": "Linting fixed concurrently with tests"
},
{
"type": "system_event",
"content": "[Notification] Background task_001 completed\nResult: 47 tests passed, 2 failed\nFailed: test_auth.py::test_token_expiry, test_api.py::test_rate_limit",
"annotation": "Background thread notifies main loop via notification queue"
},
{
"type": "assistant_text",
"content": "Linting is fixed. Tests finished: 47 passed, 2 failed. I'll fix the failing tests now.",
"annotation": "Non-blocking execution via threads + queue enables parallel work"
}
]
}

View File

@@ -0,0 +1,44 @@
{
"version": "s09",
"title": "Agent Teams",
"description": "Persistent teammates communicate via JSONL inboxes",
"steps": [
{
"type": "user_message",
"content": "Build a full-stack app: frontend in React, backend in Python, with tests",
"annotation": "Large task requiring team collaboration"
},
{
"type": "tool_call",
"content": "spawn_teammate(\n name: \"frontend-dev\",\n role: \"React specialist\"\n)\nspawn_teammate(\n name: \"backend-dev\",\n role: \"Python API specialist\"\n)",
"toolName": "teammate_manager",
"annotation": "Unlike subagents (s04) that die after one task, teammates persist"
},
{
"type": "system_event",
"content": "[TeammateManager] Team created:\n .team/config.json written\n frontend-dev [idle] - own thread, own agent loop\n backend-dev [idle] - own thread, own agent loop",
"annotation": "Each teammate runs its own while(tool_use) loop in a separate thread"
},
{
"type": "tool_call",
"content": "send_message(\n to: \"backend-dev\",\n content: \"Build the user API: GET/POST /users\"\n)",
"toolName": "send_message",
"annotation": "Message appended to .team/inbox/backend-dev.jsonl"
},
{
"type": "system_event",
"content": "[backend-dev] Checking inbox... 1 new message\n From: lead\n Content: \"Build the user API: GET/POST /users\"\n (inbox drained after read)",
"annotation": "Inbox is read-then-clear: prevents duplicate processing"
},
{
"type": "system_event",
"content": "[backend-dev -> frontend-dev] Reply via inbox:\n \"API ready: GET /users, POST /users, GET /users/:id\"\n Written to .team/inbox/frontend-dev.jsonl",
"annotation": "Teammates message each other through the same JSONL mechanism"
},
{
"type": "assistant_text",
"content": "Team is communicating via file-based inboxes. Each teammate has its own .jsonl file -- append to send, drain to read.",
"annotation": "JSONL inboxes decouple sender timing from receiver timing"
}
]
}

View File

@@ -0,0 +1,38 @@
{
"version": "s10",
"title": "Team Protocols",
"description": "Request-response protocols for shutdown and plan approval",
"steps": [
{
"type": "user_message",
"content": "Work is done. Shut down the team gracefully.",
"annotation": "Can't just kill threads -- need coordinated shutdown"
},
{
"type": "tool_call",
"content": "shutdown_request(\n teammate: \"backend-dev\",\n request_id: \"req_abc123\"\n)",
"toolName": "shutdown_teammate",
"annotation": "request_id correlates the request with the response"
},
{
"type": "system_event",
"content": "[Shutdown FSM] req_abc123: pending\n Lead -> backend-dev inbox:\n { type: \"shutdown_request\", request_id: \"req_abc123\" }",
"annotation": "FSM tracks state: pending -> approved | rejected"
},
{
"type": "system_event",
"content": "[backend-dev] Received shutdown request req_abc123\n Current task: none (idle)\n Decision: approve\n -> lead inbox: { type: \"shutdown_response\", request_id: \"req_abc123\", approve: true }",
"annotation": "Teammate decides whether to approve based on its own state"
},
{
"type": "system_event",
"content": "[Shutdown FSM] req_abc123: approved\n backend-dev thread stopped\n Status: idle -> shutdown",
"annotation": "Same request_id pattern works for plan approval too"
},
{
"type": "assistant_text",
"content": "backend-dev shut down gracefully. The same request_id + FSM pattern handles plan approval: teammate submits plan, lead approves/rejects with the correlated request_id.",
"annotation": "One pattern (request_id correlation), two applications (shutdown + plan approval)"
}
]
}

View File

@@ -0,0 +1,44 @@
{
"version": "s11",
"title": "Autonomous Agents",
"description": "Teammates self-govern with idle cycles and auto-claiming",
"steps": [
{
"type": "user_message",
"content": "Implement the full feature backlog",
"annotation": "Open-ended task for autonomous team execution"
},
{
"type": "tool_call",
"content": "create_tasks([\n { title: \"User authentication\", status: \"open\" },\n { title: \"Dashboard UI\", status: \"open\" },\n { title: \"API rate limiting\", status: \"open\" },\n { title: \"Integration tests\", status: \"open\" }\n])",
"toolName": "task_manager",
"annotation": "Tasks created with no owner -- available for auto-claiming"
},
{
"type": "tool_result",
"content": "4 tasks created on shared board",
"toolName": "task_manager",
"annotation": "Unassigned tasks visible to all teammates"
},
{
"type": "system_event",
"content": "[frontend-dev] Idle cycle triggered\n 1. Check inbox -> 0 messages\n 2. Poll task board -> found unowned task #2 \"Dashboard UI\"\n 3. Auto-claim task #2\n 4. Status: idle -> working",
"annotation": "Idle cycle: check inbox, poll tasks, auto-claim, resume work"
},
{
"type": "system_event",
"content": "[backend-dev] Idle cycle triggered\n 1. Check inbox -> 0 messages\n 2. Poll task board -> found unowned task #1 \"User authentication\"\n 3. Auto-claim task #1\n 4. Status: idle -> working",
"annotation": "Multiple teammates claim different tasks concurrently"
},
{
"type": "system_event",
"content": "[tester] Idle cycle triggered\n 1. Check inbox -> 0 messages\n 2. Poll task board -> task #4 blocked by #1, #2, #3\n 3. No claimable tasks\n 4. Status: idle (will retry in 30s)",
"annotation": "Timeout-based polling prevents busy-waiting"
},
{
"type": "assistant_text",
"content": "Team is self-organizing: frontend-dev claimed Dashboard UI, backend-dev claimed User auth. Tester is waiting for dependencies to clear.",
"annotation": "Polling + timeout makes teammates autonomous -- no micromanagement needed"
}
]
}

View File

@@ -0,0 +1,75 @@
"use client";
import { useState, useEffect } from "react";
export function useDarkMode(): boolean {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const html = document.documentElement;
setIsDark(html.classList.contains("dark"));
const observer = new MutationObserver(() => {
setIsDark(html.classList.contains("dark"));
});
observer.observe(html, { attributes: true, attributeFilter: ["class"] });
return () => observer.disconnect();
}, []);
return isDark;
}
export interface SvgPalette {
nodeFill: string;
nodeStroke: string;
nodeText: string;
activeNodeFill: string;
activeNodeStroke: string;
activeNodeText: string;
endNodeFill: string;
endNodeStroke: string;
edgeStroke: string;
activeEdgeStroke: string;
arrowFill: string;
labelFill: string;
bgSubtle: string;
}
export function useSvgPalette(): SvgPalette {
const isDark = useDarkMode();
if (isDark) {
return {
nodeFill: "#27272a",
nodeStroke: "#3f3f46",
nodeText: "#d4d4d8",
activeNodeFill: "#3b82f6",
activeNodeStroke: "#2563eb",
activeNodeText: "#ffffff",
endNodeFill: "#a855f7",
endNodeStroke: "#9333ea",
edgeStroke: "#52525b",
activeEdgeStroke: "#3b82f6",
arrowFill: "#71717a",
labelFill: "#a1a1aa",
bgSubtle: "#18181b",
};
}
return {
nodeFill: "#e2e8f0",
nodeStroke: "#cbd5e1",
nodeText: "#475569",
activeNodeFill: "#3b82f6",
activeNodeStroke: "#2563eb",
activeNodeText: "#ffffff",
endNodeFill: "#a855f7",
endNodeStroke: "#9333ea",
edgeStroke: "#cbd5e1",
activeEdgeStroke: "#3b82f6",
arrowFill: "#94a3b8",
labelFill: "#94a3b8",
bgSubtle: "#f8fafc",
};
}

View File

@@ -0,0 +1,84 @@
"use client";
import { useState, useCallback, useRef, useEffect } from "react";
import type { SimStep } from "@/types/agent-data";
interface SimulatorState {
currentIndex: number;
isPlaying: boolean;
speed: number;
}
export function useSimulator(steps: SimStep[]) {
const [state, setState] = useState<SimulatorState>({
currentIndex: -1,
isPlaying: false,
speed: 1,
});
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const clearTimer = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const stepForward = useCallback(() => {
setState((prev) => {
if (prev.currentIndex >= steps.length - 1) {
return { ...prev, isPlaying: false };
}
return { ...prev, currentIndex: prev.currentIndex + 1 };
});
}, [steps.length]);
const play = useCallback(() => {
setState((prev) => {
if (prev.currentIndex >= steps.length - 1) {
return prev;
}
return { ...prev, isPlaying: true };
});
}, [steps.length]);
const pause = useCallback(() => {
clearTimer();
setState((prev) => ({ ...prev, isPlaying: false }));
}, [clearTimer]);
const reset = useCallback(() => {
clearTimer();
setState({ currentIndex: -1, isPlaying: false, speed: state.speed });
}, [clearTimer, state.speed]);
const setSpeed = useCallback((speed: number) => {
setState((prev) => ({ ...prev, speed }));
}, []);
useEffect(() => {
if (state.isPlaying && state.currentIndex < steps.length - 1) {
const delay = 1200 / state.speed;
timerRef.current = setTimeout(() => {
stepForward();
}, delay);
} else if (state.isPlaying && state.currentIndex >= steps.length - 1) {
setState((prev) => ({ ...prev, isPlaying: false }));
}
return () => clearTimer();
}, [state.isPlaying, state.currentIndex, state.speed, steps.length, stepForward, clearTimer]);
return {
currentIndex: state.currentIndex,
isPlaying: state.isPlaying,
speed: state.speed,
visibleSteps: steps.slice(0, state.currentIndex + 1),
totalSteps: steps.length,
isComplete: state.currentIndex >= steps.length - 1,
play,
pause,
stepForward,
reset,
setSpeed,
};
}

View File

@@ -0,0 +1,84 @@
"use client";
import { useState, useCallback, useEffect, useRef } from "react";
interface SteppedVisualizationOptions {
totalSteps: number;
autoPlayInterval?: number; // ms, default 2000
}
interface SteppedVisualizationReturn {
currentStep: number;
totalSteps: number;
next: () => void;
prev: () => void;
reset: () => void;
goToStep: (step: number) => void;
isPlaying: boolean;
toggleAutoPlay: () => void;
isFirstStep: boolean;
isLastStep: boolean;
}
export function useSteppedVisualization({
totalSteps,
autoPlayInterval = 2000,
}: SteppedVisualizationOptions): SteppedVisualizationReturn {
const [currentStep, setCurrentStep] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const next = useCallback(() => {
setCurrentStep((prev) => Math.min(prev + 1, totalSteps - 1));
}, [totalSteps]);
const prev = useCallback(() => {
setCurrentStep((prev) => Math.max(prev - 1, 0));
}, []);
const reset = useCallback(() => {
setCurrentStep(0);
setIsPlaying(false);
}, []);
const goToStep = useCallback(
(step: number) => {
setCurrentStep(Math.max(0, Math.min(step, totalSteps - 1)));
},
[totalSteps]
);
const toggleAutoPlay = useCallback(() => {
setIsPlaying((prev) => !prev);
}, []);
useEffect(() => {
if (isPlaying) {
intervalRef.current = setInterval(() => {
setCurrentStep((prev) => {
if (prev >= totalSteps - 1) {
setIsPlaying(false);
return prev;
}
return prev + 1;
});
}, autoPlayInterval);
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [isPlaying, totalSteps, autoPlayInterval]);
return {
currentStep,
totalSteps,
next,
prev,
reset,
goToStep,
isPlaying,
toggleAutoPlay,
isFirstStep: currentStep === 0,
isLastStep: currentStep === totalSteps - 1,
};
}

View File

@@ -0,0 +1,74 @@
{
"meta": { "title": "Learn Claude Code", "description": "Build an AI coding agent from scratch, one concept at a time" },
"nav": { "home": "Home", "timeline": "Timeline", "compare": "Compare", "layers": "Layers", "github": "GitHub" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "Build an AI coding agent from scratch, one concept at a time", "start": "Start Learning", "core_pattern": "The Core Pattern", "core_pattern_desc": "Every AI coding agent shares the same loop: call the model, execute tools, feed results back. Everything else is details.", "learning_path": "Learning Path", "learning_path_desc": "11 progressive sessions, from a simple loop to full autonomous teams", "layers_title": "Architectural Layers", "layers_desc": "Five orthogonal concerns that compose into a complete agent", "loc": "LOC", "learn_more": "Learn More", "versions_in_layer": "versions", "message_flow": "Message Growth", "message_flow_desc": "Watch the messages array grow as the agent loop executes" },
"version": { "loc": "lines of code", "tools": "tools", "new": "New", "prev": "Previous", "next": "Next", "view_source": "View Source", "view_diff": "View Diff", "design_decisions": "Design Decisions", "whats_new": "What's New", "tutorial": "Tutorial", "simulator": "Agent Loop Simulator", "execution_flow": "Execution Flow", "architecture": "Architecture", "concept_viz": "Concept Visualization", "alternatives": "Alternatives Considered", "tab_learn": "Learn", "tab_simulate": "Simulate", "tab_code": "Code", "tab_deep_dive": "Deep Dive" },
"sim": { "play": "Play", "pause": "Pause", "step": "Step", "reset": "Reset", "speed": "Speed", "step_of": "of" },
"timeline": { "title": "Learning Path", "subtitle": "s01 to s11: Progressive Agent Design", "layer_legend": "Layer Legend", "loc_growth": "LOC Growth", "learn_more": "Learn More" },
"layers": {
"title": "Architectural Layers",
"subtitle": "Five orthogonal concerns that compose into a complete agent",
"tools": "What the agent CAN do. The foundation: tools give the model capabilities to interact with the world.",
"planning": "How work is organized. From simple todo lists to dependency-aware task boards shared across agents.",
"memory": "Keeping context within limits. Compression strategies that let agents work infinitely without losing coherence.",
"concurrency": "Non-blocking execution. Background threads and notification buses for parallel work.",
"collaboration": "Multi-agent coordination. Teams, messaging, and autonomous teammates that think for themselves."
},
"compare": {
"title": "Compare Versions",
"subtitle": "See what changed between any two versions",
"select_a": "Version A",
"select_b": "Version B",
"loc_delta": "LOC Delta",
"lines": "lines",
"new_tools_in_b": "New Tools in B",
"new_classes_in_b": "New Classes in B",
"new_functions_in_b": "New Functions in B",
"tool_comparison": "Tool Comparison",
"only_in": "Only in",
"shared": "Shared",
"none": "None",
"source_diff": "Source Code Diff",
"empty_hint": "Select two versions above to compare them.",
"architecture": "Architecture"
},
"diff": {
"new_classes": "New Classes",
"new_tools": "New Tools",
"new_functions": "New Functions",
"loc_delta": "LOC Delta"
},
"sessions": {
"s01": "The Agent Loop",
"s02": "Tools",
"s03": "TodoWrite",
"s04": "Subagents",
"s05": "Skills",
"s06": "Compact",
"s07": "Tasks",
"s08": "Background Tasks",
"s09": "Agent Teams",
"s10": "Team Protocols",
"s11": "Autonomous Agents"
},
"layer_labels": {
"tools": "Tools & Execution",
"planning": "Planning & Coordination",
"memory": "Memory Management",
"concurrency": "Concurrency",
"collaboration": "Collaboration"
},
"viz": {
"s01": "The Agent While-Loop",
"s02": "Tool Dispatch Map",
"s03": "TodoWrite Nag System",
"s04": "Subagent Context Isolation",
"s05": "On-Demand Skill Loading",
"s06": "Three-Layer Context Compression",
"s07": "Task Dependency Graph",
"s08": "Background Task Lanes",
"s09": "Agent Team Mailboxes",
"s10": "FSM Team Protocols",
"s11": "Autonomous Agent Cycle"
}
}

View File

@@ -0,0 +1,74 @@
{
"meta": { "title": "Learn Claude Code", "description": "AIコーディングエージェントをゼロから構築、一つずつ概念を追加" },
"nav": { "home": "ホーム", "timeline": "学習パス", "compare": "バージョン比較", "layers": "アーキテクチャ層", "github": "GitHub" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "AIコーディングエージェントをゼロから構築、一つずつ概念を追加", "start": "学習を始める", "core_pattern": "コアパターン", "core_pattern_desc": "全てのAIコーディングエージェントは同じループを共有モデルを呼び出し、ツールを実行し、結果をフィードバック。他は全て詳細。", "learning_path": "学習パス", "learning_path_desc": "11の段階的セッション、シンプルなループから完全自律チームまで", "layers_title": "アーキテクチャ層", "layers_desc": "5つの直交する関心事が完全なエージェントを構成", "loc": "行", "learn_more": "詳細を見る", "versions_in_layer": "バージョン", "message_flow": "メッセージの増加", "message_flow_desc": "エージェントループ実行時のメッセージ配列の成長を観察" },
"version": { "loc": "行のコード", "tools": "ツール", "new": "新規", "prev": "前のバージョン", "next": "次のバージョン", "view_source": "ソースを見る", "view_diff": "差分を見る", "design_decisions": "設計判断", "whats_new": "新機能", "tutorial": "チュートリアル", "simulator": "エージェントループシミュレーター", "execution_flow": "実行フロー", "architecture": "アーキテクチャ", "concept_viz": "コンセプト可視化", "alternatives": "検討された代替案", "tab_learn": "学習", "tab_simulate": "シミュレーション", "tab_code": "ソースコード", "tab_deep_dive": "詳細分析" },
"sim": { "play": "再生", "pause": "一時停止", "step": "ステップ", "reset": "リセット", "speed": "速度", "step_of": "/" },
"timeline": { "title": "学習パス", "subtitle": "s01からs11へ段階的エージェント設計", "layer_legend": "レイヤー凡例", "loc_growth": "コード量の推移", "learn_more": "詳細を見る" },
"layers": {
"title": "アーキテクチャ層",
"subtitle": "5つの直交する関心事が完全なエージェントを構成",
"tools": "エージェントができること。基盤:ツールがモデルに外部世界と対話する能力を与える。",
"planning": "作業の組織化。シンプルなToDoリストからエージェント間で共有される依存関係対応タスクボードまで。",
"memory": "コンテキスト制限内での記憶保持。圧縮戦略によりエージェントが一貫性を失わずに無限に作業可能。",
"concurrency": "ノンブロッキング実行。バックグラウンドスレッドと通知バスによる並列作業。",
"collaboration": "マルチエージェント連携。チーム、メッセージング、自律的に考えるチームメイト。"
},
"compare": {
"title": "バージョン比較",
"subtitle": "任意の2つのバージョン間の変更を確認",
"select_a": "バージョンA",
"select_b": "バージョンB",
"loc_delta": "コード量の差分",
"lines": "行",
"new_tools_in_b": "Bの新規ツール",
"new_classes_in_b": "Bの新規クラス",
"new_functions_in_b": "Bの新規関数",
"tool_comparison": "ツール比較",
"only_in": "のみ",
"shared": "共通",
"none": "なし",
"source_diff": "ソースコード差分",
"empty_hint": "上で2つのバージョンを選択して比較してください。",
"architecture": "アーキテクチャ"
},
"diff": {
"new_classes": "新規クラス",
"new_tools": "新規ツール",
"new_functions": "新規関数",
"loc_delta": "コード量の差分"
},
"sessions": {
"s01": "エージェントループ",
"s02": "ツール",
"s03": "TodoWrite",
"s04": "サブエージェント",
"s05": "スキル",
"s06": "コンテキスト圧縮",
"s07": "タスクシステム",
"s08": "バックグラウンドタスク",
"s09": "エージェントチーム",
"s10": "チームプロトコル",
"s11": "自律エージェント"
},
"layer_labels": {
"tools": "ツールと実行",
"planning": "計画と調整",
"memory": "メモリ管理",
"concurrency": "並行処理",
"collaboration": "コラボレーション"
},
"viz": {
"s01": "エージェント Whileループ",
"s02": "ツールディスパッチマップ",
"s03": "TodoWrite リマインドシステム",
"s04": "サブエージェント コンテキスト分離",
"s05": "オンデマンド スキルローディング",
"s06": "3層コンテキスト圧縮",
"s07": "タスク依存関係グラフ",
"s08": "バックグラウンドタスクレーン",
"s09": "エージェントチーム メールボックス",
"s10": "FSM チームプロトコル",
"s11": "自律エージェントサイクル"
}
}

View File

@@ -0,0 +1,74 @@
{
"meta": { "title": "Learn Claude Code", "description": "从零构建 AI 编程 Agent每次只加一个概念" },
"nav": { "home": "首页", "timeline": "学习路径", "compare": "版本对比", "layers": "架构层", "github": "GitHub" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "从零构建 AI 编程 Agent每次只加一个概念", "start": "开始学习", "core_pattern": "核心模式", "core_pattern_desc": "所有 AI 编程 Agent 共享同一个循环:调用模型、执行工具、回传结果。其他都是细节。", "learning_path": "学习路径", "learning_path_desc": "11 个渐进式课程,从简单循环到完整自主团队", "layers_title": "架构层次", "layers_desc": "五个正交关注点组合成完整的 Agent", "loc": "行", "learn_more": "了解更多", "versions_in_layer": "个版本", "message_flow": "消息增长", "message_flow_desc": "观察 Agent 循环执行时消息数组的增长" },
"version": { "loc": "行代码", "tools": "个工具", "new": "新增", "prev": "上一版", "next": "下一版", "view_source": "查看源码", "view_diff": "查看变更", "design_decisions": "设计决策", "whats_new": "新增内容", "tutorial": "教程", "simulator": "Agent 循环模拟器", "execution_flow": "执行流程", "architecture": "架构", "concept_viz": "概念可视化", "alternatives": "替代方案", "tab_learn": "学习", "tab_simulate": "模拟", "tab_code": "源码", "tab_deep_dive": "深入探索" },
"sim": { "play": "播放", "pause": "暂停", "step": "单步", "reset": "重置", "speed": "速度", "step_of": "/" },
"timeline": { "title": "学习路径", "subtitle": "s01 到 s11渐进式 Agent 设计", "layer_legend": "层次图例", "loc_growth": "代码量增长", "learn_more": "了解更多" },
"layers": {
"title": "架构层次",
"subtitle": "五个正交关注点组合成完整的 Agent",
"tools": "Agent 能做什么。基础层:工具赋予模型与外部世界交互的能力。",
"planning": "如何组织工作。从简单的待办列表到跨 Agent 共享的依赖感知任务板。",
"memory": "在上下文限制内保持记忆。压缩策略让 Agent 可以无限工作而不失去连贯性。",
"concurrency": "非阻塞执行。后台线程和通知总线实现并行工作。",
"collaboration": "多 Agent 协作。团队、消息传递和能独立思考的自主队友。"
},
"compare": {
"title": "版本对比",
"subtitle": "查看任意两个版本之间的变化",
"select_a": "版本 A",
"select_b": "版本 B",
"loc_delta": "代码量差异",
"lines": "行",
"new_tools_in_b": "B 中新增工具",
"new_classes_in_b": "B 中新增类",
"new_functions_in_b": "B 中新增函数",
"tool_comparison": "工具对比",
"only_in": "仅在",
"shared": "共有",
"none": "无",
"source_diff": "源码差异",
"empty_hint": "请在上方选择两个版本进行对比。",
"architecture": "架构"
},
"diff": {
"new_classes": "新增类",
"new_tools": "新增工具",
"new_functions": "新增函数",
"loc_delta": "代码量差异"
},
"sessions": {
"s01": "Agent 循环",
"s02": "工具",
"s03": "TodoWrite",
"s04": "子 Agent",
"s05": "技能",
"s06": "上下文压缩",
"s07": "任务系统",
"s08": "后台任务",
"s09": "Agent 团队",
"s10": "团队协议",
"s11": "自主 Agent"
},
"layer_labels": {
"tools": "工具与执行",
"planning": "规划与协调",
"memory": "内存管理",
"concurrency": "并发",
"collaboration": "协作"
},
"viz": {
"s01": "Agent While 循环",
"s02": "工具分发映射",
"s03": "TodoWrite 提醒系统",
"s04": "子 Agent 上下文隔离",
"s05": "按需技能加载",
"s06": "三层上下文压缩",
"s07": "任务依赖图",
"s08": "后台任务通道",
"s09": "Agent 团队邮箱",
"s10": "FSM 团队协议",
"s11": "自主 Agent 循环"
}
}

36
web/src/lib/constants.ts Normal file
View File

@@ -0,0 +1,36 @@
export const VERSION_ORDER = [
"s01", "s02", "s03", "s04", "s05", "s06", "s07", "s08", "s09", "s10", "s11"
] as const;
export const LEARNING_PATH = VERSION_ORDER;
export type VersionId = typeof LEARNING_PATH[number];
export const VERSION_META: Record<string, {
title: string;
subtitle: string;
coreAddition: string;
keyInsight: string;
layer: "tools" | "planning" | "memory" | "concurrency" | "collaboration";
prevVersion: string | null;
}> = {
s01: { title: "The Agent Loop", subtitle: "Bash is All You Need", coreAddition: "Single-tool agent loop", keyInsight: "The entire agent is a while loop + one tool", layer: "tools", prevVersion: null },
s02: { title: "Tools", subtitle: "The Loop Didn't Change", coreAddition: "Tool dispatch map", keyInsight: "Adding tools means adding handlers, the loop stays the same", layer: "tools", prevVersion: "s01" },
s03: { title: "TodoWrite", subtitle: "Plan Before You Act", coreAddition: "TodoManager + nag reminder", keyInsight: "Visible plans improve task completion and accountability", layer: "planning", prevVersion: "s02" },
s04: { title: "Subagents", subtitle: "Fresh Context via Task Tool", coreAddition: "Subagent spawn with isolated messages[]", keyInsight: "Process isolation = context isolation", layer: "planning", prevVersion: "s03" },
s05: { title: "Skills", subtitle: "SKILL.md + tool_result Injection", coreAddition: "SkillLoader + two-layer injection", keyInsight: "Skills inject via tool_result, not system prompt", layer: "planning", prevVersion: "s04" },
s06: { title: "Compact", subtitle: "Strategic Forgetting", coreAddition: "micro-compact + auto-compact + archival", keyInsight: "Forgetting old context enables infinite-length sessions", layer: "memory", prevVersion: "s05" },
s07: { title: "Tasks", subtitle: "Persistent CRUD with Dependencies", coreAddition: "TaskManager with file-based state + dependency graph", keyInsight: "File-based state survives context compression", layer: "planning", prevVersion: "s06" },
s08: { title: "Background Tasks", subtitle: "Fire and Forget", coreAddition: "BackgroundManager + notification queue", keyInsight: "Non-blocking daemon threads + notification queue", layer: "concurrency", prevVersion: "s07" },
s09: { title: "Agent Teams", subtitle: "Teammates + Mailboxes", coreAddition: "TeammateManager + file-based mailbox", keyInsight: "Persistent teammates with async mailbox inboxes", layer: "collaboration", prevVersion: "s08" },
s10: { title: "Team Protocols", subtitle: "Shutdown + Plan Approval", coreAddition: "request_id correlation for two protocols", keyInsight: "Same request-response pattern, two applications", layer: "collaboration", prevVersion: "s09" },
s11: { title: "Autonomous Agents", subtitle: "Idle Cycle + Auto-Claim", coreAddition: "Task board polling + timeout-based self-governance", keyInsight: "Polling + timeout makes teammates self-organizing", layer: "collaboration", prevVersion: "s10" },
};
export const LAYERS = [
{ id: "tools" as const, label: "Tools & Execution", color: "#3B82F6", versions: ["s01", "s02"] },
{ id: "planning" as const, label: "Planning & Coordination", color: "#10B981", versions: ["s03", "s04", "s05", "s07"] },
{ id: "memory" as const, label: "Memory Management", color: "#8B5CF6", versions: ["s06"] },
{ id: "concurrency" as const, label: "Concurrency", color: "#F59E0B", versions: ["s08"] },
{ id: "collaboration" as const, label: "Collaboration", color: "#EF4444", versions: ["s09", "s10", "s11"] },
] as const;

View File

@@ -0,0 +1,16 @@
import en from "@/i18n/messages/en.json";
import zh from "@/i18n/messages/zh.json";
import ja from "@/i18n/messages/ja.json";
type Messages = typeof en;
const messagesMap: Record<string, Messages> = { en, zh, ja };
export function getTranslations(locale: string, namespace: string) {
const messages = messagesMap[locale] || en;
const ns = (messages as Record<string, Record<string, string>>)[namespace];
const fallbackNs = (en as Record<string, Record<string, string>>)[namespace];
return (key: string): string => {
return ns?.[key] || fallbackNs?.[key] || key;
};
}

36
web/src/lib/i18n.tsx Normal file
View File

@@ -0,0 +1,36 @@
"use client";
import { createContext, useContext, ReactNode } from "react";
import en from "@/i18n/messages/en.json";
import zh from "@/i18n/messages/zh.json";
import ja from "@/i18n/messages/ja.json";
type Messages = typeof en;
const messagesMap: Record<string, Messages> = { en, zh, ja };
const I18nContext = createContext<{ locale: string; messages: Messages }>({
locale: "en",
messages: en,
});
export function I18nProvider({ locale, children }: { locale: string; children: ReactNode }) {
const messages = messagesMap[locale] || en;
return (
<I18nContext.Provider value={{ locale, messages }}>
{children}
</I18nContext.Provider>
);
}
export function useTranslations(namespace?: string) {
const { messages } = useContext(I18nContext);
return (key: string) => {
const ns = namespace ? (messages as any)[namespace] : messages;
if (!ns) return key;
return (ns as any)[key] || key;
};
}
export function useLocale() {
return useContext(I18nContext).locale;
}

3
web/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,3 @@
export function cn(...classes: (string | undefined | null | false)[]) {
return classes.filter(Boolean).join(" ");
}

View File

@@ -0,0 +1,72 @@
export interface AgentVersion {
id: string;
filename: string;
title: string;
subtitle: string;
loc: number;
tools: string[];
newTools: string[];
coreAddition: string;
keyInsight: string;
classes: { name: string; startLine: number; endLine: number }[];
functions: { name: string; signature: string; startLine: number }[];
layer: "tools" | "planning" | "memory" | "concurrency" | "collaboration";
source: string;
}
export interface VersionDiff {
from: string;
to: string;
newClasses: string[];
newFunctions: string[];
newTools: string[];
locDelta: number;
}
export interface DocContent {
version: string;
locale: "en" | "zh" | "ja";
title: string;
content: string; // raw markdown
}
export interface VersionIndex {
versions: AgentVersion[];
diffs: VersionDiff[];
}
export type SimStepType =
| "user_message"
| "assistant_text"
| "tool_call"
| "tool_result"
| "system_event";
export interface SimStep {
type: SimStepType;
content: string;
annotation: string;
toolName?: string;
toolInput?: string;
}
export interface Scenario {
version: string;
title: string;
description: string;
steps: SimStep[];
}
export interface FlowNode {
id: string;
label: string;
type: "start" | "process" | "decision" | "subprocess" | "end";
x: number;
y: number;
}
export interface FlowEdge {
from: string;
to: string;
label?: string;
}