Metadata-Version: 2.4
Name: hugpy-harness
Version: 0.1.16
Summary: Optional standalone terminal chat and agent harness for a Hugpy wrapper or fleet
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: hugpy-tools>=0.2.7
Requires-Dist: abstract-toolserver>=0.0.106

# Hugpy Harness

`hugpy-harness` has two roles:

1. **A terminal client.** It provides a full-screen TUI (`hugpy-harness`) and a
   plain chat REPL (`hugpy-chat`) for a standalone Hugpy **wrapper** or a Hugpy
   **fleet** core such as `https://dev.hugpy.ai`. It talks HTTP only. It needs
   no background service, engine, worker enrollment or toolserver.
2. **The agent-loop core library.** Since 0.1.7, hugpy-agent is built on this
   package. Its loop, gateway, adapter, journal, policy, memory and tool
   registry are the harness modules, and hugpy-agent adds its fleet extras
   through hooks. The harness never imports hugpy-agent.

The full reference, covering hooks, chat-loop behavior and the agent layer,
is in [`docs/agent-and-harness.md`](../../docs/agent-and-harness.md).

## Status (2026-10-18)

| where | version |
|---|---|
| this source (`pyproject.toml`) | **0.1.10**, prepared for release |
| installed on ae (deb, `/opt/hugpy-harness/venv`) | 0.1.9 with local source fixes |
| PyPI | 0.1.9; the 0.1.10 endpoint/TUI changes are not published yet |

## Install

**From the apt repo**, on any Debian or Ubuntu box:

```sh
curl -fsSL https://dev.hugpy.ai/apt/setup.sh | sudo bash
sudo apt install hugpy-harness            # hugpy-wrapper is Recommended, not required
sudo apt install --no-install-recommends hugpy-harness   # client only (remote wrapper/fleet)
```

**From locally built debs**, without an apt repo:

```sh
bash packaging/debian/build.sh            # -> build/debian/: harness .deb, wrapper .deb if built, install.sh
cd build/debian
./install.sh                              # missing local wrapper: prompt [Y/n]
./install.sh --yes                        # default yes, no prompts
./install.sh --without-wrapper            # harness only (remote wrapper or fleet)
./install.sh --dry-run                    # show the APT plan only
```

**From source with pip:**

```sh
pip install ../hugpy_tools .              # needs hugpy-tools>=0.2.7
```

How the deb is put together:

- The deb (Architecture `all`) carries the harness wheel and the
  `hugpy-tools` wheel in `/usr/share/hugpy-harness/`. The postinst installs
  both offline into `/opt/hugpy-harness/venv`.
- `/usr/bin/hugpy-harness` and `/usr/bin/hugpy-chat` exec that venv.
- `Recommends: hugpy-wrapper (>= 0.1.62)`. The wrapper keeps its own runtime
  and service, and neither package embeds the other.
- `apt remove hugpy-harness` leaves chat history, the wrapper, models and
  hugpy-agent untouched. A purge removes only this package's runtime.

## Connect

If you do not pass `--base` or a connection `--config`, the CLI reads the local
wrapper endpoint and a fleet or core endpoint from the user config directory
(`XDG_CONFIG_HOME/hugpy`) and `/etc/hugpy`. It checks the wrapper's `/health`
identity against the core's online worker list. A matching worker URL selects
the core, so model choice and resource monitoring cover the fleet. An absent
or unreachable core leaves the standalone wrapper selected. If no wrapper is
installed, a configured fleet or core endpoint is selected directly.

`HUGPY_WRAPPER_URL` and `HUGPY_FLEET_URL` explicitly select their respective
endpoints. `--base` and `--config` override detection. A user-level fleet
setting also selects that fleet directly. View or change it with:

```sh
hugpy-harness endpoint
hugpy-harness endpoint set-fleet https://dev.hugpy.ai
hugpy-harness endpoint clear-fleet
```

