a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

add pydantic-ai-skills + first skill (publish-blog)

introduces agentskills.io-format skills as a progressive-disclosure layer
between the personality file and tool docstrings. skills sit in a
bot/skills/ directory; each skill is a SKILL.md with YAML frontmatter
(name + description) and a markdown body. the preamble (skill names +
descriptions) goes into the system prompt automatically via
SkillsToolset on pydantic-ai>=1.74 — we're on 1.84. the full body only
loads when the agent calls load_skill(name).

this is the right layer for workflow-level guidance that previously had
nowhere to live:
- tool docstrings: mechanical per-tool ("what this call does")
- personality file: voice + disposition
- skill description: trigger ("when to reach for this")
- skill body: procedure ("how to do this well")

first skill: publish-blog. description does the activation gating; body
covers structure patterns that have actually worked, link-back
discipline, voice convention (caps in long-form), and gotchas (verify
any AT-URI via pdsx.get_record before citing; tag with specific topics,
not meta-categories).

changes:
- pyproject: added pydantic-ai-skills dependency
- config.Settings: added skills_dir (defaults to "skills", matching the
personality_file convention)
- agent.PhiAgent: constructs SkillsToolset once from settings.skills_dir
and passes it via Agent(toolsets=[...]). MCP toolsets still go per
agent.run() to work around the cancel-scope bug.
- Dockerfile: copies skills/ into the runtime image alongside
personalities/

ruff clean, 102 tests pass.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

zzstoatzz 764b49a5 f2f7648f

