Why this matters
This page introduces the shared task language that the later pages reuse. It is the quickest way to see the common shape before the concrete task kinds split apart.
Task.ts is the common contract every task state starts from. It is not one implementation file for one task kind. It is the shared interface that all the real task implementations inherit from, and the later chapters fill in the specific behavior on top of it.
The shared task vocabulary
The first half of Task.ts names the shared pieces that most task flows depend
on, plus one shell-specific input type that shows up later in this subtree:
TaskTypesays what kind of task this is.TaskStatussays where it is in its lifecycle.isTerminalTaskStatus()separates finished tasks from tasks that can still move.TaskHandle,TaskContext, andTaskStateBasedescribe the shared runtime shape.LocalShellSpawnInputis not universal. It records the inputs that start a local shell task, so it belongs here as an early example of a task-kind- specific helper type.
The isTerminalTaskStatus() helper matters because the rest of the system uses
it to avoid updating tasks that are already done. Use this to guard against
injecting messages into dead teammates, evicting finished tasks from AppState,
and orphan-cleanup paths.
export type TaskType =
| 'local_bash'
| 'local_agent'
| 'remote_agent'
| 'in_process_teammate'
| 'local_workflow'
| 'monitor_mcp'
| 'dream'
export type TaskStatus =
| 'pending'
| 'running'
| 'completed'
| 'failed'
| 'killed'
/**
* True when a task is in a terminal state and will not transition further.
* Used to guard against injecting messages into dead teammates, evicting
* finished tasks from AppState, and orphan-cleanup paths.
*/
export function isTerminalTaskStatus(status: TaskStatus): boolean {
return status === 'completed' || status === 'failed' || status === 'killed'
}
export type TaskHandle = {
taskId: string
cleanup?: () => void
}
export type SetAppState = (f: (prev: AppState) => AppState) => void
export type TaskContext = {
abortController: AbortController
getAppState: () => AppState
setAppState: SetAppState
}
// Base fields shared by all task states
export type TaskStateBase = {
id: string
type: TaskType
status: TaskStatus
description: string
toolUseId?: string
startTime: number
endTime?: number
totalPausedMs?: number
outputFile: string
outputOffset: number
notified: boolean
}
export type LocalShellSpawnInput = {
command: string
description: string
timeout?: number
toolUseId?: string
agentId?: AgentId
/** UI display variant: description-as-label, dialog title, status bar pill. */
kind?: 'bash' | 'monitor'
}
The rest of the file shows how task IDs are made and how a new task state starts with the shared base values.
Task IDs And New Tasks
generateTaskId() gives each task a short prefix plus a random suffix. The
prefix helps humans tell task kinds apart when they are looking at logs or file
paths.
createTaskStateBase() then fills in the shared starting fields for a new task:
pending status, the task description, the output file, and the initial
notification flag.
// What getTaskByType dispatches for: kill. spawn/render were never
// called polymorphically (removed in #22546). All six kill implementations
// use only setAppState — getAppState/abortController were dead weight.
export type Task = {
name: string
type: TaskType
kill(taskId: string, setAppState: SetAppState): Promise<void>
}
// Task ID prefixes
const TASK_ID_PREFIXES: Record<string, string> = {
local_bash: 'b', // Keep as 'b' for backward compatibility
local_agent: 'a',
remote_agent: 'r',
in_process_teammate: 't',
local_workflow: 'w',
monitor_mcp: 'm',
dream: 'd',
}
// Get task ID prefix
function getTaskIdPrefix(type: TaskType): string {
return TASK_ID_PREFIXES[type] ?? 'x'
}
// Case-insensitive-safe alphabet (digits + lowercase) for task IDs.
// 36^8 ≈ 2.8 trillion combinations, sufficient to resist brute-force symlink attacks.
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
export function generateTaskId(type: TaskType): string {
const prefix = getTaskIdPrefix(type)
const bytes = randomBytes(8)
let id = prefix
for (let i = 0; i < 8; i++) {
id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
}
return id
}
export function createTaskStateBase(
id: string,
type: TaskType,
description: string,
toolUseId?: string,
): TaskStateBase {
return {
id,
type,
status: 'pending',
description,
toolUseId,
startTime: Date.now(),
outputFile: getTaskOutputPath(id),
outputOffset: 0,
notified: false,
}
}