Agent Architecture¶
How a local AI agent picks up tasks from Taskbase, works them, and reports results back. This document covers the task lifecycle, authentication model, API contract, and runtime setup.
See ADR 0003 for the decision record that established the local MCP server approach.
Task Lifecycle¶
A task moves through the following states, modelled using done (boolean) and group_id (kanban column):
┌─────────┐ agent claims ┌─────────────┐ agent works ┌──────────┐
│ Backlog │ ────────────────► │ In Progress │ ───────────────► │ Done │
│ (queued) │ │ (running) │ │ │
└──────────┘ └──────┬──────┘ └──────────┘
│ context full / blocked
▼
┌─────────────┐
│ Blocked │
│ (paused w/ │
│ checkpoint)│
└─────────────┘
| Kanban Column | Meaning for agent |
|---|---|
| Backlog | Task is queued and ready for pickup |
| In Progress | Agent has claimed the task and is actively working it |
| Done | Agent has completed and closed the task (done = true) |
| Blocked | Agent has checkpointed; context was exhausted or work is blocked |
| Review | Agent has delivered work; waiting for human review |
State transitions¶
Claiming a task (Backlog → In Progress)
The agent polls GET /organizations/{id}/tasks filtered to group Backlog, picks the highest-priority task, and atomically transitions it:
PUT /tasks/{id}
{
"group_id": <in-progress group id>,
"assignee_type": 2, // agent
"assignee": "<agent-id>",
...all other fields preserved...
}
To prevent two agent instances from claiming the same task, the agent reads the full task body first, then immediately PUTs with assignee_type=2. In the current single-agent setup this is sufficient; a future version can add optimistic locking via a version column.
Completing a task (In Progress → Done)
PUT /tasks/{id}
{
"done": true,
"group_id": <done group id>,
...description updated with completion summary...
}
Checkpointing (In Progress → Blocked)
When the context budget is running low or the task is externally blocked:
PUT /tasks/{id}
{
"group_id": <blocked group id>,
"description": "<original description>\n\n## Checkpoint\n<state summary>"
}
Intermediate progress
The agent appends progress notes to the task description using the Markdown ## Progress section convention. There is no separate log table in v1; the description field is the audit trail.
Authentication Model¶
Agent ↔ Taskbase API¶
The agent authenticates to the Taskbase REST API using a long-lived API key:
- Key is generated once via
POST /api/api-keys(or the Taskbase UI) - Stored at
~/.config/taskbase-agent/config.yamlon the agent machine (or as a Cowork plugin config) - Sent as
X-API-Key: <key>header on every request - Never exposed in agent context — the MCP server holds the key; Claude never sees it
Agent ↔ External services¶
When the agent needs credentials for external services (Gitea, Kubernetes, etc.) while working a task:
| Service | How credentials are stored | How agent accesses them |
|---|---|---|
| Gitea | GITEA_TOKEN env var in task description (injected by operator) OR OpenBao secret |
Agent reads from task description or calls OpenBao via Taskbase proxy |
| Kubernetes | kubeconfig at ~/.kube/config on agent machine |
kubectl / client-go reads automatically |
| Anthropic API | ANTHROPIC_API_KEY env var on agent machine |
Set in Cowork session environment |
Principle: credentials for external services are never stored in Taskbase tasks. If a task requires credentials, the operator either injects them as environment variables on the agent machine or references a named OpenBao secret that the agent can retrieve via the platform secrets API.
Gitea PAT for Taskbase sync¶
The Taskbase API itself uses a Gitea PAT for the server-side Gitea sync feature (not the agent). This PAT is:
- Stored in OpenBao at
secret/tools/taskbase/gitea-token - Injected into the API deployment as
GITEA_TOKENenv var via External Secrets Operator - Used by the Taskbase API server (not the agent) to create/update Gitea issues
API Contract (Agent ↔ Taskbase)¶
These are the endpoints the agent (via the local MCP server) calls to manage tasks:
| Operation | Endpoint | Notes |
|---|---|---|
| List queued tasks | GET /organizations/{orgId}/tasks |
Filter by group (Backlog) client-side |
| Get task detail | GET /tasks/{id} |
Full description, metadata |
| Claim task | PUT /tasks/{id} |
Set group_id (In Progress) + assignee_type=2 |
| Update progress | PUT /tasks/{id} |
Append to description |
| Complete task | PUT /tasks/{id} |
Set done=true + group_id (Done) |
| Checkpoint task | PUT /tasks/{id} |
Set group_id (Blocked), add checkpoint to description |
| List orgs | GET /organizations |
To discover org IDs at startup |
| List projects | GET /organizations/{orgId}/projects |
To enumerate work queues |
| List groups | GET /projects/{id}/groups |
To resolve group name → ID |
All requests use X-API-Key: <key> header. Base URL: https://taskbase.skatzi.com/api.
Runtime (Mac Mini)¶
The local MCP server runs on the Mac Mini alongside Cowork as a persistent background process. Decision: launchd service (native macOS service manager).
Why launchd¶
- Native to macOS — no additional tooling needed
- Starts automatically on boot
- Restarts on crash
- Logs to the system journal (viewable with
log show) - Consistent with how other background services run on the Mac Mini
Alternative considered: manual process / tmux session¶
Rejected because it requires the operator to start the server manually after every reboot, which breaks the "zero manual steps" requirement from ADR 0003.
Config location¶
~/.config/taskbase-agent/
├── config.yaml # API base URL, key, token budget settings
└── taskbase-agent.plist # launchd service definition (symlinked to ~/Library/LaunchAgents/)
config.yaml schema:
api_base_url: https://taskbase.skatzi.com/api
api_key: <generated API key>
default_token_budget: 100000
budget_warning_threshold: 0.15 # pause when 15% budget remains
agent_id: mac-mini-agent
MCP server registration¶
The server is registered in ~/.config/claude/claude_desktop_config.json (or the Cowork equivalent) so it is available as a plugin in every Cowork session:
{
"mcpServers": {
"taskbase": {
"command": "/usr/local/bin/taskbase-agent-mcp",
"args": ["--config", "/Users/<user>/.config/taskbase-agent/config.yaml"]
}
}
}
Failure Modes and Observability¶
| Failure | Detection | Recovery |
|---|---|---|
| Agent crashes mid-task | Task stays In Progress with no updates for > 30 min |
Operator manually moves to Blocked or re-queues |
| Taskbase API unreachable | MCP get_next_task returns error |
Agent session ends; launchd restarts MCP server; operator retries |
| Context exhausted without checkpoint | Task stays In Progress |
Operator inspects description for partial work, manually checkpoints |
| Duplicate task claim (future multi-agent) | Two agents PUT same task | v1: not a concern (single agent). v2: add optimistic locking via version column |
| Gitea sync failure | Task update succeeds but Gitea issue not created | Taskbase API logs error; issue can be manually created; async retry not in v1 |
Logging¶
- Taskbase API logs to stdout → captured by the Kubernetes pod logs → visible in Grafana (Loki)
- MCP server logs to
~/Library/Logs/taskbase-agent.log - Agent session transcript available in the Cowork UI
Alerts (v1 — manual)¶
In v1 there are no automated alerts. The operator checks the Taskbase UI periodically. Automated alerting (e.g. "task stuck in In Progress for > N minutes") is a future addition.