docs: README, example config, and Phase G completion notes

Document install, config, chat/one-shot, slash commands, workspace and
safety model. Add doc/plyngent.example.toml; refresh CLAUDE and architecture.
This commit is contained in:
2026-07-15 13:51:11 +08:00
parent 0dc5c876ee
commit fb61f6bc32
4 changed files with 238 additions and 25 deletions
+11 -17
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project overview
Plyngent is an LLM chat and agent toolkit (Python 3.14+, PDM-managed). Early development: protocol clients, config, async memory, and a runtime client factory exist. Agent, CLI, web, tools, and router are still planned/stubbed.
Plyngent is an LLM chat and agent toolkit (Python 3.14+, PDM-managed). Single-user CLI is usable: protocol clients, config, async memory, agent tool loop, workspace tools, and REPL/one-shot chat. Multi-tenant `router/` / web remain Phase H.
## Commands
@@ -83,15 +83,12 @@ Shared interactive I/O: `ask` / `choose` / `form` / `confirm` with pluggable bac
Click app + readline REPL. Entry: `plyngent` / `python -m plyngent`.
- **`plyngent chat`**: provider/model selection (flags or interactive), SQLite sessions via config `[database]` (file DB under user data if unset/`:memory:`), sessions bound to workspace dir; resumes **most recently updated** session for cwd/`--workspace` by default (`--new` / `--session`).
- Slash commands: Click group in `cli/slash.py` (params/help/`UsageError`); dispatch via `awaitlet.async_def` + `awaitlet()` for async work. Completer reads `slash.list_commands`.
- Explicit `/resume` or `--session` from another workspace prompts: **keep** session path, **update** binding to current, or **abort**.
- Failed/cancelled turns: user message kept in DB; partial assistant/tool rolled back; Ctrl+C cancels in-flight turn; **TTY confirms** off-loop; auto-retry 10s/20s/30s then `/retry` (no duplicate user message).
- **`plyngent providers`**: list config providers.
- **`plyngent config path|edit`**: print or open config in `$EDITOR` (`shlex`-split, e.g. `codium --wait`).
- If no providers and `$EDITOR` is set, chat/providers prompt to edit config then reload.
- Tools default on (`--tools` / `--no-tools`); workspace defaults to cwd; `--max-rounds` default 32.
- Readline/editline: Tab completion; input history file under platformdirs user data (`repl_history`).
- **`plyngent chat`**: provider/model (flags or interactive); SQLite sessions via `[database]` (file DB under user data if unset/`:memory:`); sessions bound to workspace; resume latest for cwd/`--workspace` by default (`--new` / `--session`). One-shot: `-p/--prompt` and non-TTY stdin; exit codes 0/1/2/3; `--yes`, `--stream/--no-stream`, `--quiet`. Root `--log-level`.
- Slash: Click group in `cli/slash.py` + `awaitlet` for async work; Tab completer from registry + ParamType `shell_complete`. Multiline `"""``"""`; `/edit` via `$EDITOR`.
- Explicit `/resume` or `--session` from another workspace prompts: **keep** / **update** / **abort**.
- Failed/cancelled turns: user message kept; partial assistant/tool rolled back; Ctrl+C cancels turn; TTY confirms off-loop; auto-retry 10s/20s/30s then `/retry`.
- **`plyngent providers`**, **`config path|edit`**. No providers + `$EDITOR` → optional edit then reload.
- Tools default on; workspace defaults to cwd; `--max-rounds` default 32. Readline history under platformdirs (`repl_history`). PTY: `close_all` on chat exit.
### Composition utility: `Forward` descriptor
@@ -117,22 +114,19 @@ Basedpyright `recommended`. Ruff includes `ANN` (private return types `ANN202` i
- G3: multiline `"""``"""` input (`cli/input_text.py`); `/edit` via `$EDITOR` (`edit_text_in_editor`)
- G4: `plyngent chat -p/--prompt` (+ non-TTY stdin); exit codes 0/1/2/3; `--yes` / non-interactive confirm deny; `--stream/--no-stream`, `--quiet`
- G5: PTY master FD non-inheritable; `read_pty`/`close_pty` via `to_thread`; `PtyManager.close_all()` on chat exit; `--log-level`; clearer invalid TOML errors; export/status stay secret-free
- G6: README, `doc/plyngent.example.toml`, CLAUDE overview/CLI notes
**Planned**
- **G6 — Docs**: real README, example TOML, CLAUDE/help accuracy
Milestone order: G6.
Phase G complete for single-user CLI polish. Next roadmap work is Phase H or optional F (cost/tokenizer).
**PTY / process model (decision)**
Today `PtyManager` (`tools/process/pty_session.py`) is **in-process**: `pty.openpty` + `os.fork` → child `execvp`, parent holds master FD; tools are **sync** and run on the asyncio loop thread (blocking `select`/`read`/`waitpid`). Session registry is process-global. Safe enough for single-user CLI if the child path stays fork-then-exec only.
Today `PtyManager` (`tools/process/pty_session.py`) is **in-process**: `pty.openpty` + `os.fork` → child `execvp`, parent holds master FD; `read_pty`/`close_pty` run via `to_thread`. Session registry is process-global. Safe enough for single-user CLI if the child path stays fork-then-exec only.
| Question | Answer |
|----------|--------|
| Separate PTY supervisor process so the main app is safer with threads/greenlets? | **Not for Phase G.** Defer to **Phase H** (sandbox / multi-tenant) or if we hit real FD-leak / freeze bugs. |
| Why not now? | Fork-then-exec is already the right shape; rewrite cost (IPC, lifecycle, tests) dwarfs single-user CLI risk. awaitlet greenlets are slash-only; PTY is not forked from a worker thread today. |
| What *does* belong in G5 | (1) mark master (and ideally app) FDs non-inheritable before/after fork; (2) avoid unbounded loop block — `to_thread` or `add_reader` for long `read_pty`/`close`; (3) CLI exit / `finally`: `PtyManager.close_all()`; (4) keep `os.fork` on the loop/main thread — do not move `open` to a thread pool without redesign. |
| G5 (done) | Master FD non-inheritable; `read_pty`/`close_pty` via `to_thread`; chat exit `close_all`; fork stays on loop/main thread. |
| Phase H | Optional PTY helper process (JSON/Unix socket), or subprocess+PTY with asyncio reaping; sandboxed tools, multi-session isolation. |
- **Phase H**: multi-tenant platform (`router/`, auth, sandboxed tools, web). Optional **out-of-process PTY host** if isolation is required.
+174
View File
@@ -1 +1,175 @@
# plyngent
Single-user LLM chat and agent toolkit for the terminal.
Python **3.14+**, managed with [PDM](https://pdm-project.org/). OpenAI-compatible APIs (including DeepSeek OpenAI-compat), SQLite session memory, workspace-scoped file/process/VCS tools, and a readline REPL with slash commands.
## Install
```bash
# from a clone
pdm install
pdm run plyngent --help
# or editable install into your environment
pdm install
# entry point: plyngent
```
Dev checks:
```bash
pdm run basedpyright .
pdm run ruff check .
pdm run ruff format .
pdm run pytest
```
## Configure
Default config path (platformdirs):
```bash
plyngent config path
plyngent config edit # opens $EDITOR (e.g. codium --wait)
```
Copy the example and fill in a real token:
```bash
cp doc/plyngent.example.toml "$(plyngent config path)"
# then edit providers
```
Minimal shape:
```toml
[providers.local]
preset = "openai-compatible"
url = "https://api.openai.com/v1"
access_key_or_token = "sk-..."
[providers.local.models]
"gpt-4o-mini" = { text = true }
[agent]
system_prompt = "You are a careful coding assistant."
confirm_destructive = true
max_context_tokens = 200000
```
Supported provider presets today: `openai`, `openai-compatible`, `deepseek` (OpenAI convention). Anthropic presets are modeled in config but not wired in the runtime client yet.
If `[database]` is omitted (or SQLite `url` is empty/`":memory:"`), chat uses a durable file under the user data dir (e.g. `~/.local/share/plyngent/chat.db` on Linux).
## Chat
### Interactive REPL
```bash
plyngent chat
plyngent chat --provider local --model gpt-4o-mini
plyngent chat --workspace /path/to/project --new
plyngent chat --session 3
```
| Flag | Meaning |
|------|---------|
| `--provider` / `--model` | Select from config (required when multiple and non-interactive) |
| `--workspace` | Tool root (default: cwd); sessions bind to this path |
| `--new` / `--session ID` | Fresh session vs resume by id |
| `--tools` / `--no-tools` | Default tools on |
| `--max-rounds` | Tool-loop rounds per turn (default 32) |
| `--stream` / `--no-stream` | Streaming deltas (default on) |
| `--quiet` | Less status on stderr |
| `--yes` | Allow destructive tools without confirm (also for one-shot) |
| `--log-level` | On the root CLI: `DEBUG`, `INFO`, `WARNING`, … |
Sessions resume the **most recently updated** session for the current workspace unless you pass `--new` or `--session`.
### One-shot (scripts / CI)
```bash
plyngent chat -p "Summarize README.md" --provider local --model gpt-4o-mini --no-stream
echo "hello" | plyngent chat --provider local --model gpt-4o-mini
```
Exit codes (one-shot):
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Config / usage error |
| 2 | Cancelled |
| 3 | Turn failed (API / incomplete) |
### Input ergonomics
- **Multiline**: start a message with `"""`, end a later line with `"""`.
- **`/edit`**: compose a turn in `$EDITOR` (empty buffer cancels).
- **Tab**: completes slash commands and some arguments (provider, model, on/off, export, `/help` targets).
### Slash commands
Type `/help` in the REPL for the live list. Common ones:
| Command | Purpose |
|---------|---------|
| `/status` | Provider, session, context/usage estimates |
| `/history [n]` | Recent messages |
| `/sessions` | Sessions for this workspace |
| `/new` `/resume` `/rename` `/delete` | Session lifecycle (`/delete` confirms) |
| `/export [md\|json] [path]` | Transcript from DB (no secrets) |
| `/compact` | Soft-compact + model summary into a **new** session |
| `/stream` `/verbose` `/tools` `/rounds` | Toggles and limits |
| `/retry` | Re-run incomplete last user turn (after error/cancel) |
| `/provider` `/model` | Switch without restarting |
| `/quit` | Leave the REPL |
User messages are saved immediately. On API error or Ctrl+C, partial assistant/tool output is discarded but the user message stays so `/retry` works after resume. Interactive auto-retry uses 10s / 20s / 30s delays.
## Workspace model
- **Workspace** = root for file/process/VCS tools (default cwd).
- **Session** = SQLite chat bound to a workspace path.
- Resuming a session from another directory prompts: keep session workspace, rebind to current, or abort.
## Tools (when enabled)
Default registry: file ops, `run_command` / PTY (Unix), read-only VCS (git), and human prompts (`ask_user` / `choose_user` / `form_user`).
Safety defaults:
- Paths stay under the workspace; optional `path_denylist` substrings.
- Command basename denylist (e.g. dangerous shells/utilities).
- Destructive tools (delete/move/overwrite) can require confirm (`confirm_destructive`; default deny in non-TTY).
- PTY sessions: caps, idle TTL, output budget; master FD is non-inheritable; sessions closed on chat exit.
## Usage / context (CLI)
- **Context size** prefers API `prompt_tokens` from the last model call; otherwise a char-based estimate (~4 chars/token).
- **Turn/session usage** sum billed completion usage across tool rounds (history is re-sent each round).
- Soft compact can calibrate from reported `prompt_tokens`. See `/status`.
## Other commands
```bash
plyngent providers # list configured providers
plyngent config path|edit
plyngent --log-level INFO chat ...
```
## Architecture (short)
See [doc/architecture.md](doc/architecture.md) and [CLAUDE.md](CLAUDE.md) for developers.
- **`lmproto/`** — OpenAI-compatible (+ DeepSeek) msgspec models and async SSE clients
- **`agent/`** — tool loop, streaming, usage, compact
- **`memory/`** — async SQLAlchemy sessions/messages
- **`tools/`** — workspace tools
- **`cli/`** — Click entry + slash registry (`awaitlet` bridges sync Click to async work)
- Multi-tenant / web (`router/`, real `web/`) are **not** in scope for the single-user CLI (Phase H).
## License
MIT
+7 -8
View File
@@ -18,12 +18,11 @@
- utils: Common utilities for code architecture.
- components: Utilities for class composition.
- memory: Storage controlling for sessions and messages.
- router: Multi-source capability routing and interception.
- interceptors: Capability of stealing a few arguments for partial re-routing.
- ...
- config: Plyngent configuration center.
- agent: Agent capabilities and controlling.
- tools: Client code of tool calls.
- cli: CLI for chat/agent application.
- web: Web service for chat/agent application.
- router: Multi-source capability routing (Phase H; not implemented).
- config: Plyngent configuration center (TOML).
- agent: Tool loop, streaming, usage, compact.
- tools: Workspace file/process/VCS/chat tools.
- prompting: Shared ask/choose/form for CLI and tools.
- cli: Click entry, slash registry, REPL, one-shot chat.
- web: Web service (Phase H; not implemented).
+46
View File
@@ -0,0 +1,46 @@
# Example plyngent configuration (no real secrets).
# Copy to the path shown by: plyngent config path
# Then set access_key_or_token and adjust models.
# Optional. If omitted (or SQLite url is empty / ":memory:"), the CLI uses a
# durable file under the platform user data dir (e.g. ~/.local/share/plyngent/chat.db).
# [database]
# implementation = "sqlite"
# url = "/path/to/chat.db"
[agent]
system_prompt = "You are a careful coding assistant. Prefer small, verified edits."
max_tool_result_chars = 32000
parallel_tools = true
confirm_destructive = true
path_denylist = ["/secrets/", ".ssh/", ".gnupg/"]
# Soft context budget in estimated tokens (~4 chars/token when API usage missing).
max_context_tokens = 200000
# --- OpenAI-compatible (vLLM, LiteLLM, OpenAI, …) ---
[providers.openai_compat]
preset = "openai-compatible"
url = "https://api.openai.com/v1"
access_key_or_token = "sk-replace-me"
[providers.openai_compat.models]
"gpt-4o-mini" = { text = true, cost_factor = 1.0 }
# --- OpenAI preset (same client shape; tag is "openai") ---
# [providers.openai]
# preset = "openai"
# access_key_or_token = "sk-replace-me"
# url = "https://api.openai.com/v1"
#
# [providers.openai.models]
# "gpt-4o-mini" = { text = true }
# --- DeepSeek (OpenAI-compatible convention) ---
# [providers.deepseek]
# preset = "deepseek"
# access_key_or_token = "sk-replace-me"
# url = "https://api.deepseek.com"
# extras = { convention = "openai" }
#
# [providers.deepseek.models]
# "deepseek-chat" = { text = true }