The setting is stored in `~/.config/hugpy/fleet.endpoint.json` (or under
`XDG_CONFIG_HOME`). The URL may be a fleet core or its HTTPS proxy.

If nothing is found, the CLI tells you to pass `--base URL` or set
`HUGPY_FLEET_URL`.

**Key.** The CLI sends `HUGPY_WRAPPER_API_KEY` to a wrapper and
`HUGPY_FLEET_API_KEY` to a core.
The fleet key is a `hugpy-core keys create --name <box> --scope use` key, and
you need it only from outside the LAN once the core requires keys for outside
callers. That core key policy is being set up. As of 2026-10-15, the fleet's
`/v1` answered without a key.

**URLs.** A bare host gets `https://`, and `{base}/v1` is probed first.

**`--config FILE`** reads a JSON connection file that may hold only `base`
and `model` (both strings). The option's help text says the file comes from
`fitevict setup`, but the installed wrapper CLI (0.1.122) has no `setup`
command, so write the file yourself.

To connect a remote box to the fleet:

```sh
hugpy-harness endpoint set-fleet https://dev.hugpy.ai
hugpy-chat --list-models
hugpy-harness --model <id from the list>
```

With 0.1.7, which is what apt serves until 0.1.8 is published, pass
`--base https://dev.hugpy.ai` instead. 0.1.7 does not read the fleet
endpoint files or `HUGPY_FLEET_API_KEY`.

## Launch

```sh
hugpy-harness                                   # full-screen TUI (same as `launch` / `tui`)
hugpy-harness launch --workspace /path/to/project --model MODEL_ID
hugpy-harness --base http://HOST:PORT --workspace /path/to/project
hugpy-harness chat --model MODEL_ID             # plain REPL (= hugpy-chat)
hugpy-chat --message "one question" --model MODEL_ID   # one turn, exit code 0 on success
```

**TUI options:** `--config`, `--base`, `--workspace`, `--model`, `--session`,
`--policy {ask,readonly,auto}` (default `ask`), `--optional-tools` and
`--list-models`. The model defaults to `HUGPY_WRAPPER_MODEL`.

**`hugpy-chat` options:** the same as the TUI, plus
`--tools-mode {prompted,constrained,native,auto}`, `--max-steps` (12),
`--max-tokens` (1024), `--message` and `--resume RUN_ID`.

**In the TUI:**

- `/model` opens the model picker. Every picker filters as you type.
- `/resources` toggles telemetry, and `/resources full` opens the compute
  monitor.
- In the monitor, `e` or Delete evicts a model through the wrapper's operator
  action. A fleet core has no eviction route, so there the TUI says so instead
  of failing.
- The display shows fleet resource snapshots per worker, including stale or
  offline state.

**Sessions** are journaled in `<workspace>/.hugpy_wrapper/chat/journal.db`.
Reopen a session with the same `--workspace` and `/session`, or use
`hugpy-chat --resume RUN_ID`. Conversations stay in the same workspace
journal when you change endpoints.

## Serve (named sessions over loopback HTTP)

```sh
hugpy-harness serve --workspace ~/work            # 127.0.0.1:9126 by default
hugpy-harness serve --port 9127 --base https://dev.hugpy.ai
HUGPY_SERVE_TOKEN=... hugpy-harness serve --host 0.0.0.0   # a token is required beyond loopback
```

The harness is a client; this is its one inbound surface and it is local by
default. It wraps the same session client the TUI uses, so a session created
over HTTP is the same journal row the TUI shows, with the same per-session
model, destination and generation settings. No per-user venv or console is
scaffolded: install the harness and run `serve` in a workspace.

The routes are the ones `serve_client.hugpy_serve.HugpyServeClient` already
speaks (written for the agent session service on :9126), so `hugpy-agent tui
--serve http://127.0.0.1:9126` and any adapter that fronted that service work
unchanged against this server:

