# Tasks

> Deferred and asynchronous tool executions in adf_tasks: creation paths, statuses, querying, and resolution

Source: https://github.com/christianbalevski/adf/blob/v0.7.4/docs/guides/tasks.md (adf v0.7.4)

Tasks track deferred and asynchronous tool executions. When a tool call requires approval (HIL via `restricted`) or is executed asynchronously, the runtime creates a task entry in `adf_tasks` that records the tool name, arguments, status, and eventual result.

## When Tasks Are Created

Tasks are created in two scenarios:

### 1. Restricted Tools from the LLM Loop (HIL)

Tools configured with `enabled: true` and `restricted: true` create a task with `pending_approval` status and `requires_authorization: true` when called from the LLM loop. The agent's turn blocks until the task is resolved (approved or denied).

The task can be resolved by:
- **UI approval dialog / HTTP** — the owner clicks approve/deny in the Studio UI, or a client calls the daemon endpoint `POST /agents/:id/tasks/:taskId/resolve`
- **`on_task_create` trigger lambda** — dispatches to an external approval system (Telegram, multi-agent vote, etc.) which calls `task_resolve`
- **`task_resolve` from authorized code** — any authorized lambda can approve/deny

The resolve endpoint returns **404** for an unknown task id and **409** when the task is not in a resolvable status — only `pending` and `pending_approval` rows can be resolved. A task already swept to a terminal status by orphan reconciliation therefore returns 409.

When a **blocking** (synchronous) HIL task is **denied**, `on_task_complete` is deliberately **not** fired — the agent already receives the rejection in-band as the tool-call error result, so a trigger would be redundant. Denials of **async** (`_async: true`) HIL tasks *do* fire `on_task_complete`, since the agent has no inline context for those.

```json
{
  "tools": [
    { "name": "fs_write", "enabled": true, "restricted": true }
  ]
}
```

`restricted` is owner-set only — an agent's `sys_update_config` call touching it is hard-denied (no HIL prompt). See [Authorized Code](https://agentdocumentformat.org/guides/authorized-code) for the security model.

**Async HIL:** If the agent calls a restricted tool with `_async: true`, the task is created but the agent continues without waiting. The task reference is returned immediately:

```json
{ "task_id": "task_abc123", "status": "pending_approval", "tool": "fs_write" }
```

### 2. Async Execution (`_async: true`)

Any tool call can be made asynchronous by including `_async: true` in the input. The runtime strips the flag, creates a task in `running` status, and executes the tool in the background:

```json
{ "task_id": "task_def456", "status": "running", "tool": "sys_code" }
```

The LLM can continue its turn without waiting. The task updates to `completed` or `failed` when execution finishes.

## Task Statuses

| Status | Description |
|--------|-------------|
| `pending` | Created, not yet executing |
| `pending_approval` | Awaiting human/authorized approval (HIL) |
| `running` | Currently executing |
| `completed` | Finished successfully |
| `failed` | Finished with an error |
| `denied` | Approval rejected |
| `cancelled` | Cancelled before completion |

Terminal statuses (`completed`, `failed`, `denied`, `cancelled`) record a `completed_at` timestamp.

## Orphaned Task Reconciliation (on load)

A crash or hard shutdown can leave `adf_tasks` rows stranded in a non-terminal state. On load — before any turn of the new session runs — the executor sweeps these orphans (every row it sees predates the load, so nothing legitimately in flight is affected):

| Orphaned row | Swept to | Why |
|--------------|----------|-----|
| `running` | `failed` (error: "outcome unknown") | The tool may have produced side effects before the process died; the outcome cannot be known. |
| `pending_approval`, **executor-managed** | `cancelled` | These are the executor's own blocking HIL approvals — nobody is left waiting on them. Not `denied`: no human ever decided. |
| `pending_approval`, **not** executor-managed | *left open* | Owned by lambdas/UI, which can still resolve them after restart. |

Each swept executor-managed approval emits `hil.resolved { orphaned: true, approved: false }` (closing the one-resolve-per-request guarantee for a request whose emitter died). If any rows were swept, the executor emits a single `loop.recovered { reason: 'orphaned_tasks', running, awaiting_approval }`. Reconciliation is diagnostic — a failure never blocks agent load.

## Task Schema

The `adf_tasks` table:

| Column | Type | Description |
|--------|------|-------------|
| `id` | TEXT PRIMARY KEY | Unique identifier (`task_` + nanoid) |
| `tool` | TEXT | Tool name (e.g., `fs_write`) |
| `args` | TEXT | JSON-stringified tool arguments |
| `status` | TEXT | Current status (see above) |
| `result` | TEXT | JSON result on success, NULL otherwise |
| `error` | TEXT | Error message on failure, NULL otherwise |
| `created_at` | INTEGER | Unix timestamp (ms) |
| `completed_at` | INTEGER | Unix timestamp (ms), set on terminal status |
| `origin` | TEXT | Source — `hil:AgentName:id` for HIL, `agent:AgentName:id` for async |
| `requires_authorization` | INTEGER | `1` if only authorized code can approve/deny this task |
| `executor_managed` | INTEGER | `1` if the executor is waiting to execute the tool after approval |

## Task-Level Authorization

HIL tasks are created with `requires_authorization: true`, meaning only authorized code (or the UI dialog, which is owner-authorized) can approve or deny them. This prevents the agent from self-approving its own gated tool calls.

For non-HIL tasks, `requires_authorization` can be set via `task_resolve`:

```javascript
await adf.task_resolve({
  task_id: taskId,
  action: "pending_approval",
  requires_authorization: true
});
```

Once set, the flag cannot be unset.

## Code Execution and Restricted Tools

Restricted tools (`restricted: true`) can only be called freely from authorized code. Unauthorized code is always blocked.

| Code Context | Restricted tool |
|---|---|
| `sys_code` (always unauthorized) | Blocked |
| `sys_lambda` from loop → unauthorized target | Blocked |
| `sys_lambda` from loop → authorized target | **HIL** — approved → Allowed |
| `sys_lambda` from authorized file (authorized) | Allowed |
| Trigger/timer lambda from authorized file | Allowed |

When the LLM calls `sys_lambda` targeting an authorized file, the runtime triggers a HIL approval prompt. If approved, the lambda runs with authorization and can call restricted tools. This is the same approval mechanism used for restricted tool calls from the loop. Authorized lambdas called from code or triggers run without prompting.

Hiding a tool from the LLM does not make it inaccessible to code. Set `visible: false` to keep an enabled tool callable from code (and lambdas) while removing it from the LLM's tool schema. A tool with `enabled: false, restricted: true` can still be called from authorized code — it is just hidden from the LLM loop.

## Querying Tasks

Use `db_query` to inspect tasks. Note that the `on_task_create` trigger below is disabled by default — enable via `triggers.on_task_create.enabled`.

```sql
-- Recent tasks
SELECT * FROM adf_tasks ORDER BY created_at DESC LIMIT 20

-- Pending approval tasks
SELECT * FROM adf_tasks WHERE status = 'pending_approval'

-- Failed tasks for a specific tool
SELECT * FROM adf_tasks WHERE tool = 'fs_write' AND status = 'failed'
```

## Task Triggers

### `on_task_create`

Fires when a task is created. This is the hook for external approval routing — the lambda receives the full task details and can dispatch approval requests.

```json
{
  "on_task_create": {
    "enabled": true,
    "targets": [{
      "scope": "system",
      "lambda": "lib/hil/dispatcher.ts:onTaskCreate",
      "filter": { "tools": ["*"] }
    }]
  }
}
```

Example lambda:

```javascript
export async function onTaskCreate(event) {
  const { task } = event.data;
  if (!task.requires_authorization) return;

  await adf.msg_send({
    recipient: "telegram:123456789",
    content: `Approval needed: ${task.tool}\nArgs: ${task.args}`,
    subject: `task:${task.id}`
  });
}
```

### `on_task_complete`

Fires when a task reaches a terminal status (`completed`, `failed`, `denied`, `cancelled`). Filter by tool name and/or status:

```json
{
  "on_task_complete": {
    "enabled": true,
    "targets": [{
      "scope": "agent",
      "filter": { "tools": ["fs_write", "msg_send"], "status": "completed" }
    }]
  }
}
```

See [Triggers](https://agentdocumentformat.org/guides/triggers) for the full trigger system.

## Tool Side Effects Through Task Resolution

When `task_resolve` approves a task, the tool executes and its side effects are propagated. For HIL tasks (executor-managed), the executor runs the tool in its own context — preserving `endTurn` handling, file diffs, and state transitions. For deferred tasks, `task_resolve` executes the tool in the call handler context.

## UI

The **Tasks** tab in the [Bottom Panel](https://agentdocumentformat.org/studio/settings#bottom-panel-logs--tasks) displays all tasks with status filtering, expandable argument/result details, auto-refresh, and an AUTH badge for tasks requiring authorized code.