+53
+1
Dockerfile
··· 32 32 33 33 # Copy runtime data 34 34 COPY personalities/ /app/personalities/ 35 + COPY skills/ /app/skills/ 35 36 36 37 ENV PATH="/app/bin:$PATH" 37 38 ENV PYTHONUNBUFFERED=1
+1
pyproject.toml
··· 14 14 "numpy>=2.4.4", 15 15 "openai", 16 16 "pydantic-ai>=1.9", 17 + "pydantic-ai-skills>=0.8.0", 17 18 "pydantic-settings", 18 19 "slowapi>=0.1.9", 19 20 "turbopuffer",
+23
skills/publish-blog/SKILL.md
··· 1 + --- 2 + name: publish-blog 3 + description: Publish a long-form post on greengale.app. Use when a thought needs more space than a bluesky thread — multi-part essays, syntheses of a conversation you've been in, worked examples. For single observations use post instead; for a URL + commentary use note or save_url. 4 + --- 5 + 6 + ## structure that's worked 7 + 8 + observation-first opening (the specific thing that prompted the piece), not context-first. sections that move the argument, not sections that catalog what you know. close with what would change if you're right, not a moralizing wrap. 9 + 10 + pattern: open with the specific thing (a post, a thread, a moment) → name the old problem it touches → name what's actually new / what's still inherited → close with the consequence. 11 + 12 + ## link back 13 + 14 + if the piece came out of a thread, link the root thread in your follow-up post. don't bury the provenance. 15 + 16 + ## voice 17 + 18 + standard capitalization in long-form — readers expect it. lowercase stays for the accompanying bsky post. 19 + 20 + ## gotchas 21 + 22 + - verify any AT-URI you cite via `pdsx.get_record` first. broken rkeys in blog posts are harder to retract than in tweets. 23 + - tag with specific topic words, not meta-categories (`atproto` ✓, `thoughts` ✗).
+8
src/bot/agent.py
··· 9 9 10 10 from pydantic_ai import Agent, ImageUrl, RunContext 11 11 from pydantic_ai.mcp import MCPServerStreamableHTTP 12 + from pydantic_ai_skills import SkillsToolset 12 13 13 14 from bot.config import settings 14 15 from bot.core.atproto_client import bot_client, get_identity_block ··· 137 138 self.memory = None 138 139 logger.warning("no memory - missing turbopuffer or openai key") 139 140 141 + # Skills — filesystem-backed, progressive disclosure. The preamble 142 + # (skill names + descriptions) is injected automatically by the 143 + # toolset on pydantic-ai>=1.74. Full SKILL.md bodies are loaded on 144 + # demand via load_skill. 145 + self.skills_toolset = SkillsToolset(directories=[settings.skills_dir]) 146 + 140 147 # Create PydanticAI agent without MCP toolsets — they're created 141 148 # fresh per agent.run() call to avoid the cancel scope bug: 142 149 # https://github.com/pydantic/pydantic-ai/issues/2818 ··· 160 167 ), 161 168 output_type=str, 162 169 deps_type=PhiDeps, 170 + toolsets=[self.skills_toolset], 163 171 ) 164 172 165 173 # --- dynamic system prompts ---
+4
src/bot/config.py
··· 39 39 default="personalities/phi.md", 40 40 description="The file containing the bot's personality", 41 41 ) 42 + skills_dir: str = Field( 43 + default="skills", 44 + description="Directory containing agentskills.io-format skill packages", 45 + ) 42 46 43 47 # LLM configuration (support multiple providers) 44 48 openai_api_key: str | None = Field(
+16
uv.lock
··· 225 225 { name = "numpy" }, 226 226 { name = "openai" }, 227 227 { name = "pydantic-ai" }, 228 + { name = "pydantic-ai-skills" }, 228 229 { name = "pydantic-settings" }, 229 230 { name = "slowapi" }, 230 231 { name = "turbopuffer" }, ··· 249 250 { name = "numpy", specifier = ">=2.4.4" }, 250 251 { name = "openai" }, 251 252 { name = "pydantic-ai", specifier = ">=1.9" }, 253 + { name = "pydantic-ai-skills", specifier = ">=0.8.0" }, 252 254 { name = "pydantic-settings" }, 253 255 { name = "slowapi", specifier = ">=0.1.9" }, 254 256 { name = "turbopuffer" }, ··· 1957 1959 sdist = { url = "https://files.pythonhosted.org/packages/d0/fa/74ebb63cab77331fd98124e6665bff1634415a739a5f4aba077ce34f0e81/pydantic_ai-1.80.0.tar.gz", hash = "sha256:93aa897fac720cd9e11b4f47b0c456649cc72f4b65e575eddfd9adbecc200b81", size = 12793, upload-time = "2026-04-10T23:31:17.844Z" } 1958 1960 wheels = [ 1959 1961 { url = "https://files.pythonhosted.org/packages/2c/9a/6d65ea560f8e84f57cf5cf7f5b611d48bd5d1869120bc7fcc3b40aed0556/pydantic_ai-1.80.0-py3-none-any.whl", hash = "sha256:71d0117a7c55116c00d7dfdc973f0bead056709608b251f83a884821341177ee", size = 7549, upload-time = "2026-04-10T23:31:09.674Z" }, 1962 + ] 1963 + 1964 + [[package]] 1965 + name = "pydantic-ai-skills" 1966 + version = "0.8.0" 1967 + source = { registry = "https://pypi.org/simple" } 1968 + dependencies = [ 1969 + { name = "anyio" }, 1970 + { name = "pydantic-ai-slim" }, 1971 + { name = "pyyaml" }, 1972 + ] 1973 + sdist = { url = "https://files.pythonhosted.org/packages/5f/07/176bad219d92507c5c828bc9e91a9a8ae16248a8f09c71ccb5c45157eff5/pydantic_ai_skills-0.8.0.tar.gz", hash = "sha256:464a3d32602e98bc8d66026b007360d34fc7fcf9da9c423cc65a0441239cf5a7", size = 9011241, upload-time = "2026-04-21T02:16:18.577Z" } 1974 + wheels = [ 1975 + { url = "https://files.pythonhosted.org/packages/50/66/eb87dba97db261c18bcbeb70480fcf8d4b7dc609b553904e605ed01b0746/pydantic_ai_skills-0.8.0-py3-none-any.whl", hash = "sha256:04a6925d33e05e022595b54ae4fea1981edd2d47b937ea8f146a55e7d44529f9", size = 54782, upload-time = "2026-04-21T02:16:16.225Z" }, 1960 1976 ] 1961 1977 1962 1978 [[package]]