| route | purpose |
|---|---|
| `GET /api/state` | service, workspace, endpoint, sessions, profiles (the model catalog) |
| `GET /api/console/models` · `GET /api/tasks` | picker rows · the media task table |
| `GET` / `POST /api/sessions` | list · create `{name?, profile?, settings?, directive?, init_prompt?}`; with a model set, the init prompt runs as the first turn |
| `GET /api/sessions/<sid>?after=N` | session view (status, settings, pending question, queue, cursor) plus `{id, ts, kind, data}` events after cursor N |
| `GET /api/sessions/<sid>/events?after=N` · `GET /api/sessions/<sid>/models` | the same view, and the catalog, under sub-paths a per-session proxy (Core's `/api/sessions/<sid>/*`) can reach |
| `POST /api/sessions/<sid>/messages` | `{text, by?}`; runs the turn in the background, or **queues** it while a turn is open (`queued: true`) |
| `GET` / `POST /api/sessions/<sid>/queue` | what is waiting · `{action: add\|update\|remove\|clear, id?, text?}` |
| `POST /api/sessions/<sid>/init` | (re)send the session's `init_prompt` as a turn |
| `POST /api/sessions/<sid>/tasks` | `{task, model, text, files?, params?}`; files must be inside the workspace |
| `POST /api/sessions/<sid>/{answer,stop,clear,name,profile,settings}` | answer a question, interrupt, clear context, rename, select model, set destination/controls |

`<sid>` accepts the id, the `session:<id>` route or an unambiguous name.

Per-session **direction** is a user setting, not a role: `directive` is appended
to the system prompt, which the loop re-renders every turn, so a changed
directive applies from the next turn without touching the transcript;
`init_prompt` is the first message a host sends when it creates the session (a
station's init brief). Messages that arrive **mid-turn** queue in the journal
and the thread that ran the turn drains them into one collated next turn — a
keeper's nudges and board notes are never refused. (The wrapper's own queue
underneath is compute admission for model calls; this one is conversation
ordering.) A media task's result is also recorded in the conversation (as an
operator-shared result), so the model can reason about the output on the next
turn; before, it reached only the display transcript.

Registering a session with a fleet core so it has a route from the fleet's
routing is hugpy-agent's job (the inbound, central-facing arm); this server
never does it.

## Chat behavior

Chat runs the agent loop in conversation mode:

- A plain-text reply is a complete answer. Tools are optional.
- Each user message gets its own step budget.
- An empty reply or an engine error ends the turn with that error. The loop
  never demands a tool call.
- `<tool_call>` and `<tool_response>` markup never shows up as reply text.
- A `final_answer` call that has no answer gets one repair prompt.

## Model tools

The harness imports the standalone `hugpy_tools` catalog, so file, search,
text, data, hash, time and web tools work in both the TUI and plain chat.
Without optional tools, a loop has 36 tools, including `fs_read`, `fs_write`,
`fs_glob`, `shell`, `ask_operator` and `final_answer`.

- Tools execute **on the harness machine**, jailed to `--workspace`, even when
  the model runs on a remote wrapper or fleet.
- Writes, network access and host operations go through `--policy`.
- `--optional-tools` adds document, media, rendering, image, desktop and VM
  tools. Their backends load only when called; install the matching
  `hugpy-tools` extras, for example
  `pip install 'hugpy-tools[images,files,render]'`. Rendering also needs
  Playwright Chromium.
- Generated images and screenshots go to `.hugpy_wrapper/tool-artifacts` in
  the workspace.

## Using the core as a library

```python
from hugpy_harness.config import Config
from hugpy_harness.loop import AgentLoop

loop = AgentLoop(Config(base="http://127.0.0.1:52017", model="MODEL", workspace="."),
                 extra_tool_providers=[my_provider])     # provider(ctx) -> [ToolSpec]
print(loop.run("task"))
```

The hooks are:

