Kits & Toolbox¶
Toolbox vs Kits¶
| Concept | What it is | Scope |
|---|---|---|
| Toolbox | The global registry of all available tools and their providers | Service-wide |
| Kit | A named subset of toolbox tools for a specific task | Per-request |
The Toolbox holds every tool that has been registered across all providers. A Kit is the subset of those tools that a particular program may call — it defines the allowed namespace for validation and the callable namespace for execution.
ToolSpec fields¶
ToolSpec is the metadata record for a single tool:
| Field | Type | Description |
|---|---|---|
name |
str |
The function name used in lackpy programs |
provider |
str |
Provider name that resolves this tool (e.g. "builtin", "python") |
provider_config |
dict |
Provider-specific config (e.g. module, function for the python provider) |
description |
str |
Human-readable description, shown to LLMs in the system prompt |
args |
list[ArgSpec] |
Argument names, types, and descriptions |
returns |
str |
Return type annotation string |
grade_w |
int |
World coupling (0–3) |
effects_ceiling |
int |
Effects ceiling (0–3) |
docs |
str \| None |
Relative path to a markdown documentation file |
ArgSpec fields: name, type (string), description.
Registering tools¶
Tools are registered by adding a ToolSpec to the Toolbox and ensuring a matching provider is also registered:
from lackpy import LackpyService
from lackpy.kit.toolbox import ToolSpec, ArgSpec
svc = LackpyService()
# Register a custom tool backed by a Python function
svc.toolbox.register_tool(ToolSpec(
name="count_lines",
provider="python",
provider_config={
"module": "my_tools",
"function": "count_lines",
},
description="Count the number of lines in a file",
args=[ArgSpec(name="path", type="str", description="File path")],
returns="int",
grade_w=1,
effects_ceiling=0,
))
The python provider is always registered. It resolves tools by importing the named module and looking up the function.
Provider table¶
| Provider | Name | How it resolves tools |
|---|---|---|
BuiltinProvider |
"builtin" |
Hardcoded implementations for read_file, find_files, write_file, edit_file |
PythonProvider |
"python" |
importlib.import_module(module) then getattr(module, function) |
| Custom | any string | Implement the provider protocol (see Tool Providers) |
Kit parameter forms¶
resolve_kit() accepts these kit forms:
| Form | Type | Example | Behaviour |
|---|---|---|---|
| Named kit | str |
"filesystem" |
Loads .lackpy/kits/filesystem.kit |
| Empty kit | str |
"none" |
No base tools (use with extra_tools) |
| Tool list | list[str] |
["read_file", "find_files"] |
Uses tool names directly as aliases |
| Tool mapping | dict |
{"find": "find_files"} |
Alias → actual tool name |
| Nested dict | dict |
{"r": {"tool": "read_file"}} |
Dict entry with "tool" key |
With the tool mapping form, the program sees find(...) but the toolbox resolves it to the find_files implementation.
Extra tools¶
All kit forms support an optional extra_tools parameter — a list of tool names merged into the resolved kit:
# Named kit + extra tools
kit = resolve_kit("debug", toolbox, extra_tools=["edit_file"])
# Standalone tools (no base kit)
kit = resolve_kit("none", toolbox, extra_tools=["read_file", "find_files"])
Duplicates are silently ignored. The kit grade is recomputed after merging.
Kit file format¶
Named kits are stored as .kit files in .lackpy/kits/:
---
name: filesystem
description: Read, write, and search files
---
read_file
find_files
write_file
edit_file
- The YAML-like frontmatter between
---lines is metadata. - Lines after the closing
---are tool names, one per line. - Lines starting with
#are treated as comments. - Supported frontmatter fields:
name,description,docs.
Kit-level documentation¶
Kits can reference documentation files via the docs frontmatter field:
---
name: filesystem
description: Read, write, and search files
docs: docs/kits/filesystem.md
---
read_file
find_files
write_file
edit_file
The docs path is relative to the workspace root. It is not loaded at resolution time — consumers (like Kibitzer) query the reference and read the file on demand.
CLI management¶
# List all kits in .lackpy/kits/
lackpy kit list
# Show tools and grade for a kit
lackpy kit info filesystem
# Show tools and grade for an ad-hoc list
lackpy kit info read_file,find_files,write_file
# Create a new kit
lackpy kit create mykit --tools read_file find_files --description "Read-only tools"
Grade computation¶
compute_grade(tools) takes a dict of {name: {"grade_w": int, "effects_ceiling": int}} and returns the element-wise maximum across all tools:
from lackpy import compute_grade
grade = compute_grade({
"read_file": {"grade_w": 1, "effects_ceiling": 0},
"write_file": {"grade_w": 3, "effects_ceiling": 3},
})
# Grade(w=3, d=3)
This grade is attached to every ResolvedKit and reported in delegate() results. The grade is informational — lackpy does not block execution based on grade values, but callers can use it to gate access in security-sensitive contexts.
Tool documentation¶
Tools and kits can reference markdown documentation files. These references are stored as relative paths and resolved lazily — nothing is loaded into memory until a consumer explicitly reads the file.
How it works¶
-
ToolSpec has an optional
docsfield — a path relative to the package/workspace root: -
Kit files have an optional
docsfrontmatter field for kit-level documentation. -
At resolution time,
ResolvedKitcollects all doc references (from both the kit file and individual tools) into adocslist. -
Consumers query, not load: the service exposes
docs_index()(returns the reference map) andresolve_doc()(reads a specific file on demand).
API¶
# Get the docs index for a kit
index = svc.docs_index(kit="debug", extra_tools=["edit_file"])
# {"tool_docs": {"read_file": "docs/tools/read_file.md", ...}, "kit_docs": [...]}
# Read a specific doc file
content = svc.resolve_doc("docs/tools/read_file.md")
Kibitzer integration¶
Kibitzer (v0.4+) uses the doc index to surface relevant documentation during error correction. The integration has three parts:
1. Doc registration at init¶
When LackpyService initializes a Kibitzer session, it registers all tool doc references:
# Automatic — happens in LackpyService._init_kibitzer():
docs = svc.docs_index()
kibitzer_session.register_docs(
docs["tool_docs"],
docs_root=str(workspace),
refinement=build_doc_refinement(),
)
This gives Kibitzer a searchable index of doc paths without loading any files into memory.
2. Doc refinement callbacks¶
Lackpy provides a DocRefinement with a select callback that picks doc sections relevant to each failure mode. When Kibitzer retrieves documentation (via pluckit), the select callback filters to the most useful sections:
| Failure mode | Sections selected |
|---|---|
stdlib_leak |
Signature, Parameters, Notes |
implement_not_orchestrate |
Signature, Examples |
key_hallucination |
Signature, Returns, Notes |
path_prefix |
Notes, Parameters |
wrong_output |
Signature, Returns, Examples |
This keeps the doc context concise — a 1.5B model gets the signature and one relevant section, not the full doc file.
3. Doc context in the correction chain¶
When a generation fails, the correction chain calls get_correction_hints(failure_mode, model, attempt, tool). The tool parameter is inferred from the error — e.g., a Forbidden name: 'open' error maps to read_file.
Kibitzer returns a doc_context field containing the selected doc sections. The correction chain folds these into the error enrichment:
--- From tool documentation ---
Signature: read_file(path: str) -> str
Notes: - Raises FileNotFoundError if the path does not exist. ...
This gives the small model targeted documentation about the tool it should have used, rather than generic hints or a raw dump of all Kibitzer patterns.
External access¶
External tools (pluckit, fledgling) can query the MCP docs_index and resolve_doc endpoints to search tool documentation independently of the correction pipeline.