diff --git a/README.md b/README.md index db9f79f..6332d88 100644 --- a/README.md +++ b/README.md @@ -143,10 +143,15 @@ access_key_or_token = "sk-..." # tool_directives = '''### Workspace ...''' confirm_destructive = true max_context_tokens = 200000 +# Optional third-party tools (entry points plyngent.tools); default load none. +# tool_plugins = ["acme"] +# See doc/plugins.md. ``` Per-provider **`timeout`** is passed to the HTTP session for chat/completions, Responses, and `GET /models`. A single number sets one timeout; `{ connect, read }` splits TCP/TLS setup vs idle wait between response bytes (SSE can run longer than `read` while chunks keep arriving). Tool/process timeouts (`run_command`, PTY, policy confirm) are separate. +Third-party **tool plugins**: install a package that declares `project.entry-points."plyngent.tools"`, then allowlist it with `[agent].tool_plugins`. Details: [doc/plugins.md](doc/plugins.md). + Supported provider presets today: `openai` (default if `preset` is omitted; default models `gpt-5.4` / `gpt-5.4-mini` / `gpt-5.4-nano` when `models` is omitted), `openai-compatible`, `deepseek` (OpenAI convention; default models `deepseek-v4-flash` and `deepseek-v4-pro` if `models` is omitted). Anthropic presets are modeled in config but not wired in the runtime client yet. If `[database]` is omitted (or SQLite `url` is unset/empty), chat uses a durable file under the user data dir (e.g. `~/.local/share/plyngent/chat.db` on Linux). Set `url = ":memory:"` for a true in-memory SQLite (CLI warns; no file; useful for tests). diff --git a/doc/architecture.md b/doc/architecture.md index d1bde2a..ecc5666 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -20,9 +20,12 @@ - memory: Storage controlling for sessions and messages. - 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. + - agent: Tool loop, streaming, usage, compact; `@tool` / tags / registry. + - tools: Workspace file/process/VCS/chat/todo tools; catalog; plugins; + instance/session context and views. - 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). +Tool plugins (third-party entry points): [plugins.md](./plugins.md). + diff --git a/doc/plugins.md b/doc/plugins.md new file mode 100644 index 0000000..19dfce4 --- /dev/null +++ b/doc/plugins.md @@ -0,0 +1,172 @@ +# Tool plugins + +Third-party tools install as Python packages and register through the +**`plyngent.tools`** entry-point group. The host **allowlists** which plugins +load; default is **load none**. + +Importing a plugin only **registers** tools into the process catalog. The CLI +still **selects** which names become model-visible (`surface=local` today). + +## Layers (short) + +```text +DEFINE+REGISTER @tool → ToolDefinition + catalog entry (source=plugin:…) +SELECT catalog.select(surface=…, …) → list[ToolDefinition] +EXECUTE ToolRegistry (confirm, instance/session bind, invoke) +``` + +| Layer | Question | +|--------|----------| +| Register | What tools exist in this process? | +| Select | Which may the model see this session? | +| Execute | How are they confirmed and run? | + +Builtins use the same path with `source=builtin`. Plugins must not reuse a +builtin (or another plugin) **tool name** — registration fails with a +collision error. + +## Author a plugin package + +### 1. Define tools with `@tool` + +```python +# acme_plyngent/tools.py +from plyngent.agent import ToolTag, tool + + +@tool(name="acme_ping", tags=ToolTag.LOCAL) +async def acme_ping() -> str: + """Return a fixed pong (example plugin tool).""" + return "pong" +``` + +Notes: + +- Prefer **`async def`** handlers (builtins are async-first). +- Default **`tags=ToolTag.LOCAL`**. Set at least one of `LOCAL` or `PUBLIC`. +- Workspace/exec tools should stay **`LOCAL`** unless reviewed for a shared host. +- Session-only helpers may add **`PUBLIC`** and **`SESSION_STATE`** when they + only touch session data and host-approved APIs (see plans under `doc/plans/`). +- Optional soft-confirm bits: **`YOLO`** (eligible for YOLO auto-approve), + **`TRUSTABLE`** (grant once per session after approve). Hard denylists are + never YOLO-skipped. +- Docstring becomes the model-facing description; parameter schemas come from + type hints. +- Use `register=False` only for unit tests that build a private registry. + +### 2. Entry point + +Call a zero-arg loader (or import a module) so `@tool` runs under the plugin +registration source: + +```python +# acme_plyngent/__init__.py +def load() -> None: + """Import tool modules so @tool registers into the process catalog.""" + from acme_plyngent import tools as _tools + + _ = _tools # registration side effects +``` + +Declare the entry point (PEP 621 / setuptools / hatch style): + +```toml +# pyproject.toml of the plugin package +[project] +name = "acme-plyngent" +version = "0.1.0" +dependencies = [ + "plyngent", # or pin a compatible range +] + +[project.entry-points."plyngent.tools"] +acme = "acme_plyngent:load" +``` + +- Entry-point **name** (`acme`) is the **plugin id** used in config allowlists + and in catalog metadata (`plugin:acme`). +- Value is `module:attr`. If `attr` is callable, plyngent calls it after load; + if it is a module, import alone is enough when tools registered at import time. + +Install the plugin into the **same environment** as `plyngent` (editable fine): + +```bash +pdm add acme-plyngent +# or: pip install -e ./path/to/acme-plyngent +``` + +## Enable plugins in config + +In the user config (`plyngent config path`), under **`[agent]`**: + +```toml +[agent] +# Allowlist of entry-point names. Default empty = load no plugins. +tool_plugins = ["acme"] +# tool_plugins = ["*"] # every discovered plyngent.tools entry point +# Never load these names even if listed or matched by *: +# tool_plugins_disable = ["legacy"] +``` + +| Setting | Meaning | +|---------|---------| +| `tool_plugins = []` / omitted | Load **no** plugins (safe default). | +| `tool_plugins = ["acme", "other"]` | Load only those entry-point names. | +| `tool_plugins = ["*"]` | Load all discovered plugins. | +| `tool_plugins_disable = ["x"]` | Skip `x` even when allowlisted or `*`. | + +Restart the chat REPL after changing config so the tool registry rebuilds. + +CLI load order (simplified): + +1. `register_builtin_tools()` +2. `load_plugin_tools(tool_plugins, disable=tool_plugins_disable)` +3. `catalog.select(surface="local")` → `ToolRegistry` + +Inspect the model surface in the REPL: + +```text +/tools --list +``` + +Listed tools show **tags** and **catalog source** (`builtin` vs `plugin:…`). + +## Policy checklist for authors + +1. Prefer **LOCAL** for host FS, shell, PTY, or arbitrary network side effects. +2. Do not shadow builtin names (`read_file`, `run_command`, todo tools, …). +3. Do not rely on process-global workspace/todo alone when a host binds + instance/session context — prefer the same patterns as builtins + (`get_workspace_root` already prefers instance policy; session state via + context when you need it). +4. Soft danger: annotate with `YOLO` / `TRUSTABLE` only when the product + confirm story matches; hard denylists stay in workspace/command policy. +5. Keep tools **async**; raise ordinary exceptions or return `error: …` strings + consistently with builtins. + +## Surfaces (local vs public) + +| Catalog select | Included tools | +|----------------|----------------| +| `surface="local"` (CLI) | `LOCAL` and/or `PUBLIC` | +| `surface="public"` | `PUBLIC` only | + +There is **no** multi-tenant public host in-tree yet; `surface=public` is +available for hosts that want a PUBLIC-only select (builtins: mainly the +todo series). Plugins should not set `PUBLIC` on workspace/exec tools by +default. + +## API reference (code) + +| Piece | Module | +|-------|--------| +| `@tool`, `ToolTag`, `ToolRegistry` | `plyngent.agent` / `plyngent.agent.tools` | +| Catalog, `ToolSource`, `select` | `plyngent.tools.catalog` | +| `load_plugin_tools` | `plyngent.tools.plugins` | +| Config fields | `AgentConfig.tool_plugins`, `tool_plugins_disable` | + +## Related docs + +- [architecture.md](./architecture.md) — package layout +- [plyngent.example.toml](./plyngent.example.toml) — config sample +- Design notes (may lag code): [plans/](./plans/) — registration, tags, PUBLIC surface diff --git a/doc/plyngent.example.toml b/doc/plyngent.example.toml index 2b894b0..1befe7d 100644 --- a/doc/plyngent.example.toml +++ b/doc/plyngent.example.toml @@ -38,6 +38,12 @@ max_context_tokens = 200000 # compact_user_prefix = "Summarize the following conversation:\n\n{transcript}" # compact_seed_text = "Compacted from {src}:\n\n{summary}\n\nCarry on." +# Third-party tools (entry-point group plyngent.tools). Default: load none. +# See doc/plugins.md. +# tool_plugins = ["acme"] +# tool_plugins = ["*"] +# tool_plugins_disable = ["legacy"] + # --- OpenAI-compatible (vLLM, LiteLLM, OpenAI, …) --- [providers.openai_compat] preset = "openai-compatible"