API Reference¤
Everything is imported from the top-level package:
from jdsl import tool, root, seq, sel, repeat, act, check, predict, react, ref, store
from jdsl import invert, optional, timeout, oneshot, render # decorators + rendering
Building Blocks¤
@tool¤
Register a Python callable as a tool.
@tool
def greet(name: str) -> None: ...
@tool(description="Search Wikipedia and return titles.")
def search(query: str) -> list[str]: ...
Usable bare (@tool) or with metadata (@tool(name=..., description=...)). The
wrapped object is still directly callable. The name/description and the function's
type hints are what react exposes to the model for function-calling,
so annotate tool params and write a clear description.
root(name, *, system=None) -> Root¤
The entry point of a skill. Returns a Root that is also a small builder:
skill = root("Triage", system="You classify messages.").model("deepseek-chat").do(child)
.model(model_id)sets the model; the provider is inferred from the id (see providers.md)..do(child)sets the single child node..run(*, model=None, **inputs) -> RunContextexecutes.modelmay be aLanguageModel; if omitted and the skill needs one, it's built from stored credentials.inputsseed the blackboard.
seq(*children, context=None) -> Sequence¤
Run children in order; return FAILURE at the first child that fails, else
SUCCESS. Optional context= pushes a system fragment for this subtree.
sel(*children, context=None) -> Selector¤
Run children in order; return SUCCESS at the first child that succeeds, else
FAILURE.
repeat(child, *, until=None, max=3, context=None) -> Repeat¤
Run child up to max times, checking until (a node, usually a check) after
each pass and stopping early when it succeeds. SUCCESS when until is satisfied
or when there's no until (a fixed max-times loop). FAILURE if max is
reached unsatisfied, or if child fails (which aborts immediately). Do-while, not
while: the body runs before the first until check.
repeat(
seq(predict("draft -> critique, ok"), sel(check("ok", "yes"), predict("draft, critique -> draft"))),
until=check("ok", "yes"), max=3,
)
act(fn, *args, **kwargs) -> Action¤
A leaf that calls fn(*args, **kwargs).
- Literal args are type-checked against
fn's signature (ParamSpec). - A
ref("key")arg is resolved from the blackboard at run time. - Return value handling: a returned
Statusis honored (so a tool can reportFAILUREto a parent selector); otherwise the node succeeds, and if wrapped instore, the return value is written to the blackboard.
ref(name) -> Ref¤
A placeholder for blackboard[name], resolved when the action runs. Raises
KeyError (with guidance) if the key isn't present yet.
act(fetch, ref("selected_title"))
store(action, key) -> Action¤
Capture an action's return value onto the blackboard under key. Returns the
same action for inline use:
store(act(search, ref("query")), "titles")
check(key, equals) -> Check¤
Guard leaf: SUCCESS iff blackboard[key] matches equals, else FAILURE. Put
one at the head of a seq inside a sel to branch on a predict output.
String matches are lenient: case-insensitive, and whitespace / surrounding
punctuation are trimmed — because the value is usually fuzzy model text: "Yes.",
" YES " and "yes" all match check("ok", "yes"). Non-string values (ints,
etc.) compare with plain ==, so check("n", 2) does not match "2".
guard(expression, *, id=None) -> Guard¤
A general predicate over the blackboard using the restricted expression language:
guard({"in": [{"ref": "order.status"}, ["pending", "processing"]]})
Supported operators are exists, eq, neq, lt, lte, gt, gte, in,
and, or, and not. Refs support dotted and bracket paths, including dynamic
indexes such as orders[$selected_index].id.
guard_call(predicate, arguments=None, *, predicate_id=None, id=None) -> GuardCall¤
A guard backed by a trusted runtime predicate:
guard_call(is_cancellable, {"order": ref("order")}, predicate_id="retail.cancellable")
Compiled packages name the predicate capability; they do not ship arbitrary Python code.
predict(signature, *, instructions=None, context=None) -> Predict¤
DSPy-style LLM leaf. signature is "in1, in2 -> out1, out2". Reads the input
fields from the blackboard and writes the outputs back. A single output is
stored as the model's raw reply (no JSON envelope, which otherwise makes the model
reason about the wrapper); multiple outputs are requested as one JSON object
and parsed leniently (tolerates surrounding prose). Returns FAILURE if nothing
parseable came back. instructions prepends task guidance to the prompt. Each
leaf is stateless: state flows through the blackboard, not the message list.
react(signature, *, tools, instructions=None, max_steps=6, context=None) -> React¤
Agentic LLM leaf. Where predict is one shot with no tools, react lets the
model pick and call the given @tools in a loop through native provider
function-calling (Anthropic tool_use / OpenAI tool_calls), not text parsing.
Each tool's JSON schema is derived from its signature: str/int/float/bool
map to their JSON types, list[T]/tuple/set become an array (with an
items type from T), anything else falls back to string; args without a
default are required. The loop: ask the model, run the tools it calls, feed
results back, and repeat until it returns a final answer (written to
the single output field) or max_steps is hit (FAILURE). FAILURE also if the
final answer is empty.
signature must have exactly one output — the answer. An unknown tool name is
reported back to the model as an observation rather than raising, so it can
recover.
@tool
def population(city: str) -> int:
"""The population of a city."""
...
react("question -> answer", tools=[capital_of, population, multiply], max_steps=6)
Decorators¤
Single-child wrappers that transform a child's status without adding a new composite type.
invert(child) -> Invert¤
Flip the child's status: SUCCESS ↔ FAILURE. Handy for negative guards:
invert(check("category", "spam")).
optional(child) -> Optional¤
Fail-soft: run the child but always return SUCCESS, so a failing step never
aborts its parent seq. This is the clean way to make a sequence step "best
effort" (py_trees calls it FailureIsSuccess).
timeout(child, *, seconds=30.0) -> Timeout¤
Run the child with a wall-clock bound; FAILURE if it overruns. The child runs
in a worker thread and is abandoned on timeout (Python can't kill it), so use
it for read-only / idempotent work such as an LLM or lookup call.
oneshot(child) -> OneShot¤
Run the child at most once per run, latching and replaying its status on later
ticks. Only observable inside a repeat loop; state is per-run (a fresh
run(...) starts over).
Rendering¤
render(node) -> str¤
An ASCII-tree rendering of a skill's structure. It does not execute the tree or
call a model. Also
available as jdsl show <file> on the CLI.
root 'Triage' [deepseek-chat]
└─ seq
├─ predict(message -> category)
└─ sel
├─ seq
│ ├─ check(category == 'billing')
│ └─ act(route_to_billing)
└─ optional
└─ act(escalate)
Runtime objects¤
RunContext¤
Returned by skill.run(...). Fields:
blackboard: Blackboard: the shared store; read your results here.window: ContextWindow: accumulated system fragments.model,model_id: the model backing this run.state: dict: per-run scratch for stateful nodes, such asoneshot.
Blackboard¤
A dict with provenance. Read with normal dict access; every write is
recorded so you can trace and debug a run:
set(key, value, *, writer=...): write with attribution (nodes pass their own label aswriter; plainbb[key] = vrecordswriter="?").activity: list[Write]: every write, in order (key,value,writer,previous,overwrote).who_wrote(key) -> str | None: the last writer of a key.clobbers() -> list[Write]: writes that overwrote a key a different writer had set. This catches the silent-clobber bug where two leaves share an output name.
LanguageModel¤
The provider-dispatching backend. LanguageModel.from_config() builds one from
stored credentials / .env. generate(*, system, messages, model_id) -> str.
See providers.md.
Status¤
Status.SUCCESS / Status.FAILURE. Truthy for SUCCESS, so if node.tick(ctx):
reads naturally. A @tool may return a Status to signal failure to a parent
selector.