Skip to content

Repository files navigation

yoke

A single agent turn as a unix pipe.

CI Discord


yoke is a static binary that drives one LLM agent turn to completion. It runs tool calls in a loop until the model is satisfied, then exits. Context window in as JSONL on stdin, new context + live stream out as JSONL on stdout.

context.jsonl ──> yoke ──> tee ──> store context for follow-ups
                               └─> real-time view

No TUI, no REPL, no daemon, no persistence. Just a JSONL-in / JSONL-out primitive you compose with shell tools. Particularly Nushell, which is purpose-built for orchestrating structured data streams.

# one-shot
yoke --provider gemini --model gemini-2.5-flash "what files are here?"

# pipe context in, tee the stream to a file for follow-ups
yoke --provider anthropic --model claude-sonnet-4-20250514 "refactor main.rs"
  | tee { save -f session.jsonl }

# continue the conversation
cat session.jsonl
  | yoke --provider anthropic --model claude-sonnet-4-20250514 "now add tests"

# replay the same context against a different model
cat session.jsonl
  | yoke --provider openai --model gpt-5.4-mini "summarize what happened"

Built on yoagent.

Install

eget cablehead/yoke

Homebrew (macOS)

brew install cablehead/tap/yoke

cargo

cargo install --git https://github.com/cablehead/yoke

Build from source

git clone https://github.com/cablehead/yoke
cd yoke
cargo build --release

Providers

Run with no arguments to see available providers:

$ yoke
available providers:

  anthropic
    env: ANTHROPIC_API_KEY
    key: https://console.anthropic.com/settings/keys

  openai
    env: OPENAI_API_KEY
    key: https://platform.openai.com/api-keys

  gemini
    env: GEMINI_API_KEY
    key: https://aistudio.google.com/apikey

  ollama
    local, no API key required
    default: http://localhost:11434

Run with a provider and no model to list available models:

$ yoke --provider anthropic
claude-3-5-haiku-20241022
claude-3-5-sonnet-20241022
claude-sonnet-4-20250514
...
Provider Env var API
anthropic ANTHROPIC_API_KEY Anthropic Messages
openai OPENAI_API_KEY OpenAI Chat Completions
gemini GEMINI_API_KEY Google Generative AI
ollama -- Local, OpenAI-compatible

Ollama

Run models locally with Ollama. No API key required.

yoke --provider ollama
yoke --provider ollama --model gemma4 "hello"
yoke --provider ollama --base_url http://192.168.1.100:11434 --model llama3 "hello"

Tools

Control which tools the agent has access to with --tools:

# all tools including web search (default)
yoke --provider gemini --model gemini-2.5-flash --tools all "find recent rust news"

# code tools only
yoke --provider anthropic --model claude-sonnet-4-20250514 --tools code "refactor main.rs"

# nushell instead of bash
yoke --provider gemini --model gemini-2.5-flash --tools nu,read_file "check the logs"

# no tools
yoke --provider anthropic --model claude-sonnet-4-20250514 --tools none "explain ownership in rust"
Tool Description
bash Shell command execution
nu Nushell script execution (embedded engine)
read_file Read files with line numbers
write_file Create or overwrite files
edit_file Search/replace editing
list_files Directory listing
search Grep/ripgrep pattern search
web_search Provider-side web search

The nu tool

The builtin nu tool runs Nushell scripts in an embedded engine -- no subprocess, no shell. Output is automatically converted to nuon so structured data round-trips cleanly.

An optional input parameter accepts JSON data that gets piped as $in to the command. This lets the LLM pass structured data as native JSON without worrying about string quoting:

{"command": "$in | sort-by price -r", "input": [{"name": "Widget A", "price": 25.50}]}

Plugins and modules

Load Nushell plugins with --plugin and module search paths with -I:

# load the polars plugin
yoke --provider gemini --model gemini-2.5-flash --tools nu \
  --plugin /usr/local/bin/nu_plugin_polars \
  "open data.csv and find the top 5 rows by price"

# multiple plugins and an include path
yoke --provider gemini --model gemini-2.5-flash --tools nu \
  --plugin /usr/local/bin/nu_plugin_polars \
  --plugin /usr/local/bin/nu_plugin_formats \
  -I ./lib \
  "use mymod.nu; analyze the data"

