@pedro_klein/pi-baml

BAML integration for Pi coding agent — typed structured output from LLMs

Packages

Package details

extensionskill

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:

  1. Always use client PiClient. The real model is selected at call time via the tier system.
  2. 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_start dependency).
  • Every method takes modelRegistry as a parameter. No internal state, no ordering issues.
  • If pi-baml isn't installed, pi-baml:ready never 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