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,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>
);
}