Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

RoboZ

Chain tools. Skip calls.

RoboZ is a framework for building llm powered agents. The main idea is that every tool may be chained conditionally to a subsequent tool thus allowing easy injection of deterministic flows into agentic processes.

CI PyPI Python versions License: Apache-2.0

Table of contents

Basic idea

Tool-chaining workflow

Problems to solve: Context bloat and too many llm calls

Suppose the task we want to achieve is ask our buddy Bob out to lunch and then book a table. For the sake of argument assume that our agent has access to the following tools presented as MCP servers (Note: this is an example, RoboZ has native Tool primitives):

  • Ask Bob what they want
  • Find a restaurant
  • Book a table.

In the usual approach an agent is presented each tool separately in their system prompt and it must call them one-by-one to complete the task. When the agent is completing the task, at every turn it must choose the correct tool, formulate its output accordingly and absorb the reply into its context, which already must contain the specific instructions on how to use each tool. In addition, at each turn one has to wait for the llm to reply, each reply costs tokens and each reply risks a mistake from the llm.

Deterministic chains

The philosophy in RoboZ is that often workflows are mostly deterministic and only on occasion does one need to call an llm. For example in RoboZ an agent would trigger the "ask Bob if they want to have lunch" tool and all subsequent steps come by way of chaining: each tool can be chained to other tools upstream where their outputs are passed down the chain. Each link/edge may introduce a True/False condition, in our case the condition is if Bob is interested in having lunch with us at all. If he is not, RoboZ allows for the chain to break and returns back to the default tool, which for an agentic process is usually "ask the llm what to do next". The default mode is that chained tools are not presented to the agent, they are thus passive or in other words their role is strictly in forming deterministic workflows and they cannot be invoked.

Chaining also provides a permission guard for tool calls. CLI commands and patches share the same permission checks. A denied operation cannot execute; a CLI sequence can continue to an independently guarded fallback.

Start here: Agent with a tool

from random import choice

from roboz import Agent, tool
from roboz.examples.simpsons_quotes import QUOTES
from roboz.llm.endpoints import MockLLMEndpoint
from roboz.models import Empty, Message, Stop


@tool
def get_quote(input: Empty, messages: list[Message]) -> Stop:
    """Return a random Simpsons quote and then stop."""
    return Stop(value=choice(QUOTES))


mock = MockLLMEndpoint(
    responses=[{"action": "get_quote", "rationale": "Need Simpsons quote!"}]
)

agent = Agent(
    name="demo",
    system_prompt="You are a Simpsons quote generator",
    agent_endpoint=mock,
    tools=[get_quote],
)

output, messages_ = agent.invoke()
print(f'"{output.value}"')

The above simple example uses the accompanying quote file. First add RoboZ to your environment as shown in Try it out, then run it with

uv run python -m roboz.examples.simple

It creates an agent that returns a random Simpsons quote. The docstring in the tool is the instruction that the agent sees. It uses a mock endpoint, with pre-determined replies, so you can run it without API keys. The main contracts of RoboZ are already visible:

  • @tool creates an instance of a usable tool for the agent
  • A tool's input and output are typed. Tools also receive the entire message stack as arguments. These are a fixed contract.
  • Callable endpoints are single instances, as a hard rule
  • Different output types impact the behavior, importantly Stop breaks out of the agentic loop
  • agent.invoke() runs the agent and returns its output Stop and messages.

The above does not show the main idea of tool chaining, for that read the following sections.

The central abstraction

An agent is a loop that calls tools. Everything is defined as a tool: Skills, background agents, prompting the LLM, prompting the user, running nested agents, start up hooks etc. Everything.

A tool can be triggered in three ways:

  • Invoked by an agent: The prompt_llm tool asks an LLM what to do next and its Invoke output always calls another tool. It is constructed internally for AgentMode.STEERABLE and AgentMode.AUTONOMOUS agents, but it is still just a tool.
  • By chaining. After an invoked tool has fired RoboZ checks if a chained tool with a true chain condition exists (for more than one true condition for a fork you get a runtime error). If yes, the output is passed on and the process repeats until the first broken chain or all chained tools are exhausted
  • As default tools. Defaults are called in order when no tools in a chain are left. A default cannot itself be chained to, so it cannot declare chained_to. The prompt_llm is then typically the last default tool for agentic processes.

The traditional agentic approach is then a special case of a RoboZ agent if one just has the prompt_llm as the default with no chaining. An AgentMode.DETERMINISTIC agent has no prompt_llm; its default tools perform tasks directly, allowing deterministic branching through chaining. This is useful for a background agent that performs periodic maintenance work. AgentMode.STEERABLE agents may ask the user for input, while AgentMode.AUTONOMOUS agents cannot.

API user input

Hosts bind a roboz.runtime.UserIO adapter with bind_api_user_io. Its request_input(message, timeout=None, *, accepts_images=False) method returns Str, StrImage, or None for a timeout. The keyword describes what the prompt can consume; hosts must also check the receiving model's image capability. Ordinary, initial, interruption, and retry prompts pass accepts_images=True. Permission prompts use the text-only default. Custom conversational tools may opt in through interact_with_user(..., accepts_images=True), independent of the tool's name or lifecycle events.

Why is this framework useful?

Tool Chaining

This may be used to reduce the number of llm calls, leading to a speed increase, lower cost and fewer AI errors. It also provides a useful way of introducing a guard layer for tool calls, which can be used to restrict agentic actions.

Output Truncation

A tool’s output can be hidden from the llm, also partially, and this can start to apply after the message has been shown N times.

Tool outputs are typed

The contract in RoboZ is that every action in the agentic loop is a tool call and all outputs are typed classes. Raw strings or JSON is never exchanged (unless explicitly opted in) as is and typed classes and validation are present throughout, with designated classes for tasks such as Invoke and Stop.

LLM Endpoints are instances

As a fundamental design rule in Roboz, everything that depends on an LLM call must be trivially swappable to another provider or model. This makes changing an agent endpoint trivial and furthermore multi-endpoint functionality, where inside a single agent several endpoints are implemented, quite easy.

To see the above in practice see the complex example below.

Chains, factories, truncation and many endpoints

Number-escalation workflow

In the code example below we illustrate some of the features that make RoboZ different from other frameworks.

Tool chaining is usually introduced via the decorator argument chained_to, which points from a downstream tool to the upstream tool whose output becomes its input (tools also possess a .chain method). The input/output contract must respect the class inheritance structure, so the upstream output must be a subclass of the downstream input. A possible chain_condition can be passed in, which by definition has access to the tool's input argument and returns a boolean. The chain condition must evaluate to at most one true condition, but it can evaluate to false on all links, in which case scheduling resumes with the next configured default. Defaults may be referenced as upstream parents without also appearing in tools, but cannot declare chained_to themselves. AgentMode.STEERABLE and AgentMode.AUTONOMOUS construct a prompt_llm tool backed by the agent endpoint; AgentMode.DETERMINISTIC uses the configured default tools.

The escalate is an example of a tool factory, which accepts context parameter ctx which is added to the tool's closure and calling the factory with a context argument returns a tool. A very common use case is a tool with an endpoint as a context. In RoboZ all llm endpoints are instances, so it is easy to have a specific endpoint for a tool, that is different from that of the agent, below we construct deterministic mock endpoints so that no API keys are required for the examples. Factories have precisely the same chaining arguments in their decorator as a tool.

Also demonstrated in the escalate factory is message truncation. This parameter is present in all Tool output types and allows the tool to decide if the output should be visible in the conversation passed on to the agent. A message can be truncated partially (show only n chars or just a caller stub) or completely. Importantly, we can choose to start applying the truncation only after the complete message has been shown to the agent n times. Below, we choose to show the message once and then truncate it completely, a useful pattern for example for long tracebacks etc.

All RoboZ tools by definition include the full conversation messages as input. These are not intended to be altered in place (although they can be and this is how e.g. conversation compactification works), but can be used to alter the behavior of tools in a non-trivial way. For example, a start up hook intended to show the agent some information at the start or performing some initial maintenance can simply be one of the default tools that in its body checks if it has already been called and if it has, does nothing.

The tool instruction is its docstring. In addition, the system prompt includes a technical prompt by default that gives the specific tool calling instructions. This can of course be switched off. When in doubt, you can always use the method .show_agent_info() to check the complete agent configuration including the system prompt, tools, dependencies, persistence locations etc.

from builtins import input as read_input

from roboz import Agent, factory, tool
from roboz.llm import EndpointLike, MockLLMEndpoint, get_completion
from roboz.models import Empty, Int, Message, Role, Stop, Str, filter_messages
from roboz.models.truncation import Severity, Truncation
from roboz.runtime.io import interact_with_user
from roboz.runtime.sinks import CliSink
from roboz.tools import stop


@tool
def ask_number(input: Empty, messages: list[Message]) -> Int:
    """Ask the user for an integer, repeating until the response is valid."""
    while True:
        reply = read_input("Enter an integer: ")
        try:
            return Int(value=int(reply))
        except ValueError:
            print("Please enter an integer :)")


@factory(chained_to=ask_number, chain_condition=lambda x: x.value % 2 == 0)
def escalate(input: Int, messages: list[Message], ctx: EndpointLike) -> Stop | Str:
    """An even number?! Need to check this with HR!"""
    interact_with_user("Careful now, that is pretty spicy!", with_reply=False)
    prompt = f"The user chose {input.value}. Is this too hot to handle?! (y/n)?"
    verdict = get_completion(
        endpoint=ctx, messages=[Message(role=Role.SYSTEM, content=prompt)]
    )
    is_first_escalation = (
        len(filter_messages(caller="escalate", messages=messages)) == 0
    )
    if verdict == "y" and not is_first_escalation:
        return Stop(value="Too much spiciness, need to quit!")
    return Str(
        value="HR gave a pass, but still, let's show this to the agent only once.",
        truncation=Truncation(threshold=1, severity=Severity.REMOVE),
    )


@tool(chained_to=ask_number, chain_condition=lambda x: x.value % 2 != 0)
def give_praise(input: Int, messages: list[Message]) -> Str:
    """We need to give praise for such an erudite approach to the problem."""
    interact_with_user(f"{input.value} a fine and bold choice!", with_reply=False)
    return Str(value=f"{input.value} is good, no biggie.")


agent_endpoint = MockLLMEndpoint(
    responses=[
        *(10 * [{"action": "ask_number", "rationale": "This is my only job"}]),
        {"action": "stop", "rationale": "Enough numbers!", "value": ""},
    ]
)
guard_endpoint = MockLLMEndpoint(responses=10 * ["y"])

agent = Agent(
    name="demo",
    system_prompt=f"Without exception, use the {ask_number.name} tool.",
    event_sinks=[CliSink.default()],
    agent_endpoint=agent_endpoint,
    tools=[ask_number, escalate(guard_endpoint), give_praise, stop],
)
agent.invoke()

The above complex example is also bundled with RoboZ. First add the library to your environment as shown in Try it out, then run it with

uv run python -m roboz.examples.complex

Try it out

Add RoboZ to a uv project and run the same bundled example. The one dependency includes the core framework, Shed, Endpoints, and the OpenAI SDK:

uv add roboz
uv run python -m roboz.examples.simple

or with pip, install RoboZ into the active environment first:

python -m pip install roboz
python -m roboz.examples.simple

RoboZ CLI

Installing RoboZ provides the roboz executable. All terminal commands are implemented in roboz.cli:

Command Purpose
roboz inventory init Create editable model catalogue JSON.
roboz inventory generate Generate a typed catalogue from that JSON.
roboz env encrypt Encrypt secrets in a dotenv file.
roboz scripts serve Serve trusted host scripts on Linux.
roboz scripts check Check the host script service.
roboz tool init Create a tool wrapped as a capability.
roboz skill init Create a skill capability with an attached tool.

Use roboz --help or append --help to any command. In a uv project, prefix commands with uv run --locked to use the project's pinned RoboZ version. Successful commands exit with 0, operational failures with 1, invalid arguments with 2, and keyboard interruption with 130.

The former python -m roboz.endpoints and python -m roboz.shed.tools.safe_scripts commands and their CLI modules have been removed. Update scripts to the commands above. For catalogues generated by the old CLI, keep models.json, remove only the generated Python file, and run roboz inventory generate to recreate it with the new header.

Create a local tool or skill

Run either command from your application's repository root:

uv run --locked roboz tool init
uv run --locked roboz skill init

Each command creates its destination and any missing parent directories:

local/
├── tools/simpsons_quotes/
│   ├── __init__.py
│   ├── tool.py
│   └── requirements.txt
└── skills/simpsons_quotes_skill/
    ├── __init__.py
    ├── tool.py
    └── requirements.txt

Edit the implementation in tool.py. In __init__.py, the tool example imports that tool and wraps it in CAPABILITY; the skill example wraps its instructions and attached tool together. The examples return a Simpsons quote and stop the agent. They require no provider credentials or additional packages.

Both capabilities are selectable. The tool declares default=False; changing it to True runs it before the model, which ends the run for this quote example. The skill declares loading=SkillLoading.ON_DEMAND; use SkillLoading.AUTOMATIC to load its instructions and tool immediately when selected. Set selectable=False for a fixed capability.

Import capabilities directly into your application's deployment:

from local.tools.simpsons_quotes import CAPABILITY as quote_tool
from local.skills.simpsons_quotes_skill import CAPABILITY as quote_skill

definition.add_capabilities(quote_tool, quote_skill)

The parent directories use Python namespace packages, so they need no registration files. Each example owns an initially empty requirements.txt; declare extra packages there and install them into the application environment before importing the capability. The generator only creates source files. Applications can discover these packages and install their requirements before importing each package's CAPABILITY.

Use --path local/tools/my_quotes or --path local/skills/my_guide to choose a different destination. Its directory name must be a lowercase snake-case Python identifier and determines the generated names. A destination directly under an import root must not reuse an importable module name such as typing or pydantic; place such a package under local/tools or local/skills instead. Existing destinations are never overwritten. This repository ignores all of /local/; add that rule to your application's .gitignore too and back up private source separately.

Deployable agents and capabilities

DeployableAgent holds one ordered capability collection and the choices for that definition. Constructor entries and later add_capabilities(...) entries follow the same rules. The primitive requires no particular tool set.

from roboz import Skill
from roboz.deployment import (
    Capability, DeployableAgent, SkillLabel, SkillLoading, ToolLabel,
)
from roboz.llm import MockLLMEndpoint
from roboz.tools import stop

agent = DeployableAgent(
    name="assistant",
    system_prompt="Help the user.",
    capabilities=(Capability(label=ToolLabel("stop"), value=stop),),
)
guide = Skill(name="guide", description="Project guidance", instructions="Help.")
agent.add_capabilities(
    Capability(label=SkillLabel("guide", selectable=True), value=guide)
)
agent.set_agent_endpoint(MockLLMEndpoint([]))
agent.set_capability_selection({"guide": SkillLoading.ON_DEMAND})
runtime, background_agents = agent.build()

The configuration fields name, description, system_prompt, mode, automatic_tool_prompt, agent_endpoint, and initial_messages are writable attributes. Assign an AgentMode to mode and a tuple of paths or strings to initial_messages. The existing set_agent_endpoint() and set_initial_messages() methods remain available; the latter accepts any sequence and stores a tuple. set_attributes() is for capability-specific configuration and cannot overwrite these structural fields.

Labels are immutable typed values. Their name is unique within an owning agent; selectable=False makes the declared capability fixed. ToolLabel exposes a tool or intact chain; default=True adds its first tool to default_tools. Default roots run in declaration order; selection preserves that order. SkillLabel declares SkillLoading.ON_DEMAND or SkillLoading.AUTOMATIC. A skill's instructions and embedded tools always remain together.

Inspect agent.capabilities and their labels without building tools. Each label's kind is "tool" or "skill", so callers can expose metadata directly. Selection belongs to the agent, not the shared capability: capability_selection is a read-only view, and set_capability_selection(...) replaces it for future builds. None, the initial value, includes all declared capabilities. An explicit map includes fixed entries plus enabled optional entries; omitted optional entries are disabled. True uses declared behavior, False disables an optional entry, and a SkillLoading value changes a selectable skill's loading mode. New optional entries stay disabled under an explicit map until selected. Existing runtimes keep their configuration, and child definitions have their own selections.

agent.resolve_capabilities() returns effective choices for every declaration: fixed entries are True, enabled optional skills use their loading mode, and disabled entries are False. It leaves the explicit selection unchanged and does not build capabilities. The returned map can be passed back to set_capability_selection() or compared with another run's choices.

For a capability requiring runtime bindings, subclass Capability, pass its label to super().__init__(label=...), and override build(agent, pipe). Override required_attributes when the builder needs owner configuration. Return a tuple of runtime tools, chains, or intact skills in construction order. A tool chain is one sequence inside that tuple; multiple entries under a default-tool label are scheduled as separate roots. Deployment applies the declared ToolLabel or SkillLabel to every returned value; builders do not construct new capabilities or pass labels again. The owner selects all values from one capability together. Excluded builders and their attribute requirements are skipped. Skills' embedded tools are never filtered or validated against the selection. external_dependencies() still inspects the full declared graph, including disabled capabilities, without changing choices or invoking agents.

Migration: replace default_capabilities= with capabilities= and inspect capabilities instead of the old default/additional views. Replace the old four-field Capability build result with a tuple of runtime values. Use Capability(label=..., value=...) to register an existing tool, chain, or skill; its build returns the supplied value in a singleton tuple. Custom builders inherit from Capability; the separate AgentCapability protocol is removed. Capability constructors, including Shed's, accept keywords only. Labels remain immutable metadata. There are no compatibility aliases, inferred names, or separate registry.

Shed

roboz.shed provides reusable components built on the core primitives. Use an individual guarded file tool, add a capability to a DeployableAgent, or start from the orchestrator and Librarian definitions. Applications own their deployment recipes, model endpoints, event sinks, lifecycle, and filesystem layout.

  • roboz.shed.agents contains the orchestrator and Librarian definitions.
  • roboz.shed.capabilities binds reusable behavior to agent configuration.
  • roboz.shed.tools contains guarded file commands, patching, conversation compaction, snapshots, memory consolidation, and retention.
  • roboz.shed.skills supplies reusable instructions and complete tool bundles.
  • roboz.shed.sandbox defines filesystem scopes and tool permission policies.
  • roboz.shed.dependency_health checks configured external resources.

External resources implement the public ExternalDependency.check() method: return None on success or a DependencyFailure with reason_code and a readable, credential-redacted message. Each resource owns its failure explanation. Use DependencyFailure.from_exception(error, secrets=...) to retain linked causes while redacting explicitly configured credentials. The result and formatting utilities live with the contract in roboz.dependencies.

This replaces the former boolean return value; callers use resource.check() is None to test availability. The shared monitor consumes each resource's result without choosing messages based on its category.

