@pedro_klein/pi-baml
BAML integration for Pi coding agent — typed structured output from LLMs
Package details
Install @pedro_klein/pi-baml from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@pedro_klein/pi-baml- Package
@pedro_klein/pi-baml- Version
0.2.0- Published
- Jul 3, 2026
- Downloads
- 151/mo · 9/wk
- Author
- pedro_klein
- License
- MIT
- Types
- extension, skill
- Size
- 101 KB
- Dependencies
- 2 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
],
"skills": [
"skills/baml"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-baml
Typed structured output from LLMs for the Pi coding agent. Connects BAML (a DSL for defining typed LLM functions) to Pi's provider system, so the agent and extensions get schema-validated responses without manual JSON parsing.
Install
pi install npm:@pedro_klein/pi-baml
How it works
pi-baml registers three tools (baml_list, baml_run, baml_exec) and injects an <available_baml_functions> block into the system prompt, following the same pattern Pi uses for <available_skills>. The agent sees what functions exist, calls baml_list to get signatures, and calls baml_run to execute them.
Functions live in .baml files organized into groups (directories). Groups are discovered from standard paths at startup and compiled on demand at call time. Credentials come from Pi's ModelRegistry; there's no separate API key management.
The package also ships a bundled BAML authoring skill (skills/baml/) that teaches the agent how to write correct BAML code for baml_exec.
Configuration
Add a baml section to ~/.pi/agent/settings.json:
{
"baml": {
"models": {
"light": "github-copilot/claude-haiku-4.5",
"standard": "github-copilot/claude-sonnet-4.6",
"heavy": "hai-proxy/anthropic--claude-4.6-opus"
}
}
}
Three model tiers map to provider/model pairs from Pi's model registry (pi --list-models):
| Tier | When to use | Typical model class |
|---|---|---|
light |
Simple classification, formatting, yes/no | haiku |
standard |
Most tasks: extraction, multi-field output (default) | sonnet |
heavy |
Complex reasoning, ambiguous inputs | opus |
See docs/configuration.md for the full reference, including supported API types and provider-specific notes.
Optional settings
| Key | Default | Description |
|---|---|---|
systemPrompt |
true |
Inject <available_baml_functions> into the system prompt |
functionsDirs |
[] |
Extra directories to scan for function groups |
Function discovery
Groups are discovered from these directories (lowest to highest priority):
| Priority | Path | Notes |
|---|---|---|
| 1 (lowest) | Pi's resolved skill paths | Skill-colocated, prefixed skill:, discovered lazily |
| 2 | ~/.agents/baml/<group>/ |
Shared across agents |
| 3 | ~/.pi/baml/<group>/ |
User Pi-local |
| 4 | [functionsDirs] |
From settings |
| 5 | <cwd>/.pi/baml/<group>/ |
Project Pi-local |
| 6 (highest) | <cwd>/.agents/baml/<group>/ |
Project-specific |
Higher priority wins when names collide. Each subdirectory is one compilation unit.
Skill-colocated BAML is discovered lazily on the first agent turn using Pi's resolved skill paths — this automatically works with any profile, including custom --skill flags and npm-packaged skills.
System prompt injection
When systemPrompt is enabled, pi-baml appends an XML block to the system prompt listing all non-skill groups:
<available_baml_functions>
BAML function groups callable via the baml_run tool.
Call baml_list with a group name to see function signatures and types before invoking.
<group>
<name>extract-todos</name>
<description>Extract TODO items from freeform text.</description>
</group>
</available_baml_functions>
skill:* groups are excluded from this block. They're internal to their owning skill and documented in its SKILL.md instead.
Adding descriptions
Add a README.md with frontmatter to any group directory:
---
description: Extract TODO items from freeform text.
---
# Extract TODOs
Additional documentation shown when the agent calls baml_list(group).
The description field appears in the system prompt. The README body shows up in baml_list detail output. README.md is never compiled (only .baml files go to the BAML runtime).
Tools
| Tool | Purpose |
|---|---|
baml_list |
Browse groups and get function signatures |
baml_run |
Execute a pre-defined function by name |
baml_exec |
Compile and execute inline BAML code |
All three accept an optional model parameter: "light", "standard", or "heavy" (default: "standard").
Output includes the resolved model and tier:
{
"result": [{"task": "Buy milk", "priority": "low"}],
"model": "github-copilot/claude-haiku-4.5",
"tier": "light"
}
Writing .baml files
class ActionItem {
description string @description("what needs to be done")
assignee string? @description("who is responsible, if mentioned")
priority "high" | "medium" | "low"
}
function ExtractActionItems(meeting_notes: string) -> ActionItem[] {
client PiClient
prompt #"
Extract action items from these meeting notes.
{{ ctx.output_format }}
---
{{ meeting_notes }}
"#
}
Two rules:
- Always use
client PiClient. The real model is selected at call time via the tier system. - Always include
{{ ctx.output_format }}in the prompt so BAML can inject its schema instructions.
Skill integration
Skills can bundle their own .baml files for deterministic processing. Two layouts are supported:
Dedicated subdirectory (preferred for multiple files):
skills/
my-skill/
SKILL.md
baml/
classify.baml
README.md
Flat layout (convenient for single-file schemas):
skills/
my-skill/
SKILL.md
schema.baml
If both a baml/ subdirectory and flat .baml files exist, the subdirectory takes precedence.
These register with a skill: prefix (e.g. skill:my-skill) and are callable via baml_run("skill:my-skill/ClassifyInput", {...}). They are discovered lazily on the first agent turn using Pi's resolved skill paths — automatically working with any profile configuration. They are excluded from the system prompt. The skill's own SKILL.md documents when and how to call them.
This keeps skill-specific BAML self-contained alongside the skill that uses it, rather than polluting the global function namespace.
EventBus API (for extension authors)
Other Pi extensions can consume pi-baml programmatically:
let baml: PiBamlLibrary | null = null;
pi.events.on("pi-baml:ready", (lib) => {
baml = lib as PiBamlLibrary;
});
pi.on("session_start", async (event, ctx) => {
if (!baml?.available) return;
// One-shot: compile and execute inline BAML
const items = await baml.execBaml(
classifyBamlSource,
"ClassifySkill",
{ prompt: userMessage, skills: skillList },
ctx.modelRegistry,
"light",
);
// Execute a registered function by name
const todos = await baml.call(
"ExtractTodos",
{ notes: meetingNotes },
ctx.modelRegistry,
"heavy",
);
// Create a reusable executor from in-memory files
const executor = await baml.createExecutor(
{ "main.baml": bamlSource },
ctx.modelRegistry,
"standard",
);
const result = await executor.call("MyFunc", { input: "..." });
});
Key points:
- The library fires on the EventBus during factory phase (no
session_startdependency). - Every method takes
modelRegistryas a parameter. No internal state, no ordering issues. - If pi-baml isn't installed,
pi-baml:readynever fires. Extensions should fall back gracefully.
Bundled skill
The package includes a BAML authoring skill at skills/baml/. When installed, it appears in the agent's available skills and provides guidance on BAML's type system, prompt patterns, and pi-baml conventions. The agent loads it automatically when writing dynamic BAML for baml_exec.
Declared in package.json:
{
"pi": {
"extensions": ["./src/index.ts"],
"skills": ["skills/baml"]
}
}
Development
pnpm install
pnpm build # tsup -> dist/
pnpm typecheck # tsc --noEmit
pnpm lint # eslint src/ tests/
pnpm test # vitest (unit tests)
pnpm test:integration # real BAML compilation
Integration tests with live LLM calls:
PI_BAML_TEST_PROXY_URL=http://localhost:6655 npm run test:integration
License
MIT