Plugin names are included in the tool description so the LLM knows they're available and can discover subcommands via help.

Use --config <file.nu> to run a Nushell script once at startup. The script runs against the shared engine state, so any use, def, hide, or env mutations persist across every nu tool call:

# init.nu
use mymod
def my-favorite-number [] { 42 }
hide rm

yoke --provider gemini --model gemini-2.5-flash --tools nu \
  -I ./lib --config ./init.nu \
  "what is my favorite number?"

Parse and eval errors in the config are fatal and reported at startup with file path and span, so misconfigurations are caught immediately.

Web search

Web search is a provider-side capability:

Provider How it works With function tools?
Anthropic Server tool, model invokes mid-turn Yes
OpenAI Dedicated search models (e.g. gpt-5-search-api) No
Gemini Google Search grounding tool Yes

Input / Output

Input

JSONL on stdin. Lines with role are context messages. Everything else is silently skipped.

# simple prompt
{role: "user", content: "list files"} | to json -r
  | yoke --provider anthropic --model claude-sonnet-4-20250514

# system prompt + user message
[
  ({role: "system", content: "You are a helpful assistant."} | to json -r)
  ({role: "user", content: "list files"} | to json -r)
] | str join "\n"
  | yoke --provider anthropic --model claude-sonnet-4-20250514

Output

JSONL on stdout. Two kinds of lines:

Context lines have role. User messages, assistant responses, tool results. These round-trip as input to the next turn.

Observation lines have type. Streaming deltas, tool execution, lifecycle events. Skipped on input.

{"type":"agent_start"}
{"role":"system","content":"..."}
{"type":"turn_start"}
{"role":"user","content":[{"type":"text","text":"what files are here?"}],"timestamp":1234}
{"type":"delta","kind":"text","delta":"I'll check"}
{"type":"tool_execution_start","tool_call_id":"...","tool_name":"list_files","args":{}}
{"type":"tool_execution_end","tool_call_id":"...","tool_name":"list_files","result":{...}}
{"role":"toolResult","toolCallId":"...","toolName":"list_files","content":[...]}
{"role":"assistant","content":[...],"stopReason":"stop","model":"...","usage":{...}}
{"type":"turn_end"}
{"type":"agent_end"}

The observation lines are the live stream -- tee them to a renderer for real-time display. The context lines are the durable state -- save them for follow-ups.

Round-tripping

Save a run:

yoke --provider anthropic --model claude-sonnet-4-20250514 "what files are here?"
  | tee { save -f session.jsonl }

Continue the conversation:

cat session.jsonl
  | yoke --provider anthropic --model claude-sonnet-4-20250514 "now count them"

Replay context against a different model:

cat session.jsonl
  | yoke --provider openai --model gpt-5.4-mini "summarize what happened"

Skills

Load AgentSkills-compatible skill directories with --skills. Skill metadata is injected into the system prompt. The agent reads full SKILL.md instructions via read_file when it activates a skill.

yoke --provider gemini --model gemini-2.5-flash --skills ./skills --tools read_file "use the greet skill"

A skill directory:

skills/
  greet/
    SKILL.md
  weather/
    SKILL.md
    scripts/

SKILL.md uses YAML frontmatter with name and description fields. The body contains full instructions the agent reads on demand.

Web UI

yoke includes a browser-based UI powered by http-nu and Datastar. It streams responses in real time with rendered markdown, syntax highlighting, and grounding sources.

http-nu --datastar --store ./store :3001 ux/serve.nu

Each yoke run streams JSONL through a render pipeline. The browser morphs HTML into place as the turn progresses. Completed runs are persisted to the cross.stream store for replay.

Tool eval

tests/tools/ contains eval cases for iterating on builtin tool descriptions and behavior. Each case is a markdown file with a prompt and evaluation criteria. perform.nu runs the case through yoke and checks the output.

cd tests/tools/nu
$env.GEMINI_API_KEY = "your-key-here"
nu perform.nu case1.md

About

A single agent turn as a unix pipe.

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages