Skip to main content

A streaming terminal REPL for any agent.

Project description

partnuh

A streaming terminal REPL for any agent.

partnuh is purely aesthetic: it's the fast, multi-line, token-streaming terminal REPL. You build your agent with its own dependencies — smolagents, an OpenAI-compatible chat model, anything — and hand it to partnuh.wrap(), which auto-wraps it. partnuh never builds agents and imports no framework of its own.

import partnuh
from smolagents import ToolCallingAgent, OpenAIServerModel

agent = ToolCallingAgent(tools=[...], model=OpenAIServerModel(...))  # your agent
partnuh.wrap(agent, name="Private Caller")                           # one line → a full CLI

Examples

Each example is a self-contained project (its own requirements.txt and README.md):

example shows
examples/basic_agent the simplest starter — a smolagents agent in a partnuh CLI
examples/custom_agent the same agent with every look-&-feel knob customized
cd examples/basic_agent
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
cp .env.template .env            # add your OPENROUTER_API_KEY
.venv/bin/python main.py

Install (as a dependency)

pip install partnuh                 # core (bring your own agent)
pip install "partnuh[openai]"       # + OpenAI/OpenRouter chat backend
pip install "partnuh[smolagents]"   # + smolagents adapter
pip install "partnuh[all]"

Use

Bring your own agent, then wrap() it — one line, you get the whole CLI. What you pass is auto-wrapped: an already-CliAgent, a smolagents agent, or a plain stream function all just work.

import os, partnuh
from smolagents import OpenAIServerModel, ToolCallingAgent, tool

@tool
def add(a: int, b: int) -> int:
    "Add two integers.\n\nArgs:\n  a: first\n  b: second"
    return a + b

model = OpenAIServerModel(model_id="openai/gpt-5.4-nano",
                          api_base="https://openrouter.ai/api/v1",
                          api_key=os.environ["OPENROUTER_API_KEY"])
agent = ToolCallingAgent(tools=[add], model=model, stream_outputs=True)

partnuh.wrap(agent, name="Private Caller")   # auto-wrapped + launched; that's it

wrap() launches an interactive REPL (or runs one-shot if a prompt/CLI args are present). Tune the look with any CliConfig field as a keyword override:

partnuh.wrap(agent, name="Private Caller", prompt_sequence="❯ ", stream_speed=0.3)

Want the object without launching (to embed or test)? Use partnuh.Cli(...) and call .start() yourself.

Anything is an agent — a generator function is enough (no key, no framework):

import partnuh
from partnuh import TextDelta

def echo(prompt, session_id):
    for word in f"you said: {prompt}".split(" "):
        yield TextDelta(word + " ")

partnuh.wrap(echo, name="Echo")

Optional sugar: for the no-framework chat case there's AgentSpec, a thin helper over the OpenAI/OpenRouter backend. It's convenience only — partnuh doesn't need it.

agent = partnuh.AgentSpec(name="Private Caller", model="openai/gpt-5.4-nano").build()
partnuh.wrap(agent)

In the REPL

  • Enter submits, Shift+Enter inserts a newline (works across terminals — see below). Backspace on a blank line joins back to the previous line.
  • /tools list tools · /reset clear history · /help · /quit

Configuring the look & feel

Every appearance knob lives on CliConfig. Pass individual fields straight to wrap() (they override the defaults), or build a whole CliConfig. Many fields have named presets (partnuh.Prompt, Cursor, Spinner, Box, Separator) for discoverability — but any equivalent string works too.

import partnuh
from partnuh import Prompt, Cursor, Spinner, Box, Separator

partnuh.wrap(
    agent,
    name="Private Caller",
    # prompt & input appearance
    prompt_sequence=Prompt.ARROW,   # leading sequence "❯ " (default Prompt.BAR "▌ ")
    prompt_style="fg:#888888",      # prompt_toolkit style for the prompt
    cursor=Cursor.BEAM,             # block | beam | underline | blinking-* | None
    accent_style="cyan",            # color of /commands & special keywords
    # banner box
    banner=True,
    banner_border_style="dim",      # outline color
    banner_box=Box.ROUNDED,         # outline texture
    banner_label_style="hot_pink",  # color of the name:/model:/tools: labels (default "dim") (square/double/heavy/ascii/...)
    # separator around each response
    show_dividers=True,
    separator=Separator.DASH,       # repeated to fill width ("─", "━", "=", ...)
    separator_style="dim",
    # input keys
    newline_keys=("s-enter", "c-enter", "a-enter", "c-j"),
    # streaming
    stream_speed=0.0,               # 0 = as-fast-as-tokens; 1.0 = slow typewriter
    token_delay=None,               # explicit seconds/char; overrides stream_speed
    # spinners
    thinking_spinner=Spinner.DOTS, thinking_text="Thinking...",   # until first token
    answering_spinner=Spinner.DOTS, answering_text="Working...",  # while a tool runs / next step
    spinner_style="dim",
    # tool-call markers
    show_tool_calls=True, tool_call_prefix="⚙ ", tool_result_prefix="→ ", tool_style="dim",
    # extra slash commands
    commands={"clear": lambda d, args: __import__("os").system("clear")},
)

