docs: tool plugins guide and config cross-links

This commit is contained in:
2026-07-24 16:05:10 +08:00
parent ac3230647a
commit 278c930fae
4 changed files with 188 additions and 2 deletions
+5
View File
@@ -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).
+5 -2
View File
@@ -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).
+172
View File
@@ -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
+6
View File
@@ -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"