Task.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { randomBytes } from 'crypto'
  2. import type { AppState } from './state/AppState.js'
  3. import type { AgentId } from './types/ids.js'
  4. import { getTaskOutputPath } from './utils/task/diskOutput.js'
  5. export type TaskType =
  6. | 'local_bash'
  7. | 'local_agent'
  8. | 'remote_agent'
  9. | 'in_process_teammate'
  10. | 'local_workflow'
  11. | 'monitor_mcp'
  12. | 'dream'
  13. export type TaskStatus =
  14. | 'pending'
  15. | 'running'
  16. | 'completed'
  17. | 'failed'
  18. | 'killed'
  19. /**
  20. * True when a task is in a terminal state and will not transition further.
  21. * Used to guard against injecting messages into dead teammates, evicting
  22. * finished tasks from AppState, and orphan-cleanup paths.
  23. */
  24. export function isTerminalTaskStatus(status: TaskStatus): boolean {
  25. return status === 'completed' || status === 'failed' || status === 'killed'
  26. }
  27. export type TaskHandle = {
  28. taskId: string
  29. cleanup?: () => void
  30. }
  31. export type SetAppState = (f: (prev: AppState) => AppState) => void
  32. export type TaskContext = {
  33. abortController: AbortController
  34. getAppState: () => AppState
  35. setAppState: SetAppState
  36. }
  37. // Base fields shared by all task states
  38. export type TaskStateBase = {
  39. id: string
  40. type: TaskType
  41. status: TaskStatus
  42. description: string
  43. toolUseId?: string
  44. startTime: number
  45. endTime?: number
  46. totalPausedMs?: number
  47. outputFile: string
  48. outputOffset: number
  49. notified: boolean
  50. }
  51. export type LocalShellSpawnInput = {
  52. command: string
  53. description: string
  54. timeout?: number
  55. toolUseId?: string
  56. agentId?: AgentId
  57. /** UI display variant: description-as-label, dialog title, status bar pill. */
  58. kind?: 'bash' | 'monitor'
  59. }
  60. // What getTaskByType dispatches for: kill. spawn/render were never
  61. // called polymorphically (removed in #22546). All six kill implementations
  62. // use only setAppState — getAppState/abortController were dead weight.
  63. export type Task = {
  64. name: string
  65. type: TaskType
  66. kill(taskId: string, setAppState: SetAppState): Promise<void>
  67. }
  68. // Task ID prefixes
  69. const TASK_ID_PREFIXES: Record<string, string> = {
  70. local_bash: 'b', // Keep as 'b' for backward compatibility
  71. local_agent: 'a',
  72. remote_agent: 'r',
  73. in_process_teammate: 't',
  74. local_workflow: 'w',
  75. monitor_mcp: 'm',
  76. dream: 'd',
  77. }
  78. // Get task ID prefix
  79. function getTaskIdPrefix(type: TaskType): string {
  80. return TASK_ID_PREFIXES[type] ?? 'x'
  81. }
  82. // Case-insensitive-safe alphabet (digits + lowercase) for task IDs.
  83. // 36^8 ≈ 2.8 trillion combinations, sufficient to resist brute-force symlink attacks.
  84. const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
  85. export function generateTaskId(type: TaskType): string {
  86. const prefix = getTaskIdPrefix(type)
  87. const bytes = randomBytes(8)
  88. let id = prefix
  89. for (let i = 0; i < 8; i++) {
  90. id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
  91. }
  92. return id
  93. }
  94. export function createTaskStateBase(
  95. id: string,
  96. type: TaskType,
  97. description: string,
  98. toolUseId?: string,
  99. ): TaskStateBase {
  100. return {
  101. id,
  102. type,
  103. status: 'pending',
  104. description,
  105. toolUseId,
  106. startTime: Date.now(),
  107. outputFile: getTaskOutputPath(id),
  108. outputOffset: 0,
  109. notified: false,
  110. }
  111. }