Adnan Ahmad
June 14, 20269 min read

Designing Agent-Native Systems: A Deep Dive into StudyPal

The transition from traditional LLM applications to agent-native systems represents a fundamental shift: from static prompt-response templates to autonomous, goal-driven execution loops. StudyPal is our case study for what that looks like in practice — not just a smarter chatbot, but a full personalized tutoring platform where AI agents plan, execute, and observe across multi-step pipelines.

This article takes you under the hood: the real interfaces, the actual module structure, and the design decisions that make the system composable and observable at scale.


See It in Action


The Foundation: HKU DeepTutor's Two-Layer Architecture

StudyPal is built on the open-source HKU DeepTutor framework. Rather than flooding the LLM with every available tool upfront, it separates concerns into two clean layers:

[Web UI / CLI / SDK]
         │
         ▼
  [ChatOrchestrator]         ← runtime/orchestrator.py
         │ routes via context.active_capability
         ├──► [CapabilityRegistry]
         │         │
         │    [BaseCapability.run(UnifiedContext, StreamBus)]
         │
         └──► [ToolRegistry]  ← shared across all modes
                  │
            [BaseTool.execute(**kwargs) → ToolResult]

Level 1 (ToolRegistry) — Lightweight, stateless, single-function tools the LLM calls on-demand during any conversation turn.

Level 2 (CapabilityRegistry) — Multi-step, stateful agent pipelines that take control of the full execution loop when the user activates a deep mode.


Level 1: The Real Tool Protocol

Every tool — built-in or contributed — implements BaseTool from deeptutor/core/tool_protocol.py. The real interface uses typed dataclasses for schema generation, not simple abstract properties:

# deeptutor/core/tool_protocol.py
class BaseTool(ABC):
    @abstractmethod
    def get_definition(self) -> ToolDefinition:
        """Return the tool's metadata & parameter schema."""
        ...

    @abstractmethod
    async def execute(self, **kwargs: Any) -> ToolResult:
        """Run the tool and return a typed result."""
        ...

    @property
    def name(self) -> str:
        return self.get_definition().name

ToolDefinition carries the OpenAI function-calling schema (built from ToolParameter dataclasses), while ToolResult is a standardised return type — not a raw string — carrying content, sources, metadata, and a success flag:

@dataclass
class ToolResult:
    content: str = ""
    sources: list[dict[str, Any]] = field(
        default_factory=list
    )
    metadata: dict[str, Any] = field(
        default_factory=dict
    )
    success: bool = True

Adding a new tool means implementing one class. The ToolRegistry handles discovery, OpenAI schema generation, and aliased execution — one call surface for all consumers.

The 8 real built-in tools:

ToolPurpose
ragVector KB retrieval via LlamaIndex
web_searchLive web search
code_executionSandboxed Python runner
reasonChain-of-thought reasoning
brainstormIdea generation and expansion
paper_searcharXiv / academic paper retrieval
geogebra_analysisInteractive math graph analysis
tex_chunkerLaTeX paper parsing and chunking

Level 2: The Real Capability Protocol

When a task requires planning, multiple tool calls, validation, and synthesis, a stateless tool is not enough. That is where Capabilities come in. Each one declares a CapabilityManifest at the class level and implements a single run method:

# deeptutor/core/capability_protocol.py
class BaseCapability(ABC):
    manifest: CapabilityManifest  # declared on subclass

    @abstractmethod
    async def run(
        self,
        context: UnifiedContext,
        stream: StreamBus,
    ) -> None: ...

    @property
    def name(self) -> str:
        return self.manifest.name

The manifest drives the CLI (via cli_aliases), tool selection (via tools_used), and the observable stage lifecycle (via stages). Here is the real DeepSolveCapability:

# deeptutor/capabilities/deep_solve.py
class DeepSolveCapability(BaseCapability):
    manifest = CapabilityManifest(
        name="deep_solve",
        description="Multi-agent solving (Plan → ReAct → Write).",
        stages=["planning", "reasoning", "writing"],
        tools_used=[
            "rag", "web_search",
            "code_execution", "reason",
        ],
        cli_aliases=["solve"],
    )

    async def run(
        self, context: UnifiedContext, stream: StreamBus
    ) -> None:
        solver = MainSolver(...)
        await solver.ainit()
        # _trace_bridge routes solver events to stream bus
        result = await solver.solve(
            question=context.user_message,
        )
        await stream.result(
            {"response": result["final_answer"]},
            source=self.name,
        )

All five built-in capabilities and their pipeline stages:

CapabilityCLI AliasPipeline Stages
chatchatthinking → acting → observing → responding
deep_solvesolveplanning → reasoning → writing
deep_questionquizideation → generation
deep_researchresearchrephrasing → decomposing → researching → reporting
math_animatoranimateconcept_analysis → design → code_generation → render

The StreamBus: Making Execution Observable

StreamBus (deeptutor/core/stream_bus.py) is the async fan-out event bus that connects producers (capabilities and tools) to consumers (CLI renderer, WebSocket pusher, SDK). It is what makes any capability immediately usable across all surfaces without modification.

Every emit call is typed — no raw strings:

# Inside any capability or tool
await stream.thinking(
    "Analysing the problem...", source=self.name
)
await stream.tool_call(
    "rag", args={"query": q}, source=self.name
)
await stream.tool_result(
    "rag", result=text, source=self.name
)
await stream.content(final_answer, source=self.name)

The stage() context manager automatically emits STAGE_START and STAGE_END events, giving consumers a structured view of where execution is in the pipeline:

async with stream.stage("planning", source=self.name):
    plan = await self._plan(context)
    await stream.thinking(plan, source=self.name)
# → STAGE_START and STAGE_END emitted automatically

This is what powers live progress indicators in the UI — and what lets the CLI and WebSocket consumers share the exact same capability code.


The ChatOrchestrator: One Entry Point for Everything

ChatOrchestrator (deeptutor/runtime/orchestrator.py) is the single routing layer. CLI, WebSocket, and SDK all call handle(context) and receive the same StreamEvent stream:

# deeptutor/runtime/orchestrator.py
class ChatOrchestrator:
    async def handle(
        self, context: UnifiedContext
    ) -> AsyncIterator[StreamEvent]:
        cap_name = context.active_capability or "chat"
        capability = self._cap_registry.get(cap_name)
        bus = StreamBus()
        asyncio.create_task(
            capability.run(context, bus)
        )
        async for event in bus.subscribe():
            yield event  # same contract for all consumers

Set context.active_capability to None and you get plain chat. Set it to "deep_solve" and the same call routes to a three-stage multi-agent solver. The consumer never changes — only the capability does.


Beyond the Architecture: The Workspace Tools Suite

This is where the story gets personal. On top of the DeepTutor foundation, I built a suite of nine interactive learning tools — each deeply integrated with StudyPal's persistent memory and your document knowledge base. Not demos: production-ready features designed for how students actually learn.

🎙️ Voice Assistant

Speak directly to your TutorBot in real-time. Built on Vocal Bridge, it features an immersive full-screen audio session UI, live streaming text transcript, low-latency audio responses, and fully customizable voice agent profiles. Every voice session shares the same conversation memory as your text sessions — no context is lost when switching modes.

📅 Adaptive Study Planner

An interactive learning calendar driven by CopilotKit sidebar agents. It doesn't just let you plan — it plans with you. Based on your topics and deadlines, it automatically schedules focused study blocks. When you miss a session or a deadline shifts, it dynamically re-balances your remaining calendar. Reactive planning, not static scheduling.

⏱️ Focus Mode & Ambient Sounds

Distraction-free deep work sessions with a Pomodoro countdown timer (Focus, Short Break, and Long Break presets), integrated with a minimal tasks panel. Pair it with ambient soundscapes — Lofi Study, Rain, Forest/Birds, or White Noise — with per-track volume control. A small feature with an outsized impact on actually sitting down and doing the work.

🎨 Interactive Whiteboard

An infinite digital canvas powered by an embedded draw.io iframe, controlled via the postMessage API. The AI sidebar panel lets you toggle RAG and web search to auto-generate or refine diagrams from a natural language description. Supports graphing interactive GeoGebra curves directly on the canvas — useful for anything from system architecture diagrams to calculus visualisations.

🕸️ Semantic Mindmap

Upload notes or paste text and watch them transform into an interactive node graph. Driven by CopilotKit, you can query the AI tutor to expand a node, restructure the hierarchy, or trace relationships between concepts — all without leaving the visual canvas. The mindmap stays in sync with your actual uploaded documents, not generic topic clusters.

🎧 Podcast Generator (Audio Overviews)

Turn dense readings, notebooks, or custom topics into conversational audio. The generator produces dual-host discussion scripts between AI speakers Sarah and Alex, then synthesizes them to high-quality audio files using a Kokoro TTS pipeline. Useful for commutes, review sessions, or simply processing complex material in a different modality.

📊 Study Decks (Presenter)

Convert research papers, notes, or learning topics into clean, structured PowerPoint presentations (.pptx). The pipeline generates outline slides, fills in bulleted key concepts, and packages the deck with StudyPal design templates — ready to download or present. Particularly useful when you need to explain a topic to someone else.

🃏 Flashcards

Generate self-study card decks from any chat session. Supports Q/A and Cloze deletion formats with built-in LaTeX math and code rendering. Flip cards with the spacebar, self-grade with Again or Knew it, and let spaced-repetition logic surface the cards that need the most work. Decks are grounded in your actual documents — not generic definitions.

📝 Exam Simulator

Test your real readiness under pressure. The simulator generates a mix of Multiple Choice, Short Answer, and Long Answer questions grounded in the specific documents or topics you choose. It enforces a strict countdown timer with auto-submission — no pausing, no peeking. After submission, a comprehensive AI grading pass uses customised rubrics to score your answers and return detailed feedback, not just a number.


Why the Separation of Concerns Matters

The BaseTool / BaseCapability / StreamBus triad is what makes StudyPal composable and extensible without becoming a maintenance nightmare. Adding a new capability means implementing one BaseCapability subclass and declaring a CapabilityManifest. Adding a new tool means implementing one BaseTool subclass with a get_definition().

Every workspace tool above — regardless of its complexity — either maps to a BaseCapability pipeline using the StreamBus for observability, or uses the ToolRegistry to inject RAG, web search, or code execution into its session context. The same abstractions, composed differently.

# Deep problem solving
deeptutor solve "Prove that √2 is irrational"

# Generate a quiz grounded in your knowledge base
deeptutor quiz --kb my_notes "Chapter 4: Thermodynamics"

# Launch a deep research report
deeptutor research "Attention mechanisms in modern LLMs"

Agent-native software is about managing complexity and state at scale. By keeping the interface clean, making execution observable through a typed event bus, and strictly separating stateless tools from stateful pipelines, we build systems that don't get lost in infinite loops — and students who actually learn.