# Agent Tasks, ToolRuntime, and Resource Locking

**Feature status:** Available as of `plugin-multi-turn-multi-process` branch.

---

## Overview

Furoshiki can now run **long-horizon agent tasks** — autonomous, multi-round tool sequences that persist across Brain ticks, survive restarts, and operate concurrently without resource collisions.

Three new subsystems enable this:

| Subsystem | Module | Purpose |
|---|---|---|
| **ToolRuntime** | `scripts/tool_runtime.py` | Policy seam for all tool-calling loops: iteration budgets, dedup, cooldowns, urgency routing |
| **Resource Registry** | `scripts/resource_registry.py` | Canonical vocabulary of named resources; plugins declare which resources they touch |
| **Agent Tasks** | `scripts/agent_tasks.py`, `agent_locks.py`, `agent_runner.py` | Persistent resumable tasks with SQLite-backed locking |

---

## ToolRuntime

Every call to `call_llm_chat_with_tool_loop` can now receive an optional `runtime: ToolRuntime` parameter. When provided:

- `max_tool_rounds` is overridden by `runtime.iteration_budget()`.
- Each tool call is checked via `runtime.should_call()` — blocked calls receive a synthetic error result instead of executing.
- `runtime.record_call()` writes a `tool_call` row to `system_events`.
- High/urgent results from `runtime.route_result()` are enqueued into `mind_queue` for voice_dispatcher to surface.

**Config:** `$FUROSHIKI_DATA/config/tool_runtime.json` (template: `config/defaults/tool_runtime.json`).

**Operator tunables:** `agent_runner` section in `memory/operator_settings.json`:

| Key | Env | Default | Range | Meaning |
|---|---|---|---|---|
| `max_concurrent` | `FUROSHIKI_AGENT_RUNNER_MAX_CONCURRENT` | 3 | 1–20 | Max simultaneously running tasks |
| `default_budget` | `FUROSHIKI_AGENT_TASK_DEFAULT_BUDGET` | 80 | 10–500 | Default total iteration budget per task |
| `rounds_per_tick` | `FUROSHIKI_AGENT_RUNNER_ROUNDS_PER_TICK` | 8 | 1–50 | Max model rounds per Brain tick |

---

## Resource Registry

Plugins declare resources in their `PLUGIN.md` callable hook frontmatter:

```yaml
hooks:
  - type: callable
    name: gmail_trash
    resources:
      - name: gmail
        mode: write
    requires_approval: true
```

**Core resources** are declared in `config/defaults/resource_registry.json`. Plugin-specific resources use the namespace `plugin:<plugin_id>:<resource_name>` — any other namespace is rejected at load time.

When a plugin has an invalid resource declaration, the **entire plugin is rejected** (no partial loading).

**`get_callable_manifest()`** now returns two new fields on every entry:
- `resources: list[{name, mode}]` — normalized resource declarations (empty list if none)
- `requires_approval: bool` — whether this tool pauses for user approval

---

## Agent Tasks

### Lifecycle

```
queued → running → [awaiting_approval →] running → done
                                                  → failed
                                                  → cancelled
```

### Data model (SQLite — `longmemory.sqlite`)

- **`agent_tasks`** — one row per task: goal, status, source, handle, resource locks, allowed tools, iteration count/budget, last error, approval request.
- **`agent_task_steps`** — step history: `tool_call`, `tool_result`, `model_text`, `approval`, `system`.
- **`agent_resource_locks`** — currently held locks; PRIMARY KEY on `resource` ensures mutual exclusion.

### Resource locking

`agent_locks.try_acquire_locks()` uses `BEGIN IMMEDIATE` transactions so concurrent processes cannot both acquire the same resource. Lock acquisition is all-or-nothing — if any requested resource is held, nothing is acquired.

Locks are released on task completion, failure, cancellation, or via `furoshiki agents cancel <id>`.

### request_approval meta-tool

When a callable skill is marked `requires_approval: true`, the agent runner injects the `request_approval` built-in tool. Calling it:
1. Stores the approval request on the task row.
2. Transitions the task to `awaiting_approval`.
3. Creates a `mind_queue` row (`kind=agent_approval`) so voice_dispatcher notifies the user.

**Approval replies** in Telegram (yes / no / cancel) are handled before micro-brain classification in `telegram_listener.py`.

### Scheduling

`agent_runner.py` runs every minute via the Brain scheduler (see `memory/schedules.json`). It is a standalone script — never wired into `soul_engine.run()` — to avoid blocking emotion/event processing.

---

## Operator Commands

```
furoshiki agents                   # list active tasks
furoshiki agents list --status all # include terminal tasks
furoshiki agents view <id>         # task detail + locks
furoshiki agents cancel <id>       # cancel + release locks
furoshiki agents locks             # show all held resource locks
furoshiki agents --json            # machine-readable output
```

**Telegram shortcuts:**
- Any message routed as a long-horizon task by micro-brain creates an agent task.
- Reply `yes`/`no` to approve/deny a pending approval request.

---

## Dashboard

**Work & extensions → Agent tasks** shows:
- All tasks with status, handle, goal, budget consumed, source, and locks held.
- Pending approval details in-row.
- Resource locks table at the bottom.

**API endpoints** (require admin bearer):
- `GET /dashboard/api/agents?status=active|all`
- `GET /dashboard/api/agents/{id}`
- `GET /dashboard/api/agents/{id}/steps`
- `POST /dashboard/api/agents/{id}/cancel`
- `GET /dashboard/api/agents-locks`

---

## Safety invariants

| Invariant | Mechanism |
|---|---|
| No resource collision | `BEGIN IMMEDIATE` lock acquisition; all-or-nothing |
| No runaway loops | `iteration_budget` enforced by ToolRuntime in every tool loop |
| No duplicate tool calls | `dedup_window_seconds` in ToolRuntime; SHA1 args hash |
| Irreversible actions gated | `requires_approval: true` + `request_approval` meta-tool |
| Task state survives crash | All steps persisted to SQLite before returning |
| `soul_engine` unaffected | `agent_runner.py` is an independent Brain-scheduled subprocess |
