一键导入
code-puppy-agent
Deep reference on Code Puppy's internal architecture — agents, tools, plugins, models, MCP, sessions, and how to extend it correctly.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Deep reference on Code Puppy's internal architecture — agents, tools, plugins, models, MCP, sessions, and how to extend it correctly.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | code-puppy-agent |
| description | Deep reference on Code Puppy's internal architecture — agents, tools, plugins, models, MCP, sessions, and how to extend it correctly. |
| version | 1.0 |
| author | code-puppy |
| tags | ["code-puppy","architecture","internals","plugins","reference"] |
This skill is a self-awareness document. When activated it teaches the LLM how Code Puppy is structured under the hood so it can navigate the codebase, debug issues, and extend the product without guessing.
Philosophy: Code Puppy is plugin-first. Nearly all new functionality should be a plugin under
code_puppy/plugins/that hooks into core viacode_puppy/callbacks.py. Don't editcode_puppy/command_line/or core agent files unless a hook genuinely doesn't exist.
┌──────────────────────────────────────────────────┐
│ TUI / CLI (command_line/, tui/) │
│ user input → slash commands → agent dispatch │
├──────────────────────────────────────────────────┤
│ Agent Layer (agents/) │
│ BaseAgent → system prompt, tools, history │
│ _builder, _runtime, _compaction, _history │
├──────────────────────────────────────────────────┤
│ Tool Layer (tools/) │
│ TOOL_REGISTRY → register_tools_for_agent() │
├──────────────────────────────────────────────────┤
│ Plugin Layer (plugins/, callbacks.py) │
│ register_callback("hook", fn) at import time │
├──────────────────────────────────────────────────┤
│ Model Layer (model_factory.py, config.py) │
│ ModelFactory → pydantic-ai Model objects │
├──────────────────────────────────────────────────┤
│ pydantic-ai (external) │
│ Agent.run(), streaming, tool schemas │
└──────────────────────────────────────────────────┘
The TUI collects user input. It delegates to the agent manager which
loads the current agent. The agent builds a pydantic-ai Agent with a
system prompt + tool set, then run_with_mcp() streams the LLM response.
Plugins hook into every stage via callbacks.
agents/base_agent.py)All agents inherit from BaseAgent (ABC). Key interface:
| Member | Purpose |
|---|---|
name (abstract property) | Stable machine identifier, e.g. "code-puppy" |
display_name (abstract property) | Human name with emoji |
description (abstract property) | One-line summary |
get_system_prompt() (abstract) | The authored system prompt (identity is appended separately at runtime) |
get_available_tools() (abstract) | List of tool-name strings from TOOL_REGISTRY |
get_model_name() | Effective model: runtime override > pinned > global |
get_full_system_prompt() | Authored prompt + load_prompt plugin fragments + identity ID |
BaseAgent is a thin conductor. Real logic lives in sibling modules:
_builder.py — builds the pydantic-ai Agent, wires MCP toolsets_runtime.py — run_with_mcp() orchestration, cancellation, retries_history.py — token estimation, hashing, orphan pruning_compaction.py — summarization/truncation when context overflowsPython agents — classes in agents/ discovered automatically by
agent_manager._discover_agents() via pkgutil.iter_modules. Any
BaseAgent subclass (excluding JSONAgent itself) in a non-underscore
module gets registered.
JSON agents — JSONAgent loads from ~/.code_puppy/agents/*.json
(user) and <CWD>/.code_puppy/agents/*.json (project). Project overrides
user on name collision. Required fields: name, description,
system_prompt, tools. Optional: display_name, user_prompt,
tools_config, model, mcp_servers.
Plugin agents — registered via the register_agents callback, which
returns [{"name": "...", "class": SomeAgentClass}] (or
{"name": ..., "json_path": ".../agent.json"}).
Discovery & precedence — _discover_agents() runs three phases:
pkgutil scan of agents/).discover_json_agents(): the user dir is scanned
first, then the project dir overwrites it on collision, so project
wins over user. Both are skipped if a builtin Python agent already owns
the name (builtin beats JSON).on_register_agents() — these overwrite
_AGENT_REGISTRY unconditionally, so a plugin agent can shadow a
builtin. Register under a unique name.Net precedence: builtin Python > JSON project > JSON user; plugin agents are last-writer-wins (handled after the others, no collision guard).
agents/agent_manager.py)get_current_agent() — returns the active BaseAgent instanceset_current_agent(name) — switches agent, preserves historyget_available_agents() — dict of name → display_nameclone_agent() — creates a copy of the current agentAgent selection persists per terminal session (keyed by PPID) in
terminal_sessions.json so different terminals can run different agents.
tools/__init__.py)A flat dict mapping tool-name strings to registration functions:
TOOL_REGISTRY = {
"read_file": register_read_file,
"create_file": register_create_file,
"replace_in_file": register_replace_in_file,
# ... 60+ tools
}
Each register_* function takes a pydantic-ai agent and calls
@agent.tool to wire the tool's JSON schema.
register_tools_for_agent(agent, tool_names, agent_name=...) is called
during agent build. It:
on_register_tools() → merges into TOOL_REGISTRYon_register_agent_tools(agent_name) → unions into the requested list"edit_file" → ["create_file", "replace_in_file", "delete_snippet"])register_func(agent)| Category | Tools |
|---|---|
| File ops | list_files, read_file, grep |
| File mods | create_file, replace_in_file, delete_snippet, delete_file |
| Shell | agent_run_shell_command, agent_share_your_reasoning |
| Sub-agents | list_agents, invoke_agent, invoke_agent_with_model |
| Skills | activate_skill, list_or_search_skills |
| User | ask_user_question, load_image_for_analysis |
| Browser | 30+ browser_* tools (Playwright-backed) |
| Models | list_available_models |
| UC | universal_constructor (dynamic tool factory) |
Plugins register tools via two complementary hooks:
# 1. Define the tool (adds to TOOL_REGISTRY)
def _register_tools():
return [{"name": "my_tool", "register_func": register_my_tool}]
register_callback("register_tools", _register_tools)
# 2. Advertise it to agents (adds to agent's tool list)
def _advertise(agent_name=None):
return ["my_tool"]
register_callback("register_agent_tools", _advertise)
Step 1 makes the tool exist; step 2 makes agents see it. Both are needed.
| Tier | Location | Load order |
|---|---|---|
| Builtin | code_puppy/plugins/<name>/register_callbacks.py | 1st |
| User | ~/.code_puppy/plugins/<name>/register_callbacks.py | 2nd |
| Project | <CWD>/.code_puppy/plugins/<name>/register_callbacks.py | 3rd (highest precedence) |
Each plugin is a directory containing register_callbacks.py. The loader
auto-discovers it. Project plugins shadow user plugins on name collision.
register_callback(phase, func) at module scope. The callback engine
(callbacks.py) stores functions per phase and fires them at the right
time. All hooks accept sync or async functions.
Most important hooks for extending Code Puppy:
| Hook | When | Return value |
|---|---|---|
register_tools | Tool registration | list[dict] with name, register_func |
register_agent_tools | Per-agent tool advertisement | list[str] of tool names |
register_skills | Skill catalogue | list[dict] with name + skill_md/skill_md_path/frontmatter+body |
register_agents | Agent catalogue | list[dict] with name, class |
load_prompt | System prompt assembly | str fragment or None |
get_model_system_prompt | Per-model prompt patch | dict with instructions/handled or None |
custom_command | Unknown /slash command | True (handled), str (message), or None (not mine) |
pre_tool_call | Before tool executes | Can modify args |
post_tool_call | After tool finishes | Observes result + duration |
run_shell_command | Before shell exec | Return {"blocked": True} to block |
file_permission | Before file op | bool — allow/deny |
agent_run_start / agent_run_end | Agent lifecycle | Observes name, model, session |
The full list is in callbacks.py — PhaseType has ~45 phases.
my_plugin/
__init__.py # (can be empty)
register_callbacks.py
from code_puppy.callbacks import register_callback
def _on_load_prompt():
return "\n## Project Rules\nAlways use type hints."
register_callback("load_prompt", _on_load_prompt)
That's it. The loader handles discovery, import, and registration.
model_factory.py)Resolves a model-name string into a pydantic-ai Model object.
Supports providers: OpenAI, Anthropic, Gemini, Cerebras, OpenRouter,
Azure, and custom OpenAI/Anthropic/Gemini-compatible endpoints.
Models are defined in three places (merged at runtime):
~/.code_puppy/extra_models.json — user-defined custom modelsload_models_config callback returns a dictExample extra_models.json:
{
"my-model": {
"type": "custom_openai",
"name": "gpt-4o",
"custom_endpoint": {
"url": "https://api.example.com/v1",
"api_key": "$MY_API_KEY"
},
"context_length": 128000
}
}
| Type | Provider |
|---|---|
openai / openai_responses | OpenAI |
anthropic | Anthropic (Claude) |
gemini | Google Gemini |
cerebras | Cerebras (via CerebrasProvider) |
custom_openai | Any OpenAI-compatible endpoint |
custom_anthropic | Anthropic-compatible endpoint |
round_robin | Cycle through N models (rate-limit mitigation) |
| Plugin types | Registered via register_model_type callback |
For any agent: runtime override > agent-pinned model > agent's model
field (JSON agents) > global model name.
The global model is set via /model and stored in config.
mcp_/manager.py)Code Puppy manages external MCP (Model Context Protocol) servers that provide additional tools to agents. Key components:
mcp_/registry.py) — server definitions (stdio/SSE/streamable-http)mcp_/manager.py) — lifecycle: start, stop, health checksmcp_/agent_bindings.py) — which agents get which serversmcp_/circuit_breaker.py) — auto-disables flaky serversServers can be:
mcp_servers field, or
bound via the /mcp bind menuauto_start: true start when the
agent runsThe pre_mcp_autostart hook fires before auto-start, letting plugins
refresh tokens or mint credentials.
Use /mcp in the TUI:
/mcp list — show configured servers/mcp start <name> / /mcp stop <name>/mcp status — health dashboard/mcp bind — attach servers to agentsEach BaseAgent maintains _message_history — a list of pydantic-ai
ModelMessage objects (alternating ModelRequest/ModelResponse).
History is the conversation context sent to the LLM on each turn.
When the conversation grows too large:
_history.py) — estimates tokens per message_compaction.py) — summarizes older messages using a
dedicated summarization agent, preserving recent messagesprotected_token_count)Sessions are saved as pickle files in the state directory:
session_storage.py handles serialization/save <name> and /load <name> commands| Command | Effect |
|---|---|
/save <name> | Save current session |
/load <name> | Load a saved session |
/pop [N] | Remove N most recent messages |
/truncate <N> | Keep only N most recent messages |
/prune | Interactive context pruning UI |
A skill is a directory containing a SKILL.md with YAML frontmatter
(name, description, optional version, author, tags) and
markdown body. The body is the instruction set loaded into context when
the skill is activated.
plugins/agent_skills/discovery.py)Scans these directories (first-discovered wins on name collision):
~/.code_puppy/skills/<CWD>/.code_puppy/skills/<CWD>/skills/register_skills callback)activate_skill("skill-name") or the user types
/<skill-name>SKILL.md from diskSkills are opt-in — the model sees a one-line summary in its system prompt (name + description) and must explicitly activate to get full instructions.
| Command | Effect |
|---|---|
/skills | Interactive TUI menu |
/skills list | Text list of all skills |
/skills enable / disable | Toggle skills globally |
/skills frontmatter on/off | Toggle skill summaries in system prompt |
/skills install | Browse & install from remote catalog |
Assembled in two stages: get_full_system_prompt() builds the base
(base_agent.py), then _assemble_instructions() (_builder.py)
appends rules and runs the per-model patcher.
get_full_system_prompt() layers:
1. Authored prompt (agent.get_system_prompt())
2. load_prompt fragments (plugin-injected: kennel memory, permission
rules, …)
3. Identity ID (agent name + UUID prefix)
_assemble_instructions() then appends:
4. AGENTS.md puppy_rules (global ~/.code_puppy/AGENTS.md, then project
<CWD>/.code_puppy/AGENTS.md, then ./AGENTS.md fallback — concatenated,
not first-wins)
5. Extended-thinking note (only if active for the resolved model)
6. Per-model patches (get_model_system_prompt callbacks, via
prepare_prompt_for_model) — this is where the **skills block**
(available-skills summary) is appended, by the agent_skills plugin
The skills summary is not a
load_promptfragment — it's injected throughget_model_system_prompt, so it lands after identity and rules.
Layers 2–6 are runtime-only — recomputed every run, never persisted into static agent definitions. This prevents stale timestamps, dead file paths, and baked-in session IDs from leaking into cloned agents.
config.py)All settings live in ~/.code_puppy/puppy.cfg (INI format) managed via:
get_value(key) / set_value(key, value)/set <key> <value> slash commandKey directories:
~/.code_puppy/~/.code_puppy/state/ (or platform cache dir)~/.code_puppy/cache/~/.code_puppy/agents/~/.code_puppy/skills/~/.code_puppy/plugins/~/.code_puppy/extra_models.jsonAGENTS.md rules files are loaded from ~/.code_puppy/AGENTS.md (global),
<CWD>/.code_puppy/AGENTS.md (project), and ./AGENTS.md (fallback).
messaging/)Plugins emit UI messages through the message bus rather than printing directly:
from code_puppy.messaging import emit_info, emit_success, emit_warning, emit_error
emit_info("Something happened")
These are rendered by the TUI's event handler, so they work in both interactive and streaming contexts.
agents/event_stream_handler.py)The agent's streaming response is handled by event_stream_handler,
which processes pydantic-ai streaming events (text deltas, tool calls,
tool returns) and emits UI messages. The stream_event callback lets
plugins observe these events.
command_line/ or core agent files.register_callbacks.py per plugin — register at module scope.None from hooks/commands you don't own.ruff check --fix, ruff format .my_plugin/
├── __init__.py # docstring (can be minimal)
├── register_callbacks.py # entry point: register_callback() calls
├── helpers.py # optional: logic split out
└── README.md # optional: documentation
| File | Responsibility |
|---|---|
agents/base_agent.py | Abstract agent base — thin conductor |
agents/_builder.py | Builds pydantic-ai Agent + MCP wiring |
agents/_runtime.py | run_with_mcp() — streaming, retries, cancellation |
agents/_compaction.py | Context summarization |
agents/agent_manager.py | Agent registry, switching, discovery |
agents/json_agent.py | JSON-config agent loader |
tools/__init__.py | TOOL_REGISTRY, register_tools_for_agent() |
plugins/__init__.py | Plugin loader (builtin → user → project) |
callbacks.py | Hook engine — 45+ phases |
config.py | Config read/write, directories, model settings |
model_factory.py | Model-name → pydantic-ai Model |
mcp_/manager.py | MCP server lifecycle |
session_storage.py | Session pickle save/load |
plugins/agent_skills/ | Skills discovery, activation, UI |
pydantic_patches.py | Startup monkey-patches (clipboard fix, etc.) |
Activate this skill when you need to: