Capability Registry & Dynamic Tool Creation
Mix.install([
{:ptc_runner, path: Path.expand("..", __DIR__)},
{:kino, "~> 0.12.0"}
])
alias PtcRunner.CapabilityRegistry.{Registry, Linker, Skill, ToolEntry, Discovery, Promotion}
alias PtcRunner.MetaPlanner
alias PtcRunner.PlanRunner
Introduction
The Capability Registry is a central repository for:
- Base Tools: Raw Elixir functions documented with signatures.
- Composed Tools: Reusable PTC-Lisp programs smithed from common patterns.
- Skills: Expert advice and system prompt fragments that improve agent performance for specific tasks.
This Livebook demonstrates how to use the registry to provide a “Context Economy”—where agents only see the tools and expertise they actually need—and how to dynamically grow the registry.
Setup
First, let’s create a registry and a mock LLM for our experiments.
registry = Registry.new()
# A mock LLM that generates simple plans
mock_llm = fn input ->
prompt = hd(input.messages).content
cond do
String.contains?(prompt, "Search for products") ->
{:ok, ~S|{
"tasks": [
{"id": "search", "agent": "shopper", "input": "Search for electronics", "signature": "{items [{name :string, price :float}]}"},
{"id": "summarize", "type": "synthesis_gate", "input": "Summarize top 2 items", "depends_on": ["search"], "signature": "{summary :string}"}
],
"agents": {
"shopper": {"prompt": "You search for products.", "tools": ["search_catalog"]}
}
}|}
true ->
{:ok, "{\"tasks\": [], \"agents\": {}}"}
end
end
1. Registering Base Tools
Base tools are the building blocks. They are Elixir functions with metadata.
search_fn = fn %{query: q} ->
[%{name: "Phone", price: 699.0}, %{name: "Laptop", price: 1299.0}]
end
registry = registry
|> Registry.register_base_tool(
"search_catalog",
search_fn,
signature: "(query :string) -> [{name :string, price :float}]",
description: "Search the product catalog by keyword.",
tags: ["shopping", "search", "products"]
)
Kino.Tree.new(registry.tools["search_catalog"])
2. Context-Aware Discovery
The registry can find tools based on tags and descriptions. This allows the Meta Planner to “discover” what it needs without knowing the full toolset upfront.
results = Discovery.search(registry, "Find some gadgets", context_tags: ["shopping"])
Kino.DataTable.new(results)
3. Dynamic Tool Creation (Composed Tools)
Composed tools are programs written in PTC-Lisp that use other tools. They can be dynamically “smithed” and added to the registry.
Imagine an agent discovers that it frequently needs to “Search and Filter by Price”. It can create a composed tool for this.
composed_code = "(defn search-and-filter [q max-p]
(filter (fn [i] (<= (get i :price) max-p))
(tool/search_catalog {:query q})))"
registry = Registry.register_composed_tool(
registry,
"budget_search",
composed_code,
signature: "(query :string, max_price :float) -> [{name :string, price :float}]",
description: "Search products and filter those under a specific budget.",
tags: ["shopping", "budget"],
dependencies: ["search_catalog"]
)
Kino.Tree.new(registry.tools["budget_search"])
4. Skills (Expert Knowledge)
Skills provide system prompt fragments. The Linker automatically injects them into the agent’s prompt if the mission context or the selected tools match the skill’s interests.
shopping_skill = Skill.new(
"shopper_tips",
"Shopping Expertise",
"Always look for the best price-to-performance ratio. For electronics, mention the warranty.",
tags: ["shopping"]
)
registry = Registry.register_skill(registry, shopping_skill)
5. Linking: The Context Economy in Action
When we start a mission, the Linker gathers only the required tools and skills. This keeps the prompt clean and reduces “distraction” for the LLM.
# Requesting 'budget_search' with context 'shopping'
{:ok, linked} = Linker.link(registry, ["budget_search"], context_tags: ["shopping"])
# Notice how:
# 1. Base tools (search_catalog) are included as dependencies.
# 2. Composed tools are converted to a Lisp prelude.
# 3. The shopping skill is automatically included.
IO.puts("--- Skill Prompt ---\n#{linked.skill_prompt}\n")
IO.puts("--- Lisp Prelude ---\n#{linked.lisp_prelude}\n")
6. Full Execution with Meta Planner
Now let’s see how all this comes together. The PlanRunner uses the registry option to resolve everything on the fly.
# 1. Generate a plan (MetaPlanner)
{:ok, plan} = MetaPlanner.plan("Search for products in the electronics category",
llm: mock_llm,
available_tools: %{"search_catalog" => "Search products"}
)
# 2. Execute the plan (PlanRunner)
# We pass the registry here!
{:ok, results} = PlanRunner.execute(plan,
llm: mock_llm,
registry: registry,
context_tags: ["shopping"]
)
Kino.Tree.new(results)
7. Learning from Usage (Promotion)
The registry can track patterns. If a specific plan structure is used successfully multiple times, it can be flagged for “Promotion” into a permanent tool or skill.
# Track a success
registry = Promotion.track_pattern(registry, plan, :success, mission: "Search products")
# Check if anything is ready for promotion (using a low threshold for the demo)
candidates = Promotion.check_promotion_threshold(registry, threshold: 1)
case candidates do
[hash | _] ->
candidate = Promotion.get_candidate(registry, hash)
IO.puts("Found candidate pattern for promotion!")
Kino.Tree.new(candidate)
[] ->
IO.puts("No candidates yet.")
end