Tutorial¶
This walkthrough covers every major feature of lackpy. Follow along in order, or jump to any section.
Prerequisites: lackpy installed, workspace initialized with lackpy init. See Getting Started.
1. Setup¶
Create a project directory and initialize:
Confirm the setup:
{
"workspace": "/path/to/my-lackpy-project",
"config_dir": "/path/to/my-lackpy-project/.lackpy",
"inference_order": ["templates", "rules"],
"kit_default": "debug",
"sandbox_enabled": false,
"tools": 4
}
2. Pipeline overview¶
Every delegate call passes through this pipeline:
┌─────────────────────────────────────────┐
intent │ InferenceDispatcher │
─────────────────►│ │
│ tier 0: templates (pattern match) │
│ tier 1: rules (keyword rules) │
│ tier 2: ollama (local LLM) │
│ tier 3: anthropic (cloud LLM) │
└──────────────┬──────────────────────────┘
│ raw program text
▼
┌──────────────────────────┐
│ Validator (AST check) │
│ - allowed node types │
│ - forbidden names │
│ - namespace check │
│ - custom rules │
└──────────────┬───────────┘
│ ValidationResult
▼
┌──────────────────────────┐
│ RestrictedRunner │
│ - traced namespace │
│ - restricted __builtins__│
└──────────────┬───────────┘
│ ExecutionResult + Trace
▼
delegate() result
The tier system means simple intents are handled deterministically and fast; LLMs are only invoked when no cheaper tier can handle the request.
3. Validating programs¶
The validator checks programs before they run. It operates entirely on the AST — no code is executed during validation.
A valid program¶
cat > check.py << 'EOF'
files = find_files("**/*.py")
count = len(files)
count
EOF
lackpy validate check.py --kit find_files
An invalid program¶
The following example shows what the validator rejects. This program uses import, which is a Forbidden AST node:
{
"valid": false,
"errors": [
"Forbidden AST node: Import at line 1",
"Unknown function: 'sys' at line 2 (not in kit or builtins)"
],
"calls": []
}
What gets rejected and why¶
| Construct | Reason |
|---|---|
import / from ... import |
Arbitrary module access |
def / class |
Code definition introduces opaque scope |
while |
Unbounded loops |
try / except |
Error suppression can hide security violations |
lambda |
Anonymous functions bypass namespace checks |
open, getattr, setattr |
Direct resource or reflection access |
Strings containing __ |
Prevents getattr(obj, "__class__") patterns |
| Unknown function names | Every call must be in the kit or allowed builtins |
Python API¶
from lackpy import LackpyService
svc = LackpyService()
result = svc.validate(
'files = find_files("**/*.py")\nlen(files)',
kit=["find_files"],
)
print(result.valid) # True
print(result.errors) # []
print(result.calls) # ['find_files', 'len']
4. Working with kits¶
A kit is a named subset of tools from the toolbox. Kits control which function names are visible to the validator and runner.
List available tools¶
Use a comma-separated kit¶
Create a named kit¶
This creates .lackpy/kits/filesystem.kit:
Use the named kit¶
Kit info¶
{
"tools": {
"read_file": {"description": "", "grade_w": 3, "effects_ceiling": 3},
"find_files": {"description": "", "grade_w": 3, "effects_ceiling": 3},
"write_file": {"description": "", "grade_w": 3, "effects_ceiling": 3}
},
"grade": {"w": 3, "d": 3},
"description": " read_file(path) -> Any: \n find_files(pattern) -> Any: \n write_file(path, content) -> Any: "
}
Python API — kit forms¶
svc = LackpyService()
# Named kit (from .lackpy/kits/filesystem.kit)
result = await svc.delegate("find all Python files", kit="filesystem")
# List of tool names
result = await svc.delegate("find all Python files", kit=["find_files"])
# Dict with aliases
result = await svc.delegate(
"find all Python files",
kit={"ls": "find_files"}, # calls are `ls(...)` in the program
)
5. Generating programs¶
generate runs the inference pipeline without executing the result:
Inference tiers¶
Templates are matched first. If a template pattern matches the intent, the stored program is returned — no LLM required.
The rules tier uses keyword-based matching for common patterns. It handles intents like "read file X", "find definitions of Y", "glob Z".
If templates and rules fail, Ollama is tried. Requires pip install lackpy[ollama] and a running Ollama server.
Python API¶
result = await svc.generate("find all Python files", kit=["find_files"])
print(result.program) # the generated program
print(result.provider_name) # which tier produced it
print(result.generation_time_ms)
6. Running programs directly¶
Use run when you already have a program file:
cat > list_py.py << 'EOF'
files = find_files("**/*.py")
files
EOF
lackpy run list_py.py --kit find_files
The program is validated before execution. If validation fails, the runner returns an error without running anything.
Python API¶
result = await svc.run_program(
'files = find_files("**/*.py")\nfiles',
kit=["find_files"],
)
print(result.success)
print(result.output)
print(result.trace.entries) # list of TraceEntry
7. Using parameters¶
Parameters let you pass values into programs without interpolating them into the intent string:
result = await svc.delegate(
intent="read the target file",
kit=["read_file"],
params={
"target_file": {
"value": "README.md",
"type": "str",
"description": "file to read",
}
},
)
Inside the program, target_file is available as a pre-set variable:
Name collisions
Parameter names must not collide with tool names or allowed builtins. LackpyService raises ValueError if they do.
8. Creating templates (the ratchet)¶
Once you have a working program, save it as a template to make future matching deterministic:
cat > read_file.py << 'EOF'
content = read_file('{path}')
content
EOF
lackpy create read_file.py \
--name read-file \
--pattern "read the file {path}" \
--kit read_file
This creates .lackpy/templates/read-file.tmpl:
---
name: read-file
pattern: "read the file {path}"
success_count: 0
fail_count: 0
---
content = read_file('{path}')
content
Now lackpy delegate "read the file README.md" matches at tier 0, with {path} substituted to README.md. No LLM is called.
Python API¶
result = await svc.create(
program="content = read_file('{path}')\ncontent",
kit=["read_file"],
name="read-file",
pattern="read the file {path}",
)
print(result["path"]) # .lackpy/templates/read-file.tmpl
9. Custom rules¶
Custom rules let you tighten the validator beyond the built-in checks. They're callables: ast.Module -> list[str].
from lackpy.lang.rules import no_loops, max_calls, max_depth, no_nested_calls
# Use in validate
result = svc.validate(
program,
kit=["find_files"],
rules=[no_loops, max_calls(5)],
)
# Use in delegate (enforced on the generated program)
result = await svc.delegate(
"find all Python files",
kit=["find_files"],
rules=[no_loops, max_depth(2)],
)
Built-in rules¶
| Rule | Description |
|---|---|
no_loops |
Rejects any for loop |
max_depth(n) |
Limits nesting depth (if/for/with) |
max_calls(n) |
Limits total number of function calls |
no_nested_calls |
Forbids using a call result directly as an argument |
See Custom Rules for writing your own.
10. Reading the trace¶
Every program run produces a Trace with an entry for each tool call:
result = await svc.run_program(
'files = find_files("**/*.py")\ncount = len(files)\ncount',
kit=["find_files"],
)
for entry in result.trace.entries:
print(
f"Step {entry.step}: {entry.tool}({entry.args}) "
f"-> {entry.result} ({entry.duration_ms:.1f}ms)"
)
len is a builtin, not a tool, so it doesn't appear in the trace. Only kit tools are traced.
Trace fields¶
| Field | Type | Description |
|---|---|---|
step |
int |
Call order (0-indexed) |
tool |
str |
Tool name |
args |
dict |
Arguments by name |
result |
Any |
Return value (truncated if large) |
duration_ms |
float |
Wall-clock call time |
success |
bool |
Whether the call succeeded |
error |
str \| None |
Exception message if failed |
The Trace also has files_read and files_modified lists, populated when tools are configured with effects metadata.
Next steps¶
- Concepts: Architecture — internals of the pipeline
- Concepts: Language Spec — full allowed/forbidden reference
- Concepts: Kits & Toolbox — provider system in depth
- Concepts: Inference Pipeline — tier system and config
- Extending: Custom Rules — write your own validation rules
- Extending: Tool Providers — register custom tools
- Extending: Inference Providers — add LLM backends