The health monitor includes the same message in its cached records and technical logs. New or changed failures log at warning level, recovery at info, and repeated observations at debug. Applications retain ownership of logging handlers.

Shed permission policies guard Shed tools. They are not an operating-system sandbox.

Skill.factory(name=..., description=..., instructions=..., build_tools=...) defines a context-taking skill factory. The builder's annotated context type determines the factory's accepted context during type checking. Calling it invokes the builder and returns a concrete Skill with its instructions and tools. Each builder routes its context to the appropriate tool factories explicitly. Capability builders call skill factories when runtime configuration is available; agents accept the resulting skills, not the factories. Optional depends_on accepts an already constructed prerequisite skill.

Email and timesheet capabilities now live in Robozify. Replace roboz.shed.capabilities.Email with robozify.skills.email.Email, roboz.shed.tools.email with robozify.skills.email.tools, and roboz.shed.skills.email_tools with robozify.skills.email.instructions. Email contexts, models, and identifiers moved to their corresponding robozify.skills.email modules. Install the skills/email/requirements.txt dependencies when using Proton Bridge.

Robozium's deployment recipe and HUD guidance live in the Robozium repository. Import the recipe from robozium.hub.deployment and its skill from robozium.hub.skills. The former roboz.shed.deployments and roboz.shed.skills.robozium modules have been removed, along with the robozium export from roboz.shed.skills.

For Linux host execution, use SafeScripts(socket_path=...). See the SafeScripts guide for helper setup and execution policy.

Guarded file CLI

Guarded CLI workflow: resolve, check permissions, execute, and guard each subsequent command

The file-command tool supports cp, mv, pwd, cat, head, tail, wc, tee, touch, mkdir, grep, rg, ls, find, diff, gio trash, and rm. The Filesystem capability bundles these commands, the apply_patch literal-replacement tool, the read_images reader, and their instructions into one filesystem skill. You can also construct the complete skill directly:

from pathlib import Path

from roboz.runtime import EventPipe
from roboz.shed.models import ActionVerdict, Operation, PermissionRule
from roboz.shed.sandbox import PermissionPolicy
from roboz.shed.skills import FilesystemContext, filesystem_skill

pipe = EventPipe()
permissions = PermissionPolicy(
    base=Path.cwd(),
    default_verdict=ActionVerdict.deny,
    allow=(
        PermissionRule(
            pattern="notes/**",
            operations={Operation.READ, Operation.CREATE, Operation.DELETE},
        ),
    ),
)
file_skill = filesystem_skill(FilesystemContext(permissions=permissions, pipe=pipe))

Pass file_skill in the agent's auto_loaded_skills for startup activation, or skills for on-demand activation, and pass pipe as its event_pipe. The skill already contains all three tool chains. Filesystem() derives the policy and pipe from its owning deployment and registers the complete skill automatically. Filesystem(label=SkillLabel("filesystem")) offers the complete skill for on-demand activation: loading it makes both its instructions and tool chains available. The standalone get_run_file_command, get_apply_patch, and get_read_images_tool builders remain available for tool-only compositions.

Migration: replace FileCommands() and FileEditing() with a single Filesystem(). Replace the cli_skill and file_editing instruction objects with filesystem_skill(ctx) and remove separately registered file tools from that composition. The skill action is now filesystem; tool actions are run_file_command, apply_patch, and read_images.

run_file_command takes one ordered value array of [value, tag] pairs: CMD for commands, FLG for flags, ARG for argument values, PTH for paths, and CTL for control operators. The diagram's example creates a note and counts its lines.

Use && for dependent steps, || for a fallback, ; for independent commands, and | to pipe stdout. Pipelines bind first; && and || run left to right. Each reached command gets its own permission checks. Pipes are buffered, and earlier file changes are not rolled back if a later command fails.

