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.
eget cablehead/yokebrew install cablehead/tap/yokecargo install --git https://github.com/cablehead/yokegit clone https://github.com/cablehead/yoke
cd yoke
cargo build --releaseRun 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:11434Run 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 |
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"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 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}]}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 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 |
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-20250514JSONL 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.
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"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.
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.nuEach 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.
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