Skill composition for LLMs — like lambda calculus, but the functions talk back
Project description
tk.llmbda
Skill composition for LLM pipelines. Chain deterministic and LLM-powered steps into a skill; the runtime walks them in order until one resolves.
Deterministic skill
from tk.llmbda import Skill, SkillContext, StepResult, run_skill
def greet(ctx: SkillContext) -> StepResult:
name = ctx.entry.get("name", "world")
return StepResult(value=f"hello, {name}")
skill = Skill(name="greeter", steps=[Skill("greet", fn=greet)])
result = run_skill(skill, {"name": "λ"})
# SkillResult(skill="greeter", resolved_by=("greet",), value="hello, λ", ...)
LLM skill
Self-contained with a fake model so the snippet runs as-is:
from tk.llmbda import Skill, SkillContext, StepResult, lm, run_skill
def fake_model(*, messages, **_):
return "2025-01-15"
@lm(fake_model, system_prompt="Extract a date. Return ISO format.")
def extract_date(ctx: SkillContext, call) -> StepResult:
"""Extract a date from natural language."""
raw = call(messages=[{"role": "user", "content": ctx.entry["text"]}])
return StepResult(value=raw)
skill = Skill(name="dates", steps=[Skill("extract", fn=extract_date)])
result = run_skill(skill, {"text": "let's meet on the 15th of January 2025"})
# SkillResult(skill="dates", resolved_by=("extract",), value="2025-01-15", ...)
Multi-step with ctx.prev
Each step can access the previous step's result via ctx.prev:
from tk.llmbda import Skill, SkillContext, StepResult, run_skill
def step_a(ctx: SkillContext) -> StepResult:
return StepResult(value=ctx.entry["x"] * 2)
def step_b(ctx: SkillContext) -> StepResult:
return StepResult(value=ctx.prev.value + 10)
skill = Skill(name="math", steps=[Skill("a", fn=step_a), Skill("b", fn=step_b)])
result = run_skill(skill, {"x": 5})
# result.value == 20
Before any step runs, ctx.prev is the ROOT sentinel (ROOT.value is None).
Use ctx.prev is ROOT to check if you're the first step.
Short-circuit with resolved=True
Steps fall through by default (resolved=False). Set resolved=True to
stop early — remaining steps are skipped:
def try_cache(ctx: SkillContext) -> StepResult:
if ctx.entry.get("key") in CACHE:
return StepResult(value=CACHE[ctx.entry["key"]], resolved=True)
return StepResult(value=None)
def expensive(ctx: SkillContext) -> StepResult:
return StepResult(value=compute(ctx.entry))
skill = Skill(name="s", steps=[Skill("cache", fn=try_cache), Skill("compute", fn=expensive)])
Nested composition
Skill is the single composition primitive. Leaf (has fn), composite
(has steps), or orchestrator (has both):
skill = Skill(
name="analyzer",
steps=[
Skill("preprocess", steps=[
Skill("normalize", fn=normalize),
Skill("count_words", fn=count_words),
]),
Skill("classify", steps=[
Skill("tag_length", fn=tag_length),
]),
],
)
The runtime walks composites via DFS. All leaves share a single ctx.trace.
Orchestrator: fn + steps
A skill with both fn and steps is an orchestrator — fn receives the
children as an explicit second argument and controls how they execute:
def retry(ctx: SkillContext, steps: list[Skill]) -> StepResult:
"""Run children up to 3 times until valid."""
inner = Skill(name="inner", steps=steps)
for attempt in range(1, 4):
r = run_skill(inner, ctx.entry)
if r.metadata.get("valid"):
return StepResult(value=r.value, metadata={"attempts": attempt}, resolved_by=r.resolved_by)
return StepResult(value=r.value, metadata={"valid": False}, resolved_by=r.resolved_by)
skill = Skill(
name="retry",
fn=retry,
steps=[
Skill("ψ::extract", fn=extract_date_llm),
Skill("ψ::verify", fn=verify_date),
],
)
- Leaf fns are
(ctx) -> StepResult. Orchestrator fns are(ctx, steps) -> StepResult. - Children run in a fresh
SkillContext(viarun_skill), so they don't see the outer trace. Pass data throughentryif needed.
Static validation
check_skill catches trace-key typos and forward references at definition
time via AST analysis:
from tk.llmbda import check_skill
issues = check_skill(skill)
# ["'bad_step' references undeclared trace key 'typo'"]
- Validates orchestrator children as a separate scope.
- Checks
ctx.trace["key"]andctx.trace.get("key")patterns.
Concepts
Skill— recursive composition primitive. Leaf (fn), composite (steps), or orchestrator (fn+steps).StepResult.resolved— defaults toFalse; steps fall through. SetTrueto short-circuit.StepResult.resolved_by— inner resolution path astuple[str, ...]. Orchestrators propagate it from nestedrun_skillcalls;SkillResult.resolved_byprepends the step name, building a hierarchical path like("orchestrator", "inner_step").ctx.prev— most recently executed step'sStepResult. Starts asROOT(value=None).ctx.trace— dict of all prior results keyed by step name. Raises informativeKeyErroron miss; use.get()for optional lookups.ctx.entry— the original input passed torun_skill.@lm(model, system_prompt=...)— binds model at decoration time. Decorated fn is(ctx, call)for leaves or(ctx, steps, call)for orchestrators;callprependssystem_prompt.Skill.description— human-readable summary; falls back to fn docstring. Separate from@lmsystem prompts (read those viafn.lm_system_prompt).StepResult.metadata— auxiliary context: reasons, raw provider output, confidence.iter_skill— same asrun_skillbut yields(name, result)per step for streaming or early exit.check_skill— static validation of trace-key references. Catches typos and forward refs.- Test re-binding —
lm(fake)(my_step.__wrapped__)re-decorates with a different model.
Examples
# deterministic + LLM date extraction with retry
OPENAI_API_KEY=sk-... uv run examples/readme.py
# calendar booking: regex parsers + LLM verifier
uv run examples/calendar_booking.py
# support triage: extraction, classification, validation loop
uv run examples/support_triage.py
# all 20 use cases in one file (no external deps)
uv run examples/showcase.py
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tk_llmbda-0.2.0.tar.gz.
File metadata
- Download URL: tk_llmbda-0.2.0.tar.gz
- Upload date:
- Size: 29.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ee8322eba6a30b7a91bac3455807f54679f96c93082d8a188afdc832005fcff
|
|
| MD5 |
02bbda57abc002f169f4f56fdbf36eff
|
|
| BLAKE2b-256 |
56157e2031c469025644ba67f98580a64fa09846a5ded5493338fbd4f93e78b2
|
Provenance
The following attestation bundles were made for tk_llmbda-0.2.0.tar.gz:
Publisher:
ci.yml on tkukurin/llmbda
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tk_llmbda-0.2.0.tar.gz -
Subject digest:
4ee8322eba6a30b7a91bac3455807f54679f96c93082d8a188afdc832005fcff - Sigstore transparency entry: 1395025897
- Sigstore integration time:
-
Permalink:
tkukurin/llmbda@7182740b8987d3d8ea103eedbca56ca9ca84d7e4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/tkukurin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@7182740b8987d3d8ea103eedbca56ca9ca84d7e4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file tk_llmbda-0.2.0-py3-none-any.whl.
File metadata
- Download URL: tk_llmbda-0.2.0-py3-none-any.whl
- Upload date:
- Size: 8.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bdddb30e868bcc12c125055fab2ab39c23bab806c9c6ad77187842a4755cc8d
|
|
| MD5 |
4734e05bbc7f515da393b1472610cfe6
|
|
| BLAKE2b-256 |
3c30096a6b83a53e5434dfc2e2ae73068ea4a2fa4b3f52b49ef95a41c2e90366
|
Provenance
The following attestation bundles were made for tk_llmbda-0.2.0-py3-none-any.whl:
Publisher:
ci.yml on tkukurin/llmbda
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tk_llmbda-0.2.0-py3-none-any.whl -
Subject digest:
9bdddb30e868bcc12c125055fab2ab39c23bab806c9c6ad77187842a4755cc8d - Sigstore transparency entry: 1395025974
- Sigstore integration time:
-
Permalink:
tkukurin/llmbda@7182740b8987d3d8ea103eedbca56ca9ca84d7e4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/tkukurin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@7182740b8987d3d8ea103eedbca56ca9ca84d7e4 -
Trigger Event:
push
-
Statement type: