mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
5b0c9be90b
Register REPL slash commands as a Click Group (params, UsageError, help). Dispatch via awaitlet.async_def so sync command bodies can awaitlet() async memory/confirm work. Completer and /help derive from the registry.
140 lines
9.0 KiB
Markdown
140 lines
9.0 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## 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.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
pdm install # first-time dependency setup
|
|
pdm sync # sync after pulling changes
|
|
|
|
pdm run basedpyright . # type checking (basedpyright, "recommended" strictness)
|
|
pdm run ruff check . # linting
|
|
pdm run ruff format . # formatting
|
|
pdm run pytest # tests (pytest-asyncio auto mode)
|
|
```
|
|
|
|
## Architecture
|
|
|
|
### Data modeling: `msgspec.Struct`
|
|
|
|
All protocol models use `msgspec.Struct` — not dataclasses, not Pydantic. Optional fields use `msgspec.field(default=UNSET)` / `default=UNSET` with type `T | Unset`.
|
|
|
|
`typedef.py`: `Unset = UnsetType` (plain assignment so msgspec recognizes it; do **not** use PEP 695 `type Unset = ...`). Multi-struct unions must be **tagged** via `tag_field` / `tag` (e.g. `role`, `type`) for decode. `JSONSchema` is `dict[str, Any]`.
|
|
|
|
### Protocol layering (`lmproto/`)
|
|
|
|
```
|
|
lmproto/openai_compatible/ ← Base: model, config, client
|
|
lmproto/deepseek/openai_compat/ ← Extends base via inheritance + extra fields
|
|
```
|
|
|
|
- **`openai_compatible/model.py`** — tagged chat messages (`SystemChatMessage`, `UserChatMessage`, …), tools, request/response, streaming chunks.
|
|
- **`openai_compatible/client.py`** — `BaseOpenAIClient` / `OpenAIClient` via `niquests` async + SSE.
|
|
- **`openai_compatible/config.py`** — `OpenAIConfig` (token + base URL).
|
|
- **DeepSeek** — `DeepseekOpenAIClient`; models add `reasoning_content`, `prefix`, `ThinkingOptions`.
|
|
|
|
### Config (`config/`)
|
|
|
|
TOML load/store (`ConfigStore`): `[providers]` tagged union presets, `[database]` section. Default path via platformdirs.
|
|
|
|
### Runtime (`runtime/`)
|
|
|
|
`create_client(provider)` maps config `Provider` → protocol client. OpenAI / openai-compatible / deepseek(openai convention) supported; anthropic and deepseek anthropic convention raise `ProviderNotSupportedError`.
|
|
|
|
### Memory (`memory/`)
|
|
|
|
Async SQLAlchemy + aiosqlite. `MemoryStore`: schema init (+ lightweight SQLite `ALTER` for new columns), default local user, sessions (bound to `workspace` path), messages stored as msgspec chat message JSON.
|
|
|
|
### Agent (`agent/`)
|
|
|
|
- **`ChatClient`** Protocol for `chat_completions`.
|
|
- **`@tool` / `ToolRegistry`**: decorator infers JSON Schema from type hints; execute tools by name.
|
|
- **`run_chat_loop`**: multi-round tool loop; default **streaming** text deltas + stream tool-call merge; parallel tools; tool-result char budget; soft context compact on request (**API-calibrated** after first usage when available); cooperative cancel points; optional `on_limit`.
|
|
- **`ChatAgent`**: optional `MemoryStore` (user message persisted immediately; assistant/tools on success); `stream`; system prompt; `retry()` when history ends with a user message (failed/cancelled turn or orphan after resume).
|
|
- **`/compact`**: soft-compact tool dumps → model summary (no tools) → **new** session seeded with summary message.
|
|
- Events: text_delta, **reasoning_delta**, assistant_message, tool_call/result, max_rounds, **error** (`retryable`/`source`), **cancelled** (`reason`), **usage** (`TokenUsage`).
|
|
- Usage: API `usage` from completions (stream with `include_usage`); **char≈token fallback** (~4 chars/token) when omitted; **context size** = last request ``prompt_tokens`` (API preferred); `last_turn_usage` / `session_usage` are **billed sums** (tool rounds re-send history); CLI end-of-turn + `/status`.
|
|
- Config ``[agent]``: `system_prompt`, `max_tool_result_chars`, `parallel_tools`, `confirm_destructive`, `path_denylist`, `max_context_tokens` (default 200k est. tokens).
|
|
|
|
### Tools (`tools/`)
|
|
|
|
Module-level `@tool` handlers. Call `set_workspace_root()` before use.
|
|
|
|
- **`workspace`**: path resolve under root; path substring denylist; command basename denylist; clearer deny messages.
|
|
- **`file`**: `read_file`, `write_file`, `listdir`, `tree`, `glob_paths`, `grep_files` (regex, skip VCS/binary), `edit_replace`, `edit_lineno` (1-based range), `copy_path` / `move_path` / `delete_path`.
|
|
- **`process`**: `run_command` (argv, no shell, timeout, optional stdin/env); PTY `open_pty` / `read_pty` / `write_pty` / `close_pty` (**Unix only**: `pty`+`fork`).
|
|
- PTY: structured status (`alive`/`exit_code`/`data`); `read_pty(..., until=)`; session limit/idle TTL/output budget; close SIGTERM→SIGKILL.
|
|
- CLI limit hooks: interactive confirm to raise tool-loop rounds, PTY session cap, or PTY output budget.
|
|
- Destructive confirms: `classify_danger` + `ToolRegistry(on_confirm=…)`; CLI default deny; config `confirm_destructive` / `path_denylist`.
|
|
- **`vcs`**: read-only VCS tools (`vcs_kind` / `vcs_status` / `vcs_diff` / `vcs_log` / `vcs_branch`) via `VcsBackend` protocol; **git** implemented; detectors are pluggable for other systems.
|
|
- **`chat`**: human prompts as tools — `ask_user`, `choose_user`, `form_user` (shared `prompting` core).
|
|
- **`DEFAULT_TOOLS`**: file + process + vcs + chat tool list for a `ToolRegistry`.
|
|
|
|
### Prompting (`prompting.py`)
|
|
|
|
Shared interactive I/O: `ask` / `choose` / `form` / `confirm` with pluggable backend; non-TTY uses defaults or errors. CLI limit/confirm hooks and chat tools both use this. Async helpers serialize prompts (`run_prompt_async`).
|
|
|
|
### CLI (`cli/`)
|
|
|
|
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`).
|
|
|
|
### Composition utility: `Forward` descriptor
|
|
|
|
`utils/components.py` — `Forward[T]` / `forward()` for attribute forwarding on composed objects.
|
|
|
|
### Type annotations are mandatory
|
|
|
|
Basedpyright `recommended`. Ruff includes `ANN` (private return types `ANN202` ignored). Prefer PEP 695 aliases except where msgspec requires plain assignment (`Unset`).
|
|
|
|
## Roadmap notes (single-user → platform)
|
|
|
|
- **Phase D (context quality)**: soft char budget, request compact, `/compact`, richer errors/cancel, workspace sessions. Context size is **char estimate** plus optional API usage when reported.
|
|
- **No local tokenizer stage** for now.
|
|
- **Phase E**: tooling depth (grep/glob, VCS backends; prefer `edit_replace` / `edit_lineno` over model-generated patches).
|
|
- **Phase F (providers + usage v2)**: API `usage` + char-based estimate fallback; session/turn totals; `/status` + end-of-turn. Optional later: cost, real tokenizer.
|
|
- **Phase G (CLI polish + hardening)** — single-user only; multi-tenant stays Phase H.
|
|
|
|
**Done**
|
|
- G0: `prompting` (`ask`/`choose`/`form`/`confirm`) + chat tools (`ask_user`/`choose_user`/`form_user`)
|
|
- G1: `ReasoningDeltaEvent`, `/stream`, `/verbose`
|
|
- G2: `/rename`, `/delete` (confirm), `/export md|json`
|
|
- G2.5: Click slash registry (`cli/slash.py`) + `awaitlet` for sync Click / async memory; auto `/help`; completer from `slash.list_commands`
|
|
|
|
**Planned**
|
|
|
|
- **G3 — Input**: multiline (`"""` … `"""`) and/or `/edit` via `$EDITOR`
|
|
- **G4 — One-shot**: `plyngent chat -p/--prompt` (and stdin non-TTY); exit codes; non-interactive safety (`--yes` / deny destructive)
|
|
- **G5 — Hardening**: tool timeouts/defaults review, config error clarity, cancel edges, optional `--log-level`, no secrets in status/export
|
|
- **G6 — Docs**: real README, example TOML, CLAUDE/help accuracy
|
|
|
|
Milestone order: G3 → G4 → G5 → G6.
|
|
|
|
- **Phase H**: multi-tenant platform (`router/`, auth, sandboxed tools, web).
|
|
|
|
## Commit messages
|
|
|
|
Scoped commit messages, not conventional commits:
|
|
|
|
```
|
|
<scope>: <brief description>
|
|
```
|
|
|
|
Examples: `deps: add fastapi`, `core/mq: fix incorrect message sending`, `router: add routing service for xxx`, `test/webserver: change test client`, `ci/lint: run ruff style check`.
|
|
|
|
Check `git log` for the full convention.
|