https://github.com/cablehead/yoke
A single agent turn as a unix pipe.
https://github.com/cablehead/yoke
Last synced: about 2 months ago
JSON representation
A single agent turn as a unix pipe.
- Host: GitHub
- URL: https://github.com/cablehead/yoke
- Owner: cablehead
- Created: 2026-04-02T20:05:22.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-05-04T14:58:37.000Z (3 months ago)
- Last Synced: 2026-06-18T21:31:44.801Z (about 2 months ago)
- Language: Rust
- Homepage:
- Size: 717 KB
- Stars: 14
- Watchers: 1
- Forks: 3
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
yoke
A single agent turn as a unix pipe.
---
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](https://www.nushell.sh), which is purpose-built for orchestrating
structured data streams.
```nushell
# 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](https://github.com/yologdev/yoagent).
- [Install](#install)
- [Providers](#providers)
- [Tools](#tools)
- [Input / Output](#input--output)
- [Round-tripping](#round-tripping)
- [Skills](#skills)
- [Web UI](#web-ui)
## Install
### [eget](https://github.com/zyedidia/eget)
```nushell
eget cablehead/yoke
```
### Homebrew (macOS)
```nushell
brew install cablehead/tap/yoke
```
### cargo
```nushell
cargo install --git https://github.com/cablehead/yoke
```
### Build from source
```nushell
git clone https://github.com/cablehead/yoke
cd yoke
cargo build --release
```
## Providers
Run with no arguments to see available providers:
```nushell
$ 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:
```nushell
$ 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](https://ollama.com). No API key required.
```nushell
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`:
```nushell
# 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](https://www.nushell.sh/lang-guide/chapters/types/nuon.html) 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:
```json
{"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`:
```nushell
# 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 ` 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:
```nushell
# 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.
```nushell
# 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.
```jsonl
{"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:
```nushell
yoke --provider anthropic --model claude-sonnet-4-20250514 "what files are here?"
| tee { save -f session.jsonl }
```
Continue the conversation:
```nushell
cat session.jsonl
| yoke --provider anthropic --model claude-sonnet-4-20250514 "now count them"
```
Replay context against a different model:
```nushell
cat session.jsonl
| yoke --provider openai --model gpt-5.4-mini "summarize what happened"
```
## Skills
Load [AgentSkills](https://agentskills.io)-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.
```nushell
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](https://github.com/cablehead/http-nu) and
[Datastar](https://data-star.dev). It streams responses in real time with
rendered markdown, syntax highlighting, and grounding sources.
```nushell
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](https://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.
```nushell
cd tests/tools/nu
$env.GEMINI_API_KEY = "your-key-here"
nu perform.nu case1.md
```