How to use Grimoire as a Python library for runtime prompt assembly, tool schema generation, and in-memory binding — without touching the CLI.
This document is the definitive reference for integrating Grimoire into your Python application. It covers the Grimoire facade, the lower-level components for advanced use cases, real-world recipes, architecture, and error handling.
- Quick Start
- The Grimoire Facade
- Output Formats
- Provenance Tracking
- Lower-Level Components
- Integration Recipes
- Error Handling
- Architecture
- Thread Safety & Performance
- Type Reference
- Feature Notes by Version
from grimoire import Grimoire
# Load a grimoire repository
g = Grimoire("/path/to/my-grimoire")
# Conjure a spell → get OpenAI messages
result = g.conjure("engineering/root_cause_analysis", variables={
"issue_title": "Database connection timeout",
"symptoms": "Queries hang after 30s, connection pool exhausted",
})
messages = result.to_messages("openai")
# → [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}]
# Get tool schemas for function calling
tools = g.tool_schemas(tags=["devtools"])
# → [{"type": "function", "function": {"name": "devtools__git__status", ...}}, ...]
# Check what variables a spell needs
missing = g.missing_vars("engineering/root_cause_analysis")
# → {"issue_title": VariableSpec(...), "symptoms": VariableSpec(...)}That's the entire setup. No binders to instantiate, no engines to wire, no promptlet dicts to build. The facade handles it all.
from grimoire import Grimoire
# Load from explicit path
g = Grimoire("/path/to/grimoire-repo")
# Load from current directory
g = Grimoire()
# Non-strict mode: leave unresolved variables as {{ placeholders }}
# instead of raising MissingVariableError
g = Grimoire("/path/to/repo", strict=False)Construction loads the repository, indexes all artifacts, wires the conjure engine with all discovered promptlets and runes, and prepares the bundle assembler. Typical load time is under 50ms for repositories with hundreds of artifacts.
Optional keyword arguments: procedural_retriever / procedural_indexer (a semantiscan-compatible retriever/indexer for intent discovery; without them an in-memory fallback is used). To compose several repositories into one view, use Grimoire.layered(...) instead of the constructor.
# If files were modified on disk, reload to pick up changes
g.reload()g.name # → "my-grimoire" (from grimoire.yaml)
g.version # → "1.0.0"
g.repo # → GrimoireRepo instance (read-only access to internals)g.layers # → ["builtin", "admin", "user"] (None for a single-root facade)
g.resolve_layer("agent/system", kind="spell") # → "user"
g.invalidate_caches() # drop memoized catalog()/tool_schemas() (reload() does this for you)Grimoire.layered(roots) (0.4.0) builds the facade over ordered overlay layers —
lowest → highest precedence, highest wins — so a shipped, read-only pack can be
extended or overridden by admin and user overlays without touching shipped files.
This is how llmcore and wairu compose their prompt/tool control plane
(llmcore-builtin < wairu-core < user).
from grimoire import Grimoire
g = Grimoire.layered(
[
("builtin", "/opt/app/grimoire", False), # read-only
("admin", "/etc/app/grimoire", True), # writable overlay
("user", "~/.config/app/grimoire", True), # writable, highest precedence
],
strict=True, # conjure strictness (missing required variables raise)
load_strict=True, # fail-loud loading (see below)
scaffold_writable=True, # create missing writable layer roots
)What the composed view gives you:
- Every artifact type resolves through the layers: spells, promptlets, runes,
rituals, bundles, skilldocs, and
vars/defaults.yaml(merged per key). All facade APIs —conjure,tool_schemas,catalog,bind,validate,lint, intent search, the MCP manifest — operate on the composedCompositeRepoView, which is duck-type compatible withGrimoireRepo(including in-memory runtime rune registration; runtime runes are dropped onreload()by design). - Deterministic precedence — discovery is sorted, so the winner for a duplicated id is stable across machines (layer order first, then path-list order, then file name).
- Strict loading (
load_strict=True, the default) — a parse failure or a duplicate id inside a layer raisesRepoErrornaming the layer, instead of log-and-skip. Use this for control-plane overlays so a broken user file fails at startup. - Per-layer validation —
g.validate(layer="user")validates one layer's repo in isolation (an overlay that merely shadows artifacts must still be valid on its own).validate()withoutlayervalidates the composed view. - Writes —
write_spell/update_spell/delete_spelltarget the highest writable layer (or a named one via the underlyingLayeredGrimoire:g._layered.write_spell(spell, layer="admin")). Deleting an overlay artifact un-shadows the lower definition. - Caches —
catalog()and unfilteredtool_schemas()are memoized; static host/git conjure builtins are computed once per engine (nogitsubprocess per conjure).reload()and runtime rune registration invalidate the caches;invalidate_caches()does it manually.
The lower-level LayeredGrimoire / GrimoireLayer / CompositeRepoView classes are
exported from the package for callers that need typed per-layer access
(get_*/list_* for all artifact types, writable_layers, composite_view()).
The conjure() method auto-detects the artifact type by looking up the ID across spells, bundles, and rituals (in that order):
# Spell → returns ConjuredPrompt
result = g.conjure("engineering/root_cause_analysis", variables={
"issue_title": "OOM crash",
"symptoms": "Process killed by OOM killer at 03:42 UTC",
})
assert isinstance(result, ConjuredPrompt)
# Bundle → returns ConjuredPrompt (after assembly + variant selection)
result = g.conjure("bundles/engineering/rca_with_tools", variables={
"issue_title": "OOM crash",
"symptoms": "Process killed",
}, context={"provider": "anthropic"}) # selects the anthropic variant
assert isinstance(result, ConjuredPrompt)
# Ritual → returns list[ConjuredRitualStep]
steps = g.conjure("rituals/rca_loop", variables={
"issue_title": "OOM crash",
"symptoms": "Process killed",
})
assert isinstance(steps, list)
for step in steps:
if not step.skipped:
print(step.conjured.to_text())When you know the artifact type and want static type safety:
# Spell only — always returns ConjuredPrompt
prompt: ConjuredPrompt = g.conjure_spell("engineering/root_cause_analysis", variables={...})
# Bundle only — always returns ConjuredPrompt
prompt: ConjuredPrompt = g.conjure_bundle(
"bundles/engineering/rca_with_tools",
variables={...},
context={"provider": "anthropic"},
)
# Ritual only — always returns list[ConjuredRitualStep]
steps: list[ConjuredRitualStep] = g.conjure_ritual("rituals/rca_loop", variables={...})| Parameter | Type | Default | Description |
|---|---|---|---|
artifact_id |
str |
required | Spell, bundle, or ritual ID |
variables |
dict[str, Any] | None |
None |
Explicit variable values (highest precedence) |
defaults |
dict[str, Any] | None |
grimoire defaults | Default values (lowest precedence) |
context |
dict[str, str] | None |
None |
Bundle variant selection context |
strict |
bool | None |
instance default | Override strict mode for this call |
schema = g.spell_vars("engineering/root_cause_analysis")
# Returns: dict[str, VariableSpec]
for name, spec in schema.items():
print(f"{name}: type={spec.type.value}, required={spec.required}, "
f"default={spec.default}, ask={spec.ask!r}")Output:
issue_title: type=string, required=True, default=None, ask='Brief title of the issue or incident'
symptoms: type=multiline, required=True, default=None, ask='Observed symptoms (one per line)'
environment: type=string, required=False, default='production', ask='Environment where the issue occurred'
context: type=multiline, required=False, default='', ask='Relevant codebase or system context'
# What's still needed for this spell?
missing = g.missing_vars("engineering/root_cause_analysis")
# → {"issue_title": VariableSpec(...), "symptoms": VariableSpec(...)}
# (environment has a default, context has a default → not missing)
# After the user provides some values:
missing = g.missing_vars("engineering/root_cause_analysis", provided={
"issue_title": "Timeout bug",
})
# → {"symptoms": VariableSpec(...)}
# Works for bundles too:
missing = g.missing_vars("bundles/engineering/rca_with_tools")This is ideal for building interactive UIs or agent loops that progressively collect required inputs.
Generate OpenAI-compatible function-calling tool definitions from runes:
# All runes
tools = g.tool_schemas()
# Filter by tags
tools = g.tool_schemas(tags=["devtools"])
# Specific runes
tools = g.tool_schemas(rune_ids=["devtools/git", "semantiscan/query"])Each rune command becomes one tool definition:
{
"type": "function",
"function": {
"name": "devtools__git__status",
"description": "Show working tree status",
"parameters": {
"type": "object",
"properties": {
"porcelain": {
"type": "bool",
"default": true
}
}
}
}
}Tool names follow the pattern {rune_id}__{command_name} with / replaced by __ for JSON compatibility.
Produce bind artifacts in memory without writing to disk:
# Bind for llmcore
result = g.bind("llmcore")
assert result.ok
print(f"Produced {len(result.files)} files, hash={result.compiled_hash}")
# Iterate over produced files
for f in result.files:
print(f" {f.relative_path} ({len(f.content)} bytes)")
# f.content is the full file content as a string
# Filter by tags or specific artifacts
result = g.bind("llmcore", tags=["engineering"])
result = g.bind("llmcore", spell_ids=["examples/greet"])
result = g.bind("wairu", rune_ids=["devtools/git"])
# Specify sub-format
result = g.bind("semantiscan", fmt="toml")
result = g.bind("semantiscan", fmt="legacy_tmpl")
# Write to disk when ready
written_paths = result.write("/path/to/exports/llmcore/")Supported targets: "llmcore", "semantiscan", "wairu" (or use BindTarget enum).
# Full repository validation (structure, references, schemas)
val_result = g.validate()
print(f"Valid: {val_result.ok}")
for diag in val_result.diagnostics:
print(f" [{diag.severity.value}] {diag.source}: {diag.message}")
# Lint a single spell (style rules)
lint_result = g.lint("engineering/root_cause_analysis")
# Lint all spells at once
lint_all = g.lint()
# Custom lint configuration
from grimoire.validate.rules import LintConfig
config = LintConfig(
forbidden_tokens=["TODO", "FIXME", "PLACEHOLDER", "HACK"],
max_user_block_chars=5000,
check_engineering_quality=True,
check_sensitivity=True,
)
lint_result = g.lint("engineering/rca", config=config)Get an agent-friendly JSON catalog of everything in the grimoire:
import json
catalog = g.catalog()
print(json.dumps(catalog, indent=2)){
"grimoire": {"name": "my-grimoire", "version": "1.0.0"},
"spells": [
{"id": "engineering/root_cause_analysis", "name": "Root Cause Analysis", "version": "1.0.0", "tags": ["engineering", "debugging", "rca"]},
{"id": "examples/greet", "name": "Greeting Spell", "version": "1.0.0", "tags": ["example", "test"]}
],
"runes": [
{"id": "devtools/git", "name": "Git (read-only diagnostics)", "version": "1.0.0", "tags": ["devtools", "vcs", "engineering"], "commands": ["status", "diff"]}
],
"rituals": [
{"id": "rituals/rca_loop", "name": "RCA Loop", "steps": 2}
],
"bundles": [...],
"skilldocs": [...],
"promptlets": [...]
}This catalog is designed for agent self-discovery: an LLM agent can read the catalog to decide which spell or rune to use for a given task.
Direct access to individual artifacts and filtered listings:
# Get by ID (raises ArtifactNotFoundError if missing)
spell = g.get_spell("engineering/root_cause_analysis")
rune = g.get_rune("devtools/git")
bundle = g.get_bundle("bundles/engineering/rca_with_tools")
ritual = g.get_ritual("rituals/rca_loop")
# Filtered listings
eng_spells = g.list_spells(tags=["engineering"])
devtools = g.list_runes(tags=["devtools"])
all_bundles = g.list_bundles()
all_rituals = g.list_rituals()Find artifacts from a natural-language intent instead of hard-coding ids (0.3.0). Both
methods are async. With no retriever configured they use a deterministic token-overlap
ranking over the in-memory catalog; pass a semantiscan-compatible
procedural_retriever (and procedural_indexer for rebuild_procedural_index()) to
rank with embeddings.
hits = await g.find_by_intent(
"summarize a pull request for reviewers",
top_k=3, filter_domain=None, filter_semantic_role=None, include_deprecated=False,
)
for m in hits: # IntentMatch(spell, score, highlight)
print(m.spell.id, m.score, m.highlight)
tools = await g.find_tools_by_intent(
"run the unit tests and report failures",
top_k=5, tags=None, max_risk="medium", include_requires_approval=True,
)
for m in tools: # ToolIntentMatch(rune, command, score, highlight)
print(m.rune.id, m.command.name, m.score)
await g.rebuild_procedural_index() # (re)index spells + rune commands in the retrieverRune commands can be published to MCP clients (0.3.0).
manifest = g.to_mcp_tool_manifest(tags=["devtools"]) # or rune_id=... / rune_ids=[...]
manifest["schema_version"]
manifest["tools"] # MCP tools/list entries: name, description, inputSchema, and a
# per-tool "_meta" block (rune id, command, risk level, approval,
# OWASP categories, execution target, content hash) for routingThe bundled server (pip install "grimoire[mcp]") wraps the same manifest in a
FastAPI JSON-RPC endpoint with bearer-token auth:
GRIMOIRE_MCP_TOKEN=change-me grimoire mcp serve --host 127.0.0.1 --port 8765 --endpoint /mcpMethods: initialize, tools/list, tools/call, prompts/list, prompts/get;
GET /health is unauthenticated. Programmatic embedding:
grimoire.mcp_server.server.build_app(g, auth_token=..., endpoint_path="/mcp", tool_callables={...}, allowed_tools=[...]) returns the FastAPI app (tool_callables maps tool names to the functions tools/call dispatches to).
Rune contracts accept owasp_categories at rune level and per command (OWASP LLM
Top-10 ids such as LLM01). The metadata is preserved through parsing, carried into
binder and MCP exports, and auditable with grimoire rune audit [--require-owasp].
grimoire.federation (extra federation, which pulls in llmcore) adapts MCP
requests/responses/tool calls and rune command executions into llmcore's shared
ecosystem event envelope: federate_mcp_request, federate_mcp_response,
federate_mcp_tool_call, federate_rune_command, federate_rune_commands. The module
imports llmcore lazily, so grimoire's core has no llmcore dependency.
A ConjuredPrompt can export to multiple formats:
result = g.conjure("examples/greet", variables={"user_name": "Alice"})
# Plain text with role headers
text = result.to_text()
# → "# SYSTEM\nYou are a careful...\n\n# USER\nPlease greet Alice in English."
# OpenAI message array
messages = result.to_messages("openai")
# → [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}]
# Anthropic message array (DEVELOPER merged into system)
messages = result.to_messages("anthropic")
# Raw messages (original role names preserved)
messages = result.to_messages("raw")
# Individual blocks
for block in result.blocks:
print(f"[{block.role.value}] {block.content[:80]}...")Every conjured prompt carries a provenance record:
result = g.conjure("engineering/rca", variables={"issue_title": "Bug", "symptoms": "crash"})
prov = result.provenance
print(f"Spell: {prov.spell_id}") # → "engineering/root_cause_analysis"
print(f"Hash: {prov.spell_hash}") # → "a1b2c3d4e5f6a7b8"
print(f"Includes: {prov.includes_resolved}") # → ["safety/base_engineering", "style/principal_swe"]
print(f"Runes: {prov.runes_referenced}") # → []
print(f"Vars: {prov.variables_used}") # → {"issue_title": "Bug", "symptoms": "crash", ...}
# (SECRET variables are shown as "[redacted]")Provenance enables: drift detection, audit trails, cache invalidation, and debugging which promptlets contributed to a final prompt.
The Grimoire facade is built on top of these components. Use them directly when you need fine-grained control.
The repository loader — discovers and indexes all artifacts from disk:
from grimoire import GrimoireRepo
repo = GrimoireRepo.load("/path/to/grimoire")
# Access artifacts
spell = repo.get_spell("engineering/rca")
rune = repo.get_rune("devtools/git")
promptlet = repo.get_promptlet("safety/base_engineering")
# Listings
spells = repo.list_spells(tags=["engineering"])
runes = repo.list_runes(tags=["devtools"])
# Defaults
defaults = repo.default_vars # → dict from vars/defaults.yaml
# Catalog
catalog = repo.catalog()
# Facade accessors
repo.prompts.list(tags=["engineering"])
repo.prompts.get("engineering/rca")
repo.prompts.bundles()
repo.skills.contracts(tags=["devtools"])
repo.skills.docs()The deterministic rendering engine — stateless, all context is passed in:
from grimoire import ConjureEngine
engine = ConjureEngine(
promptlets={p.id: p for p in repo.list_promptlets()},
runes={r.id: r for r in repo.list_runes()},
)
result = engine.conjure(
spell,
variables={"issue_title": "Bug", "symptoms": "crash"},
defaults=repo.default_vars,
strict=True,
)Materializes a bundle into a conjurable spell:
from grimoire import BundleAssembler
assembler = BundleAssembler(repo)
assembled_spell = assembler.assemble(
bundle,
context={"provider": "anthropic"}, # variant selection
variables={"issue_title": "Bug"},
)
# The assembled spell can be passed to ConjureEngine
result = engine.conjure(assembled_spell, variables={...})Evaluates multi-step ritual flows:
from grimoire import RitualEvaluator
evaluator = RitualEvaluator(repo, engine=engine)
# Dry run (inspect without conjuring)
plan = evaluator.dry_run(ritual)
print(f"OK: {plan.ok}, missing spells: {plan.missing_spells}")
# Full evaluation
steps = evaluator.evaluate(ritual, variables={"issue_title": "Bug", "symptoms": "crash"})
for step in steps:
if step.skipped:
print(f" [{step.step_id}] SKIPPED: {step.skip_reason}")
else:
print(f" [{step.step_id}] → {step.spell_id}")
print(step.conjured.to_text())Low-level binder classes for each target:
from grimoire.bind import LLMCoreBinder, SemantiscanBinder, WairuBinder
binder = LLMCoreBinder()
result = binder.bind(repo, tags=["engineering"])
result.compute_hash()
result.write("/path/to/exports/llmcore/")from grimoire import Grimoire
from llmcore import LLMCore
g = Grimoire("/path/to/grimoire")
llm = await LLMCore.create()
# Conjure a spell
prompt = g.conjure("engineering/root_cause_analysis", variables={
"issue_title": "Connection pool exhaustion",
"symptoms": "All database queries timeout after 30s",
"environment": "production k8s cluster",
})
# Extract system message and user message
messages = prompt.to_messages("openai")
system_msg = next((m["content"] for m in messages if m["role"] == "system"), None)
user_msg = next((m["content"] for m in messages if m["role"] == "user"), "")
# Send to LLM
response = await llm.chat(
message=user_msg,
system_message=system_msg,
enable_rag=False, # Grimoire controls all context
)g = Grimoire("/path/to/grimoire")
# Get tool schemas for the agent
tools = g.tool_schemas(tags=["devtools", "engineering"])
# Get the tool-calling policy prompt
policy = g.conjure("agentic/tool_calling_policy_low_risk")
system_msg = policy.to_messages("openai")[0]["content"]
# Use in an agent loop
response = await llm.chat(
message="Investigate why tests are failing on main",
system_message=system_msg,
tools=[Tool.from_dict(t) for t in tools],
enable_rag=False,
)g = Grimoire("/path/to/grimoire")
# semantiscan retrieves chunks, then Grimoire assembles the prompt
result = g.conjure("semantiscan/rag_default", variables={
"context": "\n---\n".join(chunk.text for chunk in retrieved_chunks),
"question": user_query,
})
# Pass the full prompt to llmcore (external RAG pattern)
response = await llm.chat(
message=result.to_text(),
enable_rag=False,
)g = Grimoire("/path/to/grimoire")
# Discover available tools from rune contracts
catalog = g.catalog()
available_runes = catalog["runes"]
# Get tool schemas for all runes the session should have
tools = g.tool_schemas(rune_ids=[r["id"] for r in available_runes])
# Inject the appropriate policy spell based on risk level
runes = [g.get_rune(r["id"]) for r in available_runes]
has_high_risk = any(r.risk_level.value == "high" for r in runes)
policy_spell = (
"agentic/tool_calling_policy_high_risk_requires_approval"
if has_high_risk
else "agentic/tool_calling_policy_low_risk"
)
policy = g.conjure(policy_spell)import sys
from grimoire import Grimoire
g = Grimoire(".")
# Step 1: Validate structure
val = g.validate()
if not val.ok:
for d in val.diagnostics:
print(f"[{d.severity.value}] {d.source}: {d.message}", file=sys.stderr)
sys.exit(1)
# Step 2: Lint all spells
lint = g.lint()
errors = [d for d in lint.diagnostics if d.severity.value == "error"]
if errors:
for d in errors:
print(f"[LINT ERROR] {d.source}: {d.message}", file=sys.stderr)
sys.exit(1)
# Step 3: Bind and export
for target in ("llmcore", "semantiscan", "wairu"):
result = g.bind(target)
result.write(f"exports/{target}/")
print(f"✓ {target}: {len(result.files)} files, hash={result.compiled_hash}")An agent that uses the catalog to choose which spell to conjure:
g = Grimoire("/path/to/grimoire")
# Give the agent the catalog so it can self-select
catalog = g.catalog()
catalog_json = json.dumps(catalog, indent=2)
# Agent system prompt includes the catalog
agent_system = f"""You have access to a grimoire with these spells:
{catalog_json}
When given a task, select the most appropriate spell, determine the
required variables, and request conjuring.
"""
# After the agent selects a spell:
selected_id = "engineering/root_cause_analysis"
# Check what variables the agent needs to provide
missing = g.missing_vars(selected_id, provided=agent_context)
# Once variables are gathered, conjure
result = g.conjure(selected_id, variables=agent_variables)
messages = result.to_messages("openai")All grimoire errors inherit from GrimoireError:
from grimoire import (
GrimoireError, # Base exception
ArtifactNotFoundError, # Spell/rune/ritual/bundle not found
AmbiguousArtifactError, # ID matches multiple types
MissingVariableError, # Required variable not provided (strict mode)
CircularIncludeError, # Include directives form a cycle
IncludeError, # Include target not found
BundleAssemblyError, # Bundle base_template not found or inject failure
RitualError, # Ritual evaluation failure
SpellParseError, # Malformed *.spell.md
RuneParseError, # Malformed *.rune.yaml
RepoError, # Repository loading failure
)Recommended pattern:
try:
result = g.conjure(spell_id, variables=user_vars)
except ArtifactNotFoundError:
# Unknown spell/bundle/ritual ID
logger.warning(f"Artifact not found: {spell_id}")
except MissingVariableError as e:
# Required variable not provided and strict=True
# e.args[0] contains the variable name and spell ID
logger.error(f"Missing variable: {e}")
except GrimoireError as e:
# Catch-all for any grimoire error
logger.error(f"Grimoire error: {e}")┌─────────────────────────────────────────────────┐
│ Grimoire Facade │
│ conjure() · tool_schemas() · bind() · ... │
├──────────┬───────────┬──────────┬───────────────┤
│ Conjure │ Bundle │ Ritual │ Binders │
│ Engine │ Assembler │Evaluator │ llm·sem·wairu │
├──────────┴───────────┴──────────┴───────────────┤
│ GrimoireRepo │
│ spell discovery · rune indexing · promptlets │
├─────────────────────────────────────────────────┤
│ Parsers │
│ spell.md · rune.yaml · ritual.yaml · bundle │
├─────────────────────────────────────────────────┤
│ Validators │
│ structural · lint · drift detection · golden │
└─────────────────────────────────────────────────┘
Data flow for a typical conjure() call:
conjure("bundles/engineering/rca_with_tools", variables={...}, context={...})
│
├─ 1. Look up bundle in repo._bundles
├─ 2. BundleAssembler.assemble(bundle, context)
│ ├─ Load base spell
│ ├─ Select variant (first match on context)
│ ├─ Inject promptlets at injection points
│ └─ Return assembled Spell
├─ 3. ConjureEngine.conjure(assembled_spell, variables, defaults)
│ ├─ Build effective variable map (precedence chain)
│ ├─ For each message block:
│ │ ├─ Resolve includes (recursive, cycle-detected)
│ │ ├─ Resolve rune introspection
│ │ └─ Resolve variables
│ └─ Build provenance record
└─ 4. Return ConjuredPrompt(blocks, provenance)
A Grimoire instance is safe for concurrent reads: multiple threads can call conjure(), tool_schemas(), catalog(), etc. simultaneously without locking.
It is not safe for concurrent mutation. If the repository files change on disk, call g.reload() from a single thread before subsequent reads. catalog() and unfiltered tool_schemas() are memoized (invalidated by reload(), runtime rune registration, or invalidate_caches()), and conjure builtins that shell out to git are computed once per engine.
Performance characteristics:
- Construction: ~20–50ms for a typical repository (hundreds of artifacts)
- Conjure (spell): ~1–5ms (dominated by include resolution)
- Conjure (bundle): ~2–10ms (assembly + conjuring)
- tool_schemas(): <1ms (in-memory transformation)
- bind(): ~10–50ms depending on artifact count and target
| Type | Module | Description |
|---|---|---|
Grimoire |
grimoire.api |
High-level facade (single root or Grimoire.layered(...)) |
LayeredGrimoire / GrimoireLayer / CompositeRepoView |
grimoire.layered |
Ordered overlay composition over GrimoireRepo |
IntentMatch / ToolIntentMatch |
grimoire.procedural |
Results of find_by_intent / find_tools_by_intent |
GrimoireRepo |
grimoire.store.repo |
Repository loader |
ConjureEngine |
grimoire.conjure.engine |
Rendering engine |
BundleAssembler |
grimoire.bundles.assembler |
Bundle materializer |
RitualEvaluator |
grimoire.rituals.evaluator |
Ritual step evaluator |
| Type | Description |
|---|---|
Spell |
Parsed spell template |
RuneSpec |
Parsed rune contract |
Bundle |
Parsed bundle recipe |
Ritual |
Parsed ritual flow |
SkillDoc |
Parsed knowledge document |
Promptlet |
Reusable prompt fragment |
ConjuredPrompt |
Rendered output (blocks + provenance) |
ConjuredRitualStep |
One evaluated ritual step |
MessageBlock |
Single role+content pair |
Provenance |
Audit trail for a conjured prompt |
VariableSpec |
Variable schema (type, required, default, ask) |
VariableType |
Enum: string, multiline, integer, float, boolean, choice, list, json, path |
| Type | Description |
|---|---|
BindResult |
Aggregated output of a bind operation |
BoundFile |
Single file with relative_path + content |
BindTarget |
Enum: llmcore, semantiscan, wairu |
BindFormat |
Enum: toml, legacy_tmpl, registry_bundle, tool_pack |
| Type | Description |
|---|---|
ValidationResult |
Collection of diagnostics with .ok property |
Diagnostic |
Single issue: severity, source, message |
Severity |
Enum: error, warning, info |
LintConfig |
Lint rule configuration |
All types are importable from the top-level grimoire package:
from grimoire import (
Grimoire, GrimoireRepo, ConjureEngine,
Spell, RuneSpec, Bundle, Ritual,
ConjuredPrompt, ConjuredRitualStep,
VariableSpec, VariableType,
BindResult, BindTarget,
ArtifactNotFoundError, MissingVariableError,
)See Layered Control Plane: Grimoire.layered(), CompositeRepoView over every artifact type, strict loading, validate(layer=...), deterministic discovery, memoized catalog/tool schemas, lazy llmcore import in grimoire.federation.
See Intent Discovery, MCP Tool Manifest & Server and OWASP Metadata & Federation; rune command schema helpers live in grimoire.runes.schema (command_to_openai_tool_schema, command_parameters_schema).
Spell.attributes is a declared, opaque dict[str, Any] that grimoire stores
and serves verbatim but never interprets. It lets a downstream application
attach arbitrary structured data to a spell without grimoire growing
app-specific fields:
---
id: convergence/personas/skeptic
name: Skeptic
tags: [convergence, "pack:creative"]
attributes:
color: "#cc3333"
constraints: [challenge assumptions, cite evidence]
---
# SYSTEM
You are a relentless skeptic.attributes is excluded from content_hash (it is metadata, not prompt
body), so editing it does not register as prompt drift.
g = Grimoire("/path/to/repo")
# OR semantics (default is AND):
g.list_spells(tags=["analysis", "creative"], match="any")
# Distinct tag vocabulary with counts (ordered by count desc, then name):
g.list_tags() # {"convergence": 7, "pack:creative": 6, ...}
g.list_tags(prefix="pack:") # only tags starting with "pack:"
# Case-insensitive substring search:
g.search_spells("skeptic")
g.search_spells("review", fields=("name", "description"))match="all"|"any" is available on every list_* method (spells, runes,
rituals, bundles, skilldocs).
from grimoire import Grimoire, Spell, MessageBlock, MessageRole, serialize_spell
g = Grimoire("/path/to/repo")
spell = Spell(
id="team/reviewer",
name="Reviewer",
tags=["code"],
attributes={"color": "#0a0"},
raw_blocks=[MessageBlock(role=MessageRole.SYSTEM, content="You are a reviewer.")],
)
path = g.write_spell(spell) # atomic write + hot index; refuses clobber
g.update_spell(spell) # overwrite
g.delete_spell("team/reviewer") # remove file + index entry
text = serialize_spell(spell) # canonical .spell.md (round-trips with parse_spell)GrimoireRepo.load(path, writable=False) produces a read-only repo whose
write/delete operations raise RepoError.
LayeredGrimoire composes ordered repos so a read-only shipped library can be
overridden by writable admin / user overlays without mutating shipped files:
from grimoire import LayeredGrimoire
lg = LayeredGrimoire.from_roots([
("shipped", "/opt/app/spells", False), # read-only
("admin", "/var/lib/app/admin", True), # writable (scaffolded if missing)
("user", "/var/lib/app/users/alice", True), # writable, highest precedence
])
lg.get_spell("convergence/personas/skeptic") # highest-precedence layer wins
lg.resolve_layer("convergence/personas/skeptic") # -> "shipped" / "admin" / "user"
# Writing an overlay shadows the shipped spell with the same id:
lg.write_spell(my_overlay_spell, layer="user")
# Deleting the overlay un-shadows the shipped definition again:
lg.delete_spell("convergence/personas/skeptic", layer="user")
lg.list_spells(match="any", tags=["custom"]) # merged, deduped-by-id view
lg.list_tags() # vocabulary over resolved viewLayers are ordered lowest → highest precedence. Writes target a named writable layer or, by default, the highest-precedence writable layer.