# Quick Start ## Getting Started Yoker provides an interactive chat interface with Ollama and tool calling capabilities. **Architecture**: Yoker uses an event-driven, library-first design. The Agent emits events (thinking chunks, content, tool calls) that a `UIHandler` receives through a `UIBridge`. This makes the library usable in headless, web, and GUI contexts without terminal logic leaking into the agent. ```{image} _static/architecture-diagram.svg :alt: Architecture Diagram ``` ### Prerequisites - Python 3.10 or higher - An LLM provider — Ollama (free tier), OpenAI, Anthropic, or Google Gemini. Run `python -m yoker` with no config to launch the bootstrap wizard, which guides you through provider selection and model setup. ### Install ```bash pip install -e . ``` Or from PyPI: ```bash pip install yoker ``` ### Run ```bash python -m yoker ``` Or with an agent definition: ```bash python -m yoker --agents-definition examples/agents/researcher.md ``` ### Library Usage (Headless) Yoker is designed to be embedded as a library. The `yoker` package exposes a thin Pythonic facade (MBI-003) over the `Agent` and `Session` classes, plus a lower-level event-driven API for full control. #### Python API (recommended) The facade lives in `yoker.api` and is re-exported from the top-level `yoker` package. The primary entry points are: | Function | Use | |----------|-----| | `yoker.process(prompt, **kwargs)` | One-shot turn; returns the response string. | | `yoker.do(skill_name, prompt, args="", **kwargs)` | One-shot skill invocation. | | `yoker.agent(**kwargs) -> Agent` | Builder that returns a reusable `Agent`. | | `yoker.session(id=..., *, persist=True, fresh=False, **kwargs)` | Async context manager yielding a multi-turn `Session` with context persistence. | | `yoker.run_sync(coro)` | Wraps `asyncio.run` for synchronous callers (scripts, notebooks, REPLs). | All builder kwargs (`model`, `provider`, `system_prompt`, `tools`, `skills`, `plugins`, `thinking`, `event_handler`, `config`, ...) are accepted by `process`, `do`, `agent`, and `session`. One-shot, single response: ```python import asyncio import yoker async def main(): answer = await yoker.process("What is 2+2?") print(answer) asyncio.run(main()) ``` Synchronous caller (scripts, notebooks): ```python import yoker answer = yoker.run_sync(yoker.process("What files are in the current directory?")) print(answer) ``` A reusable, configured agent: ```python import asyncio import yoker from yoker.events import ToolCallEvent async def main(): reviewer = yoker.agent( model="qwen3.5:cloud", system_prompt="You are a security-focused code reviewer. Cite file:line.", tools=["read", "search", "list"], thinking="visible", ) def log_tools(event): if isinstance(event, ToolCallEvent): print(f"[tool] {event.tool_name}({event.arguments})") reviewer.on_event(log_tools) report = await reviewer.process("Review src/yoker/plugins/security.py for vulnerabilities.") print(report) asyncio.run(main()) ``` Multi-turn conversation with automatic context persistence — re-opening the same id restores history: ```python import asyncio import yoker async def main(): async with yoker.session(id="refactor-auth") as session: await session.agent.process("Read src/auth.py and identify the main responsibilities.") await session.agent.process("Suggest a refactor that splits authentication from session management.") asyncio.run(main()) ``` Invoke a skill by name as a one-shot command: ```python import asyncio import yoker async def main(): result = await yoker.do("commit", "stage and commit current changes") print(result) asyncio.run(main()) ``` For the full set of examples, see `examples/python_api/` (`one_shot.py`, `agent_builder.py`, `session.py`, `run_skill.py`, `workflow.py`, `event_handling.py`, `sync_usage.py`). #### Low-level event-driven API (advanced) For full control — custom rendering, non-terminal surfaces, or when you need to drive the UI lifecycle yourself — use the `Agent` class directly with a `UIHandler` and `UIBridge`. The example below uses the built-in `BatchUIHandler` and `UIBridge` to render agent events without the CLI. ```python import asyncio from yoker import Agent from yoker.config import get_yoker_config from yoker.ui import BatchUIHandler, UIBridge async def main(): # Load configuration from environment variables, ~/.yoker.toml, # ./yoker.toml, and built-in defaults. config = get_yoker_config(cli=False) # Create the agent. All heavy lifting (context, tools, guardrails) # is configured from the Config object. agent = Agent(config=config) # Create a UI handler and bridge agent events to it. ui = BatchUIHandler(show_thinking=True, show_tool_calls=True) bridge = UIBridge(ui) agent.on_event(bridge) # Start the UI, process a message, then shut down. await ui.start(agent) try: await agent.process("What is 2+2?") finally: await ui.shutdown("complete") asyncio.run(main()) ``` For a fully custom integration, implement the `UIHandler` protocol and wire it through `UIBridge`. See `examples/custom_handler.py` for a minimal custom handler. ### Async Model Yoker is built with an async-first architecture: - **All Agent methods are async**: `process()` is a coroutine. - **UI handlers are async**: Input, lifecycle, and streaming methods use `await`. - **Event handlers can be sync or async**: Handlers receive events in the same asyncio task as `process()`. ```python from yoker import Agent from yoker.events import TurnEndEvent class DatabaseHandler: """Example async handler writing to a database.""" def __init__(self, db_connection): self.db = db_connection async def __call__(self, event) -> None: if isinstance(event, TurnEndEvent): # Direct await - no run_coroutine_threadsafe needed! await self.db.save_response(event.response) async def main(): agent = Agent() # model resolved from config or agent definition agent.on_event(DatabaseHandler(db)) await agent.process("Hello") # Handler runs in the same event loop ``` **Threading Model:** | Context | Where handlers run | |---------|---------------------| | `await agent.process()` | Same asyncio task | | Interactive CLI | Main asyncio event loop | | Web server (FastAPI) | Request handler's event loop | Handlers run synchronously within the asyncio event loop. For long-running operations, use async handlers that yield control (for example, `await asyncio.sleep(0)`) or offload to a separate executor. ### Interactive Session ``` Yoker v0.5.0 - Using model: qwen3.5:cloud Type /help for available commands. Press Ctrl+D (or Ctrl+Z on Windows) to quit. > /help Available commands: /help - Show available commands /think - Enable/disable thinking mode: /think [on|off] /skills - List available skills /context - Show current conversation context /tools - List all known tools with availability /agents - Show loaded agent and known agents Type a message without / prefix to chat with the LLM. > What's in the README.md file? I'll read the README.md file for you. The README.md file describes **Yoker**, a Python-based agent harness... > ^D Goodbye! ``` ### Interactive Input Features The session supports: | Feature | How to use | |---------|------------| | Multiline input | `Esc+Enter` adds newlines, `Enter` submits | | Command history | `Up`/`Down` arrows navigate previous messages | | History search | `Ctrl+R` searches through history | | Mouse support | Click to position cursor | ### Slash Commands | Command | Description | |---------|-------------| | `/help` | Show available commands | | `/think on\|off\|silent` | Enable, disable, or silence LLM thinking trace | | `/skills` | List available skills | | `/context` | Show current conversation context | | `/tools` | List all known tools with availability | | `/agents` | Show loaded agent and known agents | Any command name that matches a loaded skill is treated as a skill invocation. For example, if a skill named `example` is loaded, `/example` injects the skill content into the conversation. ### Thinking Mode When enabled, the LLM shows its reasoning process in gray: ``` [Thinking] Let me analyze this request... I should check the file structure first... [Response] Based on my analysis, here's what I found... ``` ### Command Line Options ```bash python -m yoker --help ``` Key options generated from the Clevis configuration schema: | Flag | Description | |------|-------------| | `--backend-ollama-model MODEL` | Model to use (overrides config) | | `--agents-definition PATH` | Path to agent definition file (Markdown with frontmatter) | | `--ui-mode interactive\|batch` | Run in interactive or batch mode | | `--ui-show-thinking` | Show thinking output | | `--ui-show-tool-calls` | Show tool call information | | `--ui-show-stats` | Show turn statistics | | `--with PACKAGE` | Load a Python package plugin (repeatable) | The `--with` argument is intercepted before Clevis parses the rest of the command line, so it can be combined with any other option: ```bash python -m yoker --with yoker_plugin_demo --agents-definition examples/agents/researcher.md ``` ### Session Persistence Yoker supports session persistence for resuming conversations. Configure it in `yoker.toml`: ```toml [context] manager = "basic_persistence" storage_path = "./context" session_id = "auto" persist_after_turn = true ``` When `persist_after_turn` is true, the session is saved after each turn. To resume a specific session, set `session_id` to the previously generated id. **Programmatic usage (recommended):** the `yoker.session` facade handles persistence for you — pass an `id` and leave `persist=True` (the default). Re-opening the same id restores the previous conversation history automatically. ```python import asyncio import yoker async def main(): # First run: start a conversation under a named id. async with yoker.session(id="my-session") as session: await session.agent.process("What is 2+2?") # Later run: re-open the same id to resume history. async with yoker.session(id="my-session") as session: await session.agent.process("Now what is 4+4?") asyncio.run(main()) ``` **Lower-level manual construction:** the `Persisted` context-manager wrapper (`yoker.context.Persisted`) wraps a base in-memory manager and persists turns to JSONL. The storage path comes from the `Config` (the `context.storage_path` field), not a constructor argument. ```python import asyncio from yoker import Agent from yoker.config import get_yoker_config from yoker.context import Persisted, SimpleContextManager async def main(): config = get_yoker_config(cli=False) # storage_path is read from config.context.storage_path context = Persisted(SimpleContextManager(), session_id="my-session") agent = Agent(config=config, context_manager=context) await agent.process("What is 2+2?") # Later, construct another Persisted with the same session_id to # load the previous conversation automatically. asyncio.run(main()) ``` ## Tools Yoker provides several tools for file operations, web access, and subagent spawning: | Tool | Description | |------|-------------| | `read` | Read file contents with guardrails | | `list` | Directory listing with pattern filtering | | `write` | Write files with overwrite protection | | `update` | Edit existing files with replace/insert/delete | | `search` | Search file contents or filenames | | `existence` | Check if files or folders exist | | `mkdir` | Create directories with parent creation | | `git` | Git operations with permission-controlled commit/push | | `agent` | Spawn subagents with isolated context | | `skill` | Invoke skills dynamically by name | | `websearch` | Web search with SSRF protection and domain filtering | | `webfetch` | Fetch web content with SSRF protection and URL validation | ### Agent Tool The `agent` tool allows spawning subagents for specialized tasks: ```{image} _static/demo-agent-tool.svg :alt: Agent Tool Demo ``` ``` > Use the agent tool to spawn a researcher agent to find all TODO comments [Tool Call] agent(agent_path="examples/agents/researcher.md", prompt="Find all TODO comments") The researcher agent found 15 TODO items across the codebase... ``` Key features: - **Isolated context**: Subagents start with fresh context - **Timeout**: 5-minute default execution limit - **Tool filtering**: Subagents use only their defined tools ### Skill Tool The `skill` tool allows the agent to invoke skills dynamically by name: ``` > use the example skill [Tool Call] skill(skill_name="example") ``` Key features: - **Dynamic loading**: Skills are loaded from configured `skills.directories` - **Full content**: Returns the complete skill content for the agent to follow - **Args support**: Pass arguments to skills via the `args` parameter - **Discovery**: Available skills shown in the system reminder ## Plugins External Python packages can provide tools, skills, and agents through the plugin system. **Important**: Plugins are disabled by default. Enable them in your configuration: ```toml [plugins] enabled = true ``` Then load plugins with `--with`: ```bash # Install and load a plugin pip install pkgq python -m yoker --with pkgq ``` When you load a plugin for the first time, Yoker displays a confirmation dialog showing the plugin's components. Review them carefully—plugins can execute arbitrary code on your system. After accepting a plugin, you can trust it permanently by adding to your config: ```toml [plugins.trusted] pkgq = true ``` Verify loaded components with `/skills` and `/tools` commands. For comprehensive plugin documentation including the complete workflow, security best practices, and troubleshooting, see [Using Plugins](plugins.md). See `examples/plugins/demo/README.md` for plugin development guide. ## Agent Definitions Yoker supports loading agent definitions from Markdown files with YAML frontmatter. This allows you to define custom system prompts and tool availability. ### Agent Definition Format Create a file like `examples/agents/researcher.md`: ```markdown --- name: researcher description: Research assistant that searches and reads files tools: List, Read, Search color: blue --- # Researcher Agent You are a research assistant specialized in finding and analyzing information. ## Workflow 1. Use Search to find relevant files 2. Use Read to examine file contents 3. Compile findings into a structured report ``` ### Using Agent Definitions ```bash # Load an agent definition python -m yoker --agents-definition examples/agents/researcher.md ``` Programmatically: ```python from yoker import Agent from yoker.agents import load_agent_definition, validate_agent_definition from yoker.config import get_yoker_config # Load configuration and agent config = get_yoker_config(cli=False) agent_def = load_agent_definition("examples/agents/researcher.md") # Validate agent tools against config warnings = validate_agent_definition(agent_def, config.tools) # Create agent with definition agent = Agent(config=config, agent_definition=agent_def) ``` ### Example Agents | Agent | File | Description | |-------|------|-------------| | Main | `examples/agents/main.md` | Default assistant with read-only tools | | Researcher | `examples/agents/researcher.md` | Research assistant with search capabilities | | Markdown | `examples/agents/markdown.md` | Formats all responses as structured Markdown | ## Configuration ### Configuration File Create a `yoker.toml` file in your project directory: ```toml [harness] name = "my-yoke" [logging] level = "INFO" [backend] provider = "ollama" # or "openai", "anthropic", "gemini", or any litellm provider [backend.ollama] base_url = "http://localhost:11434" model = "qwen3.5:cloud" timeout_seconds = 60 [backend.ollama.parameters] temperature = 0.7 top_p = 0.9 [agents] definition = "./agents/researcher.md" # Optional: agent definition file [tools.read] enabled = true allowed_extensions = [".txt", ".md", ".py"] ``` For OpenAI, Anthropic, or Gemini, change the provider and use the matching config section. API keys can be interpolated from environment variables via Clevis (`${VAR}` syntax): ```toml [backend] provider = "anthropic" [backend.anthropic] api_key = "${ANTHROPIC_API_KEY}" model = "claude-haiku-4-5" ``` See `examples/yoker.toml` for the full configuration reference and {doc}`models` for the curated model lists per provider. ### Environment Variables Yoker auto-discovers configuration files in this order: 1. `./yoker.toml` (current directory) 2. `~/.yoker.toml` (user home directory) 3. Built-in defaults You can also specify an explicit config file with Clevis: ```bash python -m yoker --config path/to/config.toml ``` ## Available Tools | Tool | Purpose | |------|---------| | `read` | Read file contents | | `list` | List directory contents with pattern filtering | | `write` | Write content to files with overwrite protection | | `update` | Edit existing files with replace, insert, and delete | | `search` | Search file contents with regex or filenames with glob | | `existence` | Check if a file or folder exists | | `mkdir` | Create directories with parent creation | | `git` | Git operations (status, log, diff, branch, show) | | `agent` | Spawn subagents with isolated context | | `skill` | Invoke skills dynamically by name | | `websearch` | Web search with SSRF protection | | `webfetch` | Fetch web content with URL validation | ## Tool Examples ### Read Tool ![Read Tool Demo](_static/demo-read-tool.svg) Reading the first 3 lines of `README.md`. ### List Tool ![List Tool Demo](_static/demo-list-tool.svg) Listing files matching `CLAUDE*` in the current directory. ### Write Tool ![Write Tool Demo](_static/demo-write-tool.svg) Writing "Hello from Yoker!" to `/tmp/yoker-demo.txt` and reading it back. ### Update Tool ![Update Tool Demo](_static/demo-update-tool.svg) Replacing text in an existing file with the update tool. ### Search Tool ![Search Tool Demo](_static/demo-search-tool.svg) Searching for content with regex patterns or filenames with glob patterns. ### Git Tool ![Git Tool Demo](_static/demo-git-tool.svg) Running Git operations (status, log, diff) on a repository. ### Commands ![Commands Demo](_static/demo-commands.svg) Using `/help`, `/think on`, and `/think off` slash commands. ### Thinking Mode ![Thinking Mode Demo](_static/demo-thinking.svg) Thinking mode enabled shows the LLM reasoning process in gray before the response. --- ## Next Steps - {doc}`installation` - Detailed installation guide - [Architecture](https://github.com/christophevg/yoker/blob/master/analysis/architecture.md) - System design