| hook | purpose |
|---|---|
| `AgentLoop(rag_factory=, comms_factory=, catalog_provider=, extra_tool_providers=)` | constructor hooks |
| `AgentLoop.turn_ctx(run_id)` | spans one drive |
| `Gateway.request_ctx(chat_url, key)` | per-request headers and cancel |
| `Config.state_dir` and `config.state_path()` | per-workspace state; default `.hugpy_wrapper/chat` |
| the `hugpy_harness.tools` entry-point group | each entry is a `provider(ctx) -> [ToolSpec]` |
| `imports.constants.SERVICE_NAME` | the harness's name (declared, not yet read anywhere) |
| the `imports/` layer | `constants`, `functions`, `classes`, `init_imports` |

Details and caveats are in
[`docs/agent-and-harness.md`](../../docs/agent-and-harness.md#the-hooks-a-host-uses).

## Development

```sh
python -m pytest -q tests          # 20 tests, offline
bash packaging/debian/build.sh     # runs plain `python3`; BUILD_PYTHON is not read (pure-Python wheels)
```

Build, release and test conventions for the whole tree are in
[`docs/development.md`](../../docs/development.md).

## Changes

### 0.1.23 (2026-10-20; local deb build from staging; 0.1.20–0.1.22 were earlier cuts of the same day)

- `hugpy-harness tui --serve URL`: the TUI as a client of a harness session server (the console's Station tab runs exactly this).


- `hugpy-harness serve` (`hugpy_harness.serve`, console script `hugpy-harness-serve`):
  the harness's named sessions over loopback HTTP, speaking the `HugpyServeClient`
  route vocabulary. Bearer token beyond loopback; loopback Host/same-origin only without one.
- `WrapperClient.raw_events` and the pending question's text/options are exposed for remote clients.
- A media task's result is appended to the session's conversation, not only to the display transcript.
- `hugpy_harness.toolserver_client` exists; the TUI's toolserver status line and loci registry
  discovery imported it but it was never shipped, so both silently degraded.
- `AbstractServeClient.roll_mode` posted keyword arguments to an `Http.post(path, body)` helper and raised.

### 0.1.8 (2026-10-15; installed on ae, not yet in the apt pool)

- Fleet connection. Endpoint resolution falls back to `HUGPY_FLEET_URL`, then
  `~/.config/hugpy/fleet.endpoint.json`, then `/etc/hugpy/fleet.endpoint.json`.
  The key falls back to `HUGPY_FLEET_API_KEY`. When no endpoint is found, the
  error now names `--base` and `HUGPY_FLEET_URL`.
- Evicting a model on a fleet core (no `/v1/operator/actions`) reports "this
  endpoint has no eviction" instead of a raw HTTP error.

### 0.1.7 (2026-10-15; apt, bundled in hugpy-agent 0.1.118)

- The harness became the core library for hugpy-agent. 0.1.7 added
  `rag_factory`, `comms_factory`, `catalog_provider` and `extra_tool_providers`
  on `AgentLoop`, the `turn_ctx` method, `Gateway.request_ctx`,
  `Config.state_dir` and `state_path()`, the `hugpy_harness.tools` entry-point
  group, and the `imports/` layer, which includes `SERVICE_NAME`.

### 0.1.6 (2026-10-14; PyPI)

- The right-hand resources pane shows one box per loaded model, and `e` or
  Delete unloads a model.
- The sessions pane lists the loaded models each session used.
- A model is strictly per session.
- Tool markup (`<tool_call>` and `<tool_response>`) is gated out of reply
  text.

### 0.1.5 (2026-10-14)

- Pickers search as you type.
- `/resources full` is the compute panel in the terminal, with eviction.
- A local model's reply is no longer shown twice.

Local Harness controls:

- `/models` offers an engine filter; `/models gguf`, `/models transformers`, or `/models all` selects it directly. Each model shows its size and engine/runner.
- `/settings` chooses the inference endpoint: Fleet or a specific wrapper. `/observe` selects resource telemetry independently of inference routing.
- `/rename NAME` names a session. `/pin` keeps it near the top of the session list. The session retains its model, endpoint and controls across restarts.
- `/session-settings` shows controls; append `max_tokens 1024`, `temperature 0.2`, `no_think false`, or `alloc_mode auto|gpu-only|ram-only|max-gpu|max-ram` to set one. Current controls also appear in the sidebar.
- `/clear` removes this session's transcript and model context. `/clear-model` keeps the transcript and resets only model context.
- Model reasoning is retained in a collapsed thinking card after the reply; Enter/Space/click expands it. The activity line follows the transcript and reports observed load stages; estimates without evidence remain unknown.

Local agents receive the actual registered toolset in their init prompt. When Core is available they get `fleet_list_models`, `fleet_list_workers` and `fleet_call_model` (chat, embeddings, image generation). The shared toolserver client exposes `toolserver_list_categories`, `toolserver_list_tools` and `toolserver_call_tool` through Core's `/ts/*` relay. Core uses its existing management access gate for this relay; inference-only keys do not grant toolserver operator access. Toolserver calls retain target-specific policy checks and the shared privileged-tool allowlist. `HUGPY_AGENT_TOOLSERVER=0` disables the bridge. The shared MCP catalog gives frontier clients the same explicit toolserver names and argument fields; older call names remain accepted as compatibility aliases.

## User-defined persistent sessions

See [Named sessions](NAMED_SESSIONS.md) for arbitrary names, persistent model destinations, stable routes and delegation shared with Agent.


Pending human input (0.1.17): structured model questions and provider approvals
join an inline queue beside the composer. Arrival never opens a modal, takes
keyboard focus or replaces a prompt draft. Press Ctrl-A (or click the pending
notice, or use /questions) to answer when ready. Tab / Shift-Tab cycles requests;
Up / Down or 1–9 selects a choice; Enter sends it. Questions also accept a custom
answer in a separate editor. Esc returns to the exact prompt draft/cursor;
answers and selection survive switching queued requests. Answering one request
does not automatically focus the next. A request resolved elsewhere stays on a
resolved notice until you explicitly leave, preventing late keystrokes from
answering another question or sending your draft. Requests and answers remain
scoped to their originating session, including reused provider request IDs.
Provider wait timeouts still apply. Plain numbered prose remains ordinary text;
selectable inputs use structured question events such as ask_operator.

Task inputs are available with `/task`: choose a task, filter compatible models,
and browse local files for image-to-image, inpainting, image-to-video or audio
transcription. Type the prompt in the main input pane and press Enter to run;
`/task-params {"steps":20,"size":"512x512"}` sets task options, and `/task cancel`
returns to chat. Inputs are uploaded as bytes to the selected endpoint. Results
are saved in `hugpy-outputs` under the workspace, with a JSON result record.
The chat target and its model context stay separate from task execution.

Local agents expose basic file and shell tools directly. Additional utilities
are nested under `local_list_categories`, `local_list_tools` and
`local_call_tool`. The dispatcher preserves the target's permission policy,
workspace confinement and durable execution context. Frontier configurations
retain their existing tool exposure.

Ctrl-R focuses resources. Up/Down and page keys scroll that column; Tab selects
an entry, `[`/`]` cycles wrappers, `t`/`T` cycles types, `y` copies the full error
or process details, and Enter expands errors. Drag selection stays within its
starting column. Ctrl-S saves unsent input; Ctrl-B or `/drafts` restores it
without sending. Task drafts retain their task, model and input-file choices.


<!-- hugpy-public-downloads-gallery -->
## Downloads and feature gallery

Get the Debian packages, architecture details, SHA-256 checksums and installation commands from [Hugpy Downloads](https://hugpy.ai/downloads/). The [feature gallery](https://hugpy.ai/downloads/#features) shows the console, Agent, Station, Media and Video interfaces. Screenshots reflect the versions captured; the current interface may differ.