Command options are separate FLG tokens; options taking values use the next ARG or PTH token. -- ends options. Source paths accept * within a path component and one recursive **/ component; unmatched patterns fail. ? and bracket patterns are unsupported, and destinations must be literal paths. Relative paths are resolved from base. A rule for directory/** covers the directory and its descendants.

Commands Options Permission checks
pwd, ls, find pwd -P; ls -l/-a/-h/-R; find -name/-type/-maxdepth/-print READ on the base or selected roots
cat, head, tail, wc, diff cat -n/-b; head/tail -n/-c; wc -l/-w/-c; diff -u/-q READ on selected files
grep, rg -n/-i/-F/-m; grep -r/-R; rg --hidden/--no-ignore READ on selected files or search trees
cp, mv -t/-T/-v/-f; cp -r/-R READ on copied sources, CREATE on destinations, DELETE on moved sources or overwritten entries
tee, touch, mkdir tee -a; touch -c/-d/-r; mkdir -p/-v CREATE on targets; existing file updates also need READ and DELETE
gio trash, rm gio trash; rm -r/-f DELETE on targets and recursive descendants

The filesystem skill's command instructions contain the full option allowlist and describe command-specific operand ordering and restrictions. Its patch instructions cover literal replacements and choosing between patches and full-file rewrites.

Image reading

The filesystem skill includes read_images for visual inspection of local PNG, JPEG, WebP, and GIF files. Use an image-capable endpoint configured with accepts_images=True, and request multiple files together:

{"action": "read_images", "rationale": "Compare the charts", "paths": ["before.png", "after.png"], "detail": "high"}

The nonempty paths list accepts relative or absolute file paths without globs or symlinks. Each file receives an independent READ permission check and any required approval before its contents are opened. Denied, missing, unsupported, or unreadable files are reported while the batch continues. One final report maps successful paths to numbered image attachments, preserving request order and duplicate entries. The batch needs no intervening model calls.

Optional detail applies to every image: auto, low, or high; omitting it uses the model default. File signatures identify the format. Original bytes are attached without decoding, resizing, conversion, or pixel validation; provider format and size limits still apply.

For a standalone tool composition:

from pathlib import Path

from roboz.runtime import EventPipe
from roboz.shed.sandbox import PermissionPolicy
from roboz.shed.tools import get_read_images_tool

pipe = EventPipe()
permissions = PermissionPolicy.local(Path.cwd())
image_tools = get_read_images_tool(**permissions.tool_options(pipe))

Pass image_tools in the agent's tools and pipe as its event_pipe. Only read_images is exposed to the model; guarding, execution, and continuation are automatic chained steps.

PDF reading

Add Pdf() from roboz.shed.capabilities to a deployment with a configured sandbox. It loads the guarded process_pdf reader:

{"action": "process_pdf", "rationale": "Read the report", "path": "report.pdf", "start_page": 1, "end_page": 3}

Reading uses pypdf, includes page numbers, and limits the combined text output. Use smaller page ranges when output is truncated. Images are ignored and no OCR is performed; an existing OCR text layer can be read. Encrypted PDFs require a decrypted copy.

Pdf(label=SkillLabel("pdf")) offers the skill on demand. Direct compositions can use pdf_skill(**permissions.tool_options(pipe)) from roboz.shed.skills, or get_pdf_tool(**permissions.tool_options(pipe)) from roboz.shed.tools.pdf for the guarded tool chain. Both reader builders accept the same keyword-only base, default_verdict, allow_rules, deny_rules, ask_rules, takes_precedence, and optional pipe as the other file tools.

Migration: replace get_pdf_tool(ctx) and pdf_skill(ctx) with get_pdf_tool(**permissions.tool_options(pipe)) and pdf_skill(**permissions.tool_options(pipe)). PdfContext has been removed.

Endpoints and model catalogues

roboz.endpoints builds concrete LLMEndpoint and TranscriptionEndpoint objects, provides ready-made endpoint examples, and can generate a typed catalogue for an application. Both forms expose provider and model names to Pylance and other Python type checkers.

Only OpenAI-compatible API protocols are currently supported. Catalogue data contains provider URLs, credential environment-variable names, and model routes. It never contains credential values.

The OpenAI SDK is installed with RoboZ, but client construction and credential lookup remain deferred until an endpoint is materialized or used. Importing and inspecting the bundled catalogue needs no credentials.

Use the bundled examples

The installed catalogue includes example routes for OpenRouter, Cerebras, and Groq:

from roboz.endpoints.inventory import openrouter

endpoint = openrouter.z_ai__glm_5_3
print(endpoint.model_name)
print(endpoint.max_context_tokens)

The bundled declarations are shipped with RoboZ, so an editor can complete provider and model attributes without loading credentials or constructing SDK clients.

Image inputs

Set accepts_images=True on an image-capable endpoint or its ChatModelSpec. The default is False; existing catalogue routes are not inferred to support images. Project catalogue JSON accepts the same optional boolean.

from roboz.endpoints.adapters.openai_compatible import chat_endpoint
from roboz.llm import call_llm_api
from roboz.models import Image, Message, Role, StrImage

endpoint = chat_endpoint(
    model="your-vision-model",
    max_context_tokens=128_000,
    accepts_images=True,
)
image = Image(url="https://example.com/chart.png", detail="high")
reply, usage = call_llm_api(
    endpoint,
    [Message(role=Role.USER, content="Describe this chart.", images=[image])],
)

# Tool results carry attachments through normal message finalization too.
result = StrImage(value="Chart generated.", images=[image])

Tool payloads carry images only when their type declares them. StrImage extends Str with a required images: list[Image] field; an empty list is allowed. Custom result types can declare that field themselves. The selected input model defines the arguments the model can supply. Declare an image argument explicitly to expose it:

from pydantic import Field

from roboz import tool
from roboz.models import Empty, Image, Message, StrImage


class InspectImageInput(Empty):
    images: list[Image] = Field(...)


@tool
def inspect_image(input: InspectImageInput, messages: list[Message]) -> StrImage:
    """Present the supplied images for visual inspection."""
    return StrImage(value="Inspect these images.", images=input.images)

Image arguments can also use another name, such as photo: Image, or appear in nested input models. Structured-output models use the same explicit declaration. Plain Empty, Int, and Str have no image field. A chained tool accepting Str receives the text from a StrImage result; a tool accepting StrImage receives the text and images. Images already recorded on a conversation message remain available to the LLM regardless of the next tool's input type.

Model-generated tool calls retain their supplied arguments in the assistant message's JSON text, including explicitly supplied image arguments. Unset defaults are omitted, and the message's attachment list keeps its empty default. Tool results carry images as message attachments without duplicating them in the result's JSON text. Persisted messages retain these same content and image fields.

Runtime metadata (truncation, message_kind, endpoint, model, token_input, and token_output) is omitted directly by model_json_schema() on value and message models. It remains available on instances and in ordinary model_dump() and model_dump_json() output. Finalization keeps that metadata on Message, outside its JSON content, and outgoing provider records omit it.

UserIO.request_input() and interact_with_user() return Str | StrImage | None. Ordinary and initial prompt tools preserve the returned result, including its images. Host adapters supply typed replies:

from roboz.models import Str, StrImage

text_reply = Str(value="Describe the chart.")
image_reply = StrImage(value="See the uploaded chart.", images=[image])
image_only_reply = StrImage(value="", images=[image])

The CLI wraps input in Str; a timeout returns None. Notifications also return None. Permission approvals use only reply.value; images are ignored and are not carried into the permission result. Files and their lifetime belong to the host; filenames and file links can be included in the reply's value.

Migration: replace Str(value=..., images=...) with StrImage(value=..., images=...). Custom image input and result models must declare their image fields explicitly. Update host adapters from returning strings to returning Str(value=...), and read text from reply.value when consuming a non-None reply. There is no legacy string-returning adapter path. Image and Message.images remain unchanged.

Use HTTP(S) URLs, base64 image data URLs, or Image.from_bytes(data, media_type="image/png"). Byte inputs support PNG, JPEG, WebP, and GIF. RoboZ checks the source and encoding without fetching URLs or decoding pixels; provider format, size, and detail limits still apply. Image inputs require a user role after message mapping and scrubbing. Text-only requests retain their existing shape, and streaming uses the same image input format as non-streaming calls.

Custom messages_scrubber callbacks receive typed messages and return records with role, content, and optional images. The built-in scrubber retains each message's images list, including empty lists. Keep images when filtering or reordering records; remove the field to redact attachments. The caller constructs API content parts from those returned records only.

Persisted conversations retain attachments. LIGHT truncation leaves image data intact; STUB removes attachments and REMOVE drops the whole message. Context token estimates count text only. Compaction and memory snapshots use image URLs or numbered embedded-image markers as text, without sending images to summary models. Snapshot source metadata identifies the conversation and message sequence. Memory consolidation handles those references as ordinary text; summaries can discard them under their usual relevance rules.

Create a project catalogue

Run these commands from the application project root:

uv run roboz inventory init
# Edit the generated models.json.
uv run roboz inventory generate

For a src-layout project named my-app, this creates:

src/my_app/model_catalogue/
├── __init__.py
├── models.json       # edit this
└── providers.py      # generated; do not edit

The default location follows the package layout: src/my_app/ or my_app/. If neither package exists, the CLI uses model_catalogue/ in the project root. Use --path if automatic detection chooses the wrong location.

Add providers and models to models.json, then run generate again. The generated module contains explicit type declarations for every provider and model, giving Pylance the same autocomplete and concrete endpoint types as the bundled catalogue.

For example, a provider entry can contain:

"my_service": {
  "base_url": "https://models.example.com/v1",
  "api_key_env": "MY_SERVICE_API_KEY",
  "models": {
    "my_chat_model": {
      "model_id": "my-chat-model",
      "endpoint_type": "llm",
      "max_context_tokens": 128000
    }
  }
}

Import the generated endpoint through the application package:

from my_app.model_catalogue.providers import my_service

endpoint = my_service.my_chat_model

Provider and model keys become Python attributes and must be valid public Python identifiers. Chat models are typed as LLMEndpoint; transcription models are typed as TranscriptionEndpoint. Regenerate after every JSON change so runtime behavior and editor completion stay in sync. Chat models require max_context_tokens; transcription models do not accept it.

Use --path to choose another JSON location. generate creates providers.py beside that file unless --output selects another module:

uv run roboz inventory init --path src/my_app/endpoints/models.json
uv run roboz inventory generate --path src/my_app/endpoints/models.json

Create the parent Python package first, then reuse the same paths when regenerating. generate replaces only a module carrying its generated-file marker. Restart a running application after generation to import the new snapshot.

Restore the bundled examples

To discard project customizations and start again from the examples in the installed RoboZ version:

  1. Delete the catalogue's models.json.
  2. Run inventory init to recreate it.
  3. Run inventory generate to refresh the generated Python module.

init never overwrites JSON. generate replaces an existing Python file only when it carries the RoboZ generated-file marker.

Run roboz inventory <command> --help for command options.

API key encryption

Tools and providers read ordinary names such as OPENROUTER_API_KEY and PROTON_BRIDGE_PASSWORD. Encryption adds _ENCRYPTED to stored names; unlocking restores the original name. Choose secrets explicitly, regardless of their name. _ENCRYPTED is reserved for storage.

uv run roboz env encrypt --secret OPENROUTER_API_KEY --secret PROTON_BRIDGE_PASSWORD
# --path another.env selects another plaintext source.

The command prompts for a password twice (or consumes ROBOZ_ENV_PASSWORD). It atomically writes .env.encrypt beside the untouched source with owner-only permissions. Selected values use roboz:v1: ciphertext; ordinary settings stay plaintext. Remove the source yourself if you want only encrypted storage.

from getpass import getpass
from roboz.endpoints import load_secrets

load_secrets(password=getpass("Secret password: "))

The default loader reads .env.encrypt, with a manually maintained .env overriding it. Existing process values win, including explicit empty values. An explicit path= selects a file without the default .env overlay. Values are validated before any environment mutation. Endpoint credentials remain lazy; explicit adapter keys take precedence.

UIs can use encrypt_env_values(values, secret_names=..., password=...), decrypt_env_values(values, password=...), and serialize_env(values) directly in memory, with no plaintext files or process-environment side effects. encrypt_env(path, secret_names=..., password=...) is the file wrapper. Public constants are ENCRYPTED_SUFFIX ("_ENCRYPTED"), ENCRYPTED_NAMESPACE ("roboz:"), and DEFAULT_ENCRYPTED_ENV_PATH (Path(".env.encrypt")), exported by roboz.endpoints and its env module.

Migration from 0.10

Replace provider and tool names ending in _SECRET with their base names in configuration, custom catalogues, and .env.example files. Replace the removed SECRET_SUFFIX import with ENCRYPTED_SUFFIX when inspecting stored entries. Recreate encrypted files using explicitly selected base names; old _SECRET ciphertext is rejected rather than silently treated as a credential. Recover old secrets using the previous release before upgrading if you need them.

Module map

Module Provides
roboz Agent, tool, factory, and skill authoring facade.
roboz.cli Central command parser and handlers for the roboz executable.
roboz.agent Agent implementations, nested agents, and background agents.
roboz.deployment Reusable agent definitions and capabilities.
roboz.llm Endpoint contracts, selection, calls, and request policies.
roboz.models Typed messages and tool input/output models.
roboz.runtime Events, pipes, sinks, persistence, and observability.
roboz.shed Reusable capabilities, guarded tools, agents, and skills.
roboz.endpoints OpenAI-compatible adapters and typed model catalogues.

Robozium

Robozium, a multi-agent app built on RoboZ, is now published as open source. See the Robozium README for more information and instructions for running it locally.

Robozium project chat Robozium runs overview

Development

uv sync --locked --dev
uv run pytest
uv run ruff check
uv run pyright
bash scripts/run_type_tests.sh

See CONTRIBUTING.md for development and testing guidance. The verification workflow defines the complete CI gate. Roboz is typed and ships a PEP 561 py.typed marker.

License

Roboz is licensed under the Apache License 2.0. Copyright © 2026 Tachion Oy.

Metadata

Release files for roboz 0.13.1a1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for roboz 0.13.1a1
File Size Uploaded
roboz-0.13.1a1.tar.gz 230.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for roboz 0.13.1a1
File Interpreter ABI Platform
roboz-0.13.1a1-py3-none-any.whl Python 3 none any Details

Total release size: 506.5 kB

Release files / roboz-0.13.1a1.tar.gz

Download URL roboz-0.13.1a1.tar.gz
Size 230.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3c0241dcf0610fa583471441561e9ddd0326589644756a37b15cff8a78937357
BLAKE2b-256 checksum
How to use checksums
d54f741222e5c69386e05d5656548d4c7d98dcd1f0e5180dcd66d5a9be4b3248
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release files / roboz-0.13.1a1-py3-none-any.whl

Download URL roboz-0.13.1a1-py3-none-any.whl
Size 276.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1b6eeed27ec4fcde51650530ce1ea38d5e3cd52fbdd3058b36fc046039a3adad
BLAKE2b-256 checksum
How to use checksums
791d01b100a4277443af0a5feca041013d6029f0854da30b038e9f73ba3619ea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.13.1a1 This release

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.5.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page