diff --git a/README.md b/README.md index 4a7036f..65ac158 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ rename / release-pipeline wiring. | Agent runtime | `graph/agent.py`, `server.py` | LangGraph `create_agent()` wired to the A2A handler, with streaming token capture for cost-v1 | | LLM gateway | `graph/llm.py` | OpenAI-compatible client pointed at LiteLLM — swap models by editing the gateway config, not the fork | | Subagents | `graph/subagents/config.py` | DeerFlow-pattern delegation via a `task()` tool; one placeholder `worker` ships | -| Starter tools | `tools/lg_tools.py` | Keyless general tools (`current_time`, `calculator` safe AST eval, `web_search` via DuckDuckGo, `fetch_url`) plus memory tools (`memory_ingest`, `memory_recall`, `memory_list`, `memory_stats`, `daily_log`) bound to the bundled store | +| Starter tools | `tools/lg_tools.py` | Twelve tools default-on: 4 keyless general (`current_time`, `calculator` safe AST eval, `web_search` via DuckDuckGo, `fetch_url`) + 5 memory (`memory_ingest`, `memory_recall`, `memory_list`, `memory_stats`, `daily_log`) bound to the KB store + 3 scheduler (`schedule_task`, `list_schedules`, `cancel_schedule`) bound to the scheduler backend | | Knowledge store | `knowledge/store.py` | sqlite + FTS5 (LIKE fallback). One `chunks` table for operator notes, daily-log entries, and conversation findings. Default-on; turn off with `middleware.knowledge: false` | | Scheduler | `scheduler/` | `schedule_task` / `list_schedules` / `cancel_schedule` tools backed by either a bundled sqlite scheduler or a Workstacean adapter (env-selected). Multi-agent-safe — every job is namespaced by `AGENT_NAME`. See [Schedule future work](./docs/guides/scheduler.md) | | Eval harness | `evals/` | Side-effect-verified A2A test harness — audit log + reply text + KB state. `python -m evals.runner` against a running agent. See [Eval your fork](./docs/guides/evals.md) | diff --git a/docs/guides/customize-and-deploy.md b/docs/guides/customize-and-deploy.md index 81fdeec..7ba875a 100644 --- a/docs/guides/customize-and-deploy.md +++ b/docs/guides/customize-and-deploy.md @@ -66,7 +66,9 @@ Replace with the skills your agent actually advertises over A2A. The `name` and ## 5. (Optional) Add domain tools -`tools/lg_tools.py` ships with `current_time`, `calculator`, `web_search`, `fetch_url`. Keep the ones you want, drop the rest, add your own. Update `get_all_tools()` at the bottom. Any tool returned from there becomes a checkbox in the wizard and drawer automatically. +`tools/lg_tools.py` ships with `current_time`, `calculator`, `web_search`, `fetch_url` plus 5 memory tools (`memory_ingest`, `memory_recall`, `memory_list`, `memory_stats`, `daily_log`) bound to the bundled `KnowledgeStore`. The 3 scheduler tools (`schedule_task`, `list_schedules`, `cancel_schedule`) are wired in separately by `server.py::_build_scheduler` when the scheduler backend is enabled. Keep the ones you want, drop the rest, add your own. Update `get_all_tools()` at the bottom of `tools/lg_tools.py`. Any tool returned from there (or from `_build_scheduler_tools`) becomes a checkbox in the wizard and drawer automatically. + +The memory tools are dropped automatically when `middleware.knowledge: false`; the scheduler tools when `middleware.scheduler: false`. See [Schedule future work](/guides/scheduler) and [Configuration](/reference/configuration#middleware) for the toggles. ## 6. (Optional) Configure subagents diff --git a/docs/guides/fork-the-template.md b/docs/guides/fork-the-template.md index d5472e4..3de87b0 100644 --- a/docs/guides/fork-the-template.md +++ b/docs/guides/fork-the-template.md @@ -43,7 +43,7 @@ Keep the `` / `` protocol block in `prompts.py` — the A2A ## 4. Replace the starter tools -`tools/lg_tools.py` ships with `current_time`, `calculator`, `web_search`, `fetch_url`. Keep what you want, drop the rest, add your own. Update `get_all_tools()` at the bottom of the file. +Twelve tools ship by default: `current_time`, `calculator`, `web_search`, `fetch_url` (keyless general) plus `memory_ingest`, `memory_recall`, `memory_list`, `memory_stats`, `daily_log` (bound to the bundled `KnowledgeStore`) plus `schedule_task`, `list_schedules`, `cancel_schedule` (bound to the scheduler backend). Keep what you want, drop the rest, add your own. Update `get_all_tools()` at the bottom of `tools/lg_tools.py`. See the [starter tools reference](/reference/starter-tools) for the shapes of the shipped ones. diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index e27d074..d74ea6b 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -36,6 +36,37 @@ Session memory is enabled by default. See [architecture § Session memory](/expl To persist memory across container restarts, mount a volume at whatever `MEMORY_PATH` resolves to. Without a volume the directory is ephemeral. +## Knowledge store + +The bundled `KnowledgeStore` (sqlite + FTS5) is enabled by default. See [Configuration § knowledge](/reference/configuration#knowledge) for the full guide. + +| Variable | Default | What | +|---|---|---| +| `KNOWLEDGE_DB_PATH` | (unset — uses YAML `knowledge.db_path`) | Runtime override for the sqlite path. Falls back to `~/.protoagent/knowledge/agent.db` when the resolved path is unwritable (e.g. running locally without `/sandbox`). | + +To opt out entirely, set `middleware.knowledge: false` in YAML. The memory tools (`memory_ingest`, `memory_recall`, etc.) are dropped from the agent loop when the store is disabled. + +## Audit log + +| Variable | Default | What | +|---|---|---| +| `AUDIT_PATH` | `/sandbox/audit/audit.jsonl` | Directory + filename of the JSONL audit log written by `AuditMiddleware`. Read by `evals/verify.py` for side-effect assertions. | + +## Scheduler + +The bundled scheduler is enabled by default. See [Schedule future work](/guides/scheduler) and [Configuration § scheduler](/reference/configuration#scheduler) for the full guide. **Backend selection** is env-driven; **enable/disable** lives in YAML (`middleware.scheduler`) so the drawer can toggle without a restart. + +| Variable | Default | What | +|---|---|---| +| `WORKSTACEAN_API_BASE` | (unset) | When set together with `WORKSTACEAN_API_KEY`, swaps the bundled `LocalScheduler` for the `WorkstaceanScheduler` HTTP adapter. | +| `WORKSTACEAN_API_KEY` | (unset) | Auth token sent as `X-API-Key` to Workstacean's `/publish`. | +| `WORKSTACEAN_TOPIC_PREFIX` | `cron.` | Override the bus topic the adapter fires on, when your Workstacean install uses a different convention. | +| `SCHEDULER_DB_DIR` | `/sandbox/scheduler` | Local backend: parent directory for `/jobs.db`. Falls back to `~/.protoagent/scheduler//jobs.db` when unwritable. | +| `SCHEDULER_INVOKE_URL` | `http://127.0.0.1:` | Local backend: where to POST `message/send` when a job fires. Override only if the agent's A2A endpoint isn't on localhost. | +| `SCHEDULER_DISABLED` | (unset) | Runtime escape hatch — set to `1` / `true` to drop the scheduler tools entirely without editing YAML. `middleware.scheduler: false` is the canonical opt-out. | + +> **protoLabs operators**: the fleet's Workstacean lives on the `ava` node. `WORKSTACEAN_API_KEY` is in the org's secrets manager under `secret-management → workstacean`. + ## Tracing (optional) | Variable | What | diff --git a/docs/reference/starter-tools.md b/docs/reference/starter-tools.md index 60d74d4..9ef37aa 100644 --- a/docs/reference/starter-tools.md +++ b/docs/reference/starter-tools.md @@ -1,11 +1,12 @@ # Starter tools -Nine tools ship in `tools/lg_tools.py`: +Twelve tools ship by default: - Four keyless general-purpose tools — `current_time`, `calculator`, `web_search`, `fetch_url` — that work without any state. - Five **memory tools** — `memory_ingest`, `memory_recall`, `memory_list`, `memory_stats`, `daily_log` — bound to the bundled `KnowledgeStore` (sqlite + FTS5, see [Configuration](/reference/configuration#knowledge)). +- Three **scheduler tools** — `schedule_task`, `list_schedules`, `cancel_schedule` — bound to the bundled scheduler backend (local sqlite or the Workstacean adapter, see [Schedule future work](/guides/scheduler)). -`get_all_tools(knowledge_store)` is the registry. When `knowledge_store` is `None` (the store is disabled in config) the memory tools are omitted automatically. +`get_all_tools(knowledge_store, scheduler)` is the registry. When `knowledge_store` is `None` the memory tools are omitted; when `scheduler` is `None` the scheduler tools are omitted. Both backends are constructed by default in `server.py`; opt out via `middleware.knowledge: false` / `middleware.scheduler: false` in `config/langgraph-config.yaml`. ## `current_time` @@ -158,6 +159,47 @@ async def daily_log(content: str) -> str Convenience wrapper around `memory_ingest` that writes to `domain='daily-log'` with today's UTC date as the heading. Same-day entries cluster under the same heading for `memory_list(domain='daily-log')`. +## `schedule_task` + +```python +@tool +async def schedule_task(prompt: str, when: str, job_id: str | None = None) -> str +``` + +Persist a future invocation. The agent receives `prompt` as a fresh turn when the schedule fires. + +`when` is either a 5-field cron expression (`"0 9 * * 1-5"` = every weekday at 9am) or an ISO-8601 datetime (`"2026-05-01T15:00:00"` = once at 3pm UTC on May 1). Backends auto-detect. + +`job_id` is optional — auto-generated as `-` when omitted. You'll need it later for `cancel_schedule`. + +Output: `"Scheduled job next at ."` on success. Returns `"Error: ..."` on malformed `when` or backend failure. + +Prompts are self-contained — the agent has no memory of the scheduling moment when the task fires, so write the prompt as a fresh turn ("review last week's pipeline incidents and post a summary"), not a reference ("do that thing we discussed"). + +## `list_schedules` + +```python +@tool +async def list_schedules() -> str +``` + +List the current scheduled jobs for *this* agent. Multi-agent isolation: each agent only sees jobs it created. + +Output: one job per line with id, next-fire timestamp, schedule, and prompt preview. Returns `"No scheduled jobs."` when empty. + +The Workstacean adapter intentionally returns `[]` (Workstacean owns scheduling state and its `list` action publishes asynchronously to a topic). Run the local backend or query Workstacean directly for live introspection there. + +## `cancel_schedule` + +```python +@tool +async def cancel_schedule(job_id: str) -> str +``` + +Cancel a scheduled job by id. Returns `"Canceled ."` or `"Error: no such job ."`. + +Cross-agent cancellation is blocked — `gina-personal` cannot cancel `gina-work`'s jobs even when sharing a sqlite path or a Workstacean install. + ## Adding your own Follow the same pattern: @@ -180,13 +222,15 @@ async def my_tool(required_arg: str, optional_arg: int = 5) -> str: return f"Success: {result}" ``` -Then append it to the keyless tool list in `get_all_tools()` — keep the conditional `_build_memory_tools(knowledge_store)` extension below it so the bundled memory tools still ship when a store is configured: +Then append it to the keyless tool list in `get_all_tools()` — keep the two conditional extensions below it so the bundled memory + scheduler tools still ship when their backends are configured: ```python -def get_all_tools(knowledge_store=None): +def get_all_tools(knowledge_store=None, scheduler=None): tools = [current_time, calculator, web_search, fetch_url, my_tool] if knowledge_store is not None: tools.extend(_build_memory_tools(knowledge_store)) + if scheduler is not None: + tools.extend(_build_scheduler_tools(scheduler)) return tools ``` @@ -195,5 +239,6 @@ See [Write your first tool](/tutorials/first-tool) for the full walkthrough. ## Related - [Configure subagents](/guides/subagents) — tools are allowlisted per subagent -- [Environment variables](/reference/environment-variables) — SSRF allowlist vars affect `fetch_url` +- [Environment variables](/reference/environment-variables) — SSRF allowlist vars affect `fetch_url`; scheduler backend selection lives there too - [Eval your fork](/guides/evals) — the eval harness exercises every tool listed here end-to-end +- [Schedule future work](/guides/scheduler) — the firing model + multi-agent isolation story behind the scheduler tools diff --git a/docs/tutorials/first-agent.md b/docs/tutorials/first-agent.md index ce12744..e58ad76 100644 --- a/docs/tutorials/first-agent.md +++ b/docs/tutorials/first-agent.md @@ -40,7 +40,7 @@ Walk through the four steps: 1. **Connect to your model.** Paste your API base URL (`https://api.openai.com/v1` for OpenAI direct, `http://localhost:4000/v1` for a local LiteLLM gateway) and API key. Click **Test connection & fetch models** — the dropdown fills with whatever the endpoint actually exposes. Pick one. 2. **Name your agent.** Short lowercase slug (e.g. `product-director`). Pick a persona preset — **Generic Assistant** is the safe default; **Research** / **Coding** / **Blank** are the alternatives — and click **Load preset into SOUL.md**. Edit the loaded text if you want to make it specific to your agent. -3. **Tools & middleware.** All nine starter tools (`current_time`, `calculator`, `web_search`, `fetch_url`, plus the memory tools `memory_ingest` / `memory_recall` / `memory_list` / `memory_stats` / `daily_log`) are enabled by default. Leave **Audit**, **Memory**, and **Knowledge** middleware on — the template ships a working sqlite + FTS5 store under `/sandbox/knowledge/agent.db` (falls back to `~/.protoagent/knowledge/agent.db` outside Docker). +3. **Tools & middleware.** All twelve starter tools are enabled by default — four keyless general (`current_time`, `calculator`, `web_search`, `fetch_url`), five memory (`memory_ingest`, `memory_recall`, `memory_list`, `memory_stats`, `daily_log`), and three scheduler (`schedule_task`, `list_schedules`, `cancel_schedule`). Leave **Audit**, **Memory**, **Knowledge**, and **Scheduler** middleware on — the template ships a working sqlite + FTS5 store under `/sandbox/knowledge/agent.db` and a sqlite-backed scheduler under `/sandbox/scheduler//jobs.db`, both with `~/.protoagent/...` fallbacks outside Docker. 4. **Optional — you, security, autostart.** Your name makes the agent address you directly. A2A auth token blank for local dev, set it before you expose the port. "Launch this agent automatically on login" installs a macOS LaunchAgent so the server is up after every reboot without remembering to `python server.py`. Hit **Launch agent**. The wizard closes, the chat UI appears, and the Configuration drawer on the right is now populated with your choices. diff --git a/docs/tutorials/first-tool.md b/docs/tutorials/first-tool.md index 056a8e4..af3b767 100644 --- a/docs/tutorials/first-tool.md +++ b/docs/tutorials/first-tool.md @@ -108,5 +108,5 @@ The template runs tests via `pytest` with `pytest-asyncio` in auto mode — no e ## Where to go next - [Add a custom skill](/guides/add-a-skill) — advertise new capabilities on the agent card so A2A callers can find them -- [Starter tools reference](/reference/starter-tools) — the shapes of the five tools that ship +- [Starter tools reference](/reference/starter-tools) — the shapes of all twelve tools that ship by default - [Configure subagents](/guides/subagents) — add specialized delegates beyond the placeholder `worker`