From fb61f6bc32a13f279cc2ed121cc532fb0ebdf2a2 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Wed, 15 Jul 2026 13:51:11 +0800 Subject: [PATCH] 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. --- CLAUDE.md | 28 +++--- README.md | 174 ++++++++++++++++++++++++++++++++++++++ doc/architecture.md | 15 ++-- doc/plyngent.example.toml | 46 ++++++++++ 4 files changed, 238 insertions(+), 25 deletions(-) create mode 100644 doc/plyngent.example.toml diff --git a/CLAUDE.md b/CLAUDE.md index d364d8f..970a19e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/README.md b/README.md index 86feaaa..6599be6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/doc/architecture.md b/doc/architecture.md index d707a2d..d1bde2a 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -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). diff --git a/doc/plyngent.example.toml b/doc/plyngent.example.toml new file mode 100644 index 0000000..454a027 --- /dev/null +++ b/doc/plyngent.example.toml @@ -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 }