How it works (ports & adapters)

There is no universal in-process agent object shared across frameworks, so partnuh defines a tiny contract and ships adapters:

  • CliAgent protocol — name, model, tools, and stream(prompt, session_id) -> Iterator[Event].
  • Event union — TextDelta, ToolCallStarted, ToolResult, Error, Done. Every adapter translates its framework's native stream into these; a bare str is treated as a TextDelta.
  • Adaptersfrom_openai, from_smolagents, from_callable. Add your own by yielding Events.
  • Pacer — renders the event stream to the terminal with optional speed control.

The Shift+Enter trick

Terminals encode Shift+Enter differently, and prompt_toolkit's default collapses the common Ghostty/xterm encoding into plain Enter (so it submits instead of adding a newline). partnuh registers every known Shift+Enter escape sequence (\x1b[27;2;13~, kitty's \x1b[13;2u, …) and maps it to "insert newline" — no terminal config required.

Public API

Everything below is importable from the top level (import partnuh):

Run a CLI

  • wrap(agent, *, name=None, model=None, config=None, **overrides) — wrap any agent and launch the REPL (one-shot if a prompt/CLI args are present). The one-liner; **overrides are CliConfig fields.
  • Cli(agent, ...) — the CLI object. Construct it without launching, then .start() (or .interactive() / .once(prompt)).
  • CliConfig(...) — the settings object: all look-&-feel state. See Configuring the look & feel above.

Get an agent in

  • demo_agent(script=None, *, name="Demo", echo_prompt=False) — a deterministic, key-less agent (streams Hamlet by default). Great for trying the CLI.
  • from_smolagents(agent, *, name, model=None) — wrap a smolagents agent.
  • from_openai(*, name, model, base_url=None, api_key=None, ...) — a streaming chat agent over the OpenAI SDK (set base_url for OpenRouter).
  • from_callable(fn, *, name, model="custom", tools=None) — wrap a fn(prompt, session_id) -> Iterator[Event | str].
  • AgentSpec(name, model, backend="openrouter", ...) — optional declarative builder for the OpenAI/OpenRouter chat backend; .build() returns a CliAgent.
  • adapt(agent, *, name=None, model=None) — coerce any of the above into a CliAgent (what wrap() calls for you).

Implement your own agent

  • CliAgent — the structural type an agent satisfies: name, model, tools, and stream(prompt, session_id) -> Iterator[Event].
  • ToolInfo(name, description="") — display metadata for a tool.
  • Events: Event (the union), TextDelta, ToolCallStarted, ToolResult, Error, Done; normalize() (coerces a bare str to TextDelta).

Presets — string constants for the config knobs (any equivalent string also works): Prompt, Cursor, Spinner, Box, Separator.

__version__ — the installed version string.

Development

pip install -e ".[dev]"
pytest                       # or: python tests/test_smoke.py

Tests cover the wiring that's easy to break: the unit tests (tests/test_unit.py) check agent adaptation, config merging, presets, and pacer rendering; the smoke tests (tests/test_smoke.py) drive the interactive REPL headlessly (via a prompt_toolkit pipe input) so a renamed config field fails in CI, not in front of a user.

CI runs the suite on every push (Python 3.11–3.13), and merging to main requires the ci check to pass. Run pytest before publishing.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

partnuh-0.0.4.tar.gz (25.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

partnuh-0.0.4-py3-none-any.whl (25.9 kB view details)

Uploaded Python 3

File details

Details for the file partnuh-0.0.4.tar.gz.

File metadata

  • Download URL: partnuh-0.0.4.tar.gz
  • Upload date:
  • Size: 25.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for partnuh-0.0.4.tar.gz
Algorithm Hash digest
SHA256 7259f74448f70af05ff0df57da9e5cb39ed22f150c3c6c69d4a7f3dd5a34f9a2
MD5 64e312669061f27e5acd95667383731a
BLAKE2b-256 198d10e5f4ce4f710206a70aa3aa168f71a0f1b714ee3bbc6d8f0b4f19b2a4c6

See more details on using hashes here.

File details

Details for the file partnuh-0.0.4-py3-none-any.whl.

File metadata

  • Download URL: partnuh-0.0.4-py3-none-any.whl
  • Upload date:
  • Size: 25.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for partnuh-0.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 4bb0abc558b0e7f2d8d9716c306fa596cf789b6844c3c557deef0d314debd0d8
MD5 bcfe260508a0f648c8423ccce45ced71
BLAKE2b-256 786ddd81b8c13898602c3b435482194b2a543dd0da2059b1367e165345beac1b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page