Task Dispatch¶
How It Works¶
- Create a task with an agent image, instructions, and optional skills/configs
- Call
dispatch_task(task)-- assembles brief, selects dispatcher, POSTs to it - Dispatcher queues the task, consumer dequeues and spawns the container
- Agent boots, downloads brief, checks in, does work, checks back
- Dispatcher fires callback to Controller with result
Brief Assembly¶
The brief is a JSON package containing everything the agent needs:
{
"task_id": "uuid",
"task_name": "Code review",
"skills": [{"slug": "code-review", "instructions": "..."}],
"configs": [{"slug": "style-guide", "content": "..."}],
"instructions": "Review the PR for quality.",
"context": "Repository: org/repo, PR #42"
}
The Controller assembles the brief from five sources:
| Component | Source | Example |
|---|---|---|
| Skills | Skills Registry | GitHub tool, CRM connector, code reviewer |
| Config | Config Registry | Response style, model settings, standards |
| Instructions | Per-task (inline) | "Review this PR against our style guide" |
| Secrets | Secrets store | GitHub PAT, API keys (injected as env vars) |
| Context | Per-task (inline) | Codebase overview, domain rules |
The assembled brief is uploaded to object storage. The agent receives only a key at boot -- downloads, verifies the content hash, and loads.
Dispatch Modes¶
Controller POSTs to the Dispatcher's /tasks endpoint directly.
Dependency Resolution¶
Before dispatch, the Controller resolves infrastructure dependencies:
- Tier deps -- derive base dependency set from the agent tier
- Definition deps -- merge any additional deps from the
AgentDefinition - Service deps -- look up each
InfraServicerecord. If missing or unhealthy, reject dispatch - Port deps -- add required port mappings to the runtime spec
| Dep type | Env vars injected |
|---|---|
scuttlebot-irc |
SCUTTLEBOT_URL, SCUTTLEBOT_TOKEN, SCUTTLEBOT_CHANNEL, SCUTTLEBOT_IRC_ADDR |
navegador-redis |
NAVEGADOR_REDIS_URL |
Early failure
If a required service is missing or unhealthy, dispatch fails loud at dispatch time -- not at agent boot.
Task Request Schema¶
What the Controller sends to POST /tasks:
{
"task_id": "uuid",
"agent_image": "ghcr.io/conflicthq/kohakku-agents:terminal-claude-latest",
"runtime_target": {"type": "k8s", "namespace": "agents"},
"resources": {"cpu": "1", "memory": "2Gi"},
"env": {"TASK_ID": "...", "BRIEF_KEY": "s3://..."},
"ports": [{"container_port": 6080, "protocol": "tcp"}],
"brief_key": "briefs/guid/hash.json",
"callback_url": "https://controller/api/v1/tasks/guid/callback/",
"callback_token": "signed-bearer-token",
"timeout_seconds": 3600
}
Task State Machine¶
QUEUED
| (container booting)
PROVISIONING
| (agent checks in)
RUNNING
| (exit 0) | (exit != 0) | (deadline exceeded)
COMPLETED FAILED TIMED_OUT
The Dispatcher owns this state in Redis. The agent moves it from QUEUED to RUNNING (at check-in) and RUNNING to terminal (at check-back).
Retry Policy¶
dispatch_with_retry() wraps dispatch with exponential backoff:
Retryable:
- Connection errors, timeouts, 5xx responses
- "No active dispatcher" (temporary -- dispatchers may be restarting)
Non-retryable:
BriefAssemblyError-- skill or config resolution failedImageResolutionError-- agent image not foundValueError-- invalid task parameters- 4xx responses
Default: 3 attempts, exponential backoff. Configurable at the workflow level via Temporal retry policy.
Task Deduplication¶
Tasks are keyed by a hash of their config. The Dispatcher will not re-run a task that is active or completed unless forced. Check-in is idempotent.
Monitoring¶
Two ways to get task state:
| Method | How |
|---|---|
| Callback (push) | Dispatcher POSTs to the Controller's callback URL on completion/failure |
| Poll (pull) | GET /tasks/{task_id} returns current state at any time |
Telemetry tracked per task: status, started_at, completed_at, duration_seconds, exit_code, tokens_used, log_stream, error, result_key.