stagent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +399 -0
  3. package/components.json +21 -0
  4. package/dist/cli.js +171 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.mjs +15 -0
  7. package/package.json +114 -0
  8. package/postcss.config.mjs +8 -0
  9. package/public/icon-512.png +0 -0
  10. package/public/icon.svg +13 -0
  11. package/public/readme/home-workspace.png +0 -0
  12. package/public/readme/inbox-approvals.png +0 -0
  13. package/public/readme/workflow-blueprints.png +0 -0
  14. package/public/stagent-s-128.png +0 -0
  15. package/public/stagent-s-64.png +0 -0
  16. package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
  17. package/src/app/api/blueprints/[id]/route.ts +39 -0
  18. package/src/app/api/blueprints/import/route.ts +68 -0
  19. package/src/app/api/blueprints/route.ts +29 -0
  20. package/src/app/api/command-palette/recent/route.ts +31 -0
  21. package/src/app/api/data/clear/route.ts +22 -0
  22. package/src/app/api/data/seed/route.ts +22 -0
  23. package/src/app/api/documents/[id]/file/route.ts +44 -0
  24. package/src/app/api/documents/[id]/route.ts +123 -0
  25. package/src/app/api/documents/route.ts +59 -0
  26. package/src/app/api/logs/stream/route.ts +101 -0
  27. package/src/app/api/notifications/[id]/route.ts +36 -0
  28. package/src/app/api/notifications/mark-all-read/route.ts +13 -0
  29. package/src/app/api/notifications/pending-approvals/route.ts +10 -0
  30. package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
  31. package/src/app/api/notifications/route.ts +34 -0
  32. package/src/app/api/permissions/route.ts +46 -0
  33. package/src/app/api/profiles/[id]/route.ts +79 -0
  34. package/src/app/api/profiles/[id]/test/route.ts +42 -0
  35. package/src/app/api/profiles/import/route.ts +108 -0
  36. package/src/app/api/profiles/route.ts +50 -0
  37. package/src/app/api/projects/[id]/route.ts +72 -0
  38. package/src/app/api/projects/route.ts +53 -0
  39. package/src/app/api/schedules/[id]/route.ts +185 -0
  40. package/src/app/api/schedules/route.ts +117 -0
  41. package/src/app/api/settings/budgets/route.ts +24 -0
  42. package/src/app/api/settings/openai/route.ts +24 -0
  43. package/src/app/api/settings/route.ts +21 -0
  44. package/src/app/api/settings/test/route.ts +26 -0
  45. package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +90 -0
  47. package/src/app/api/tasks/[id]/logs/route.ts +95 -0
  48. package/src/app/api/tasks/[id]/output/route.ts +47 -0
  49. package/src/app/api/tasks/[id]/respond/route.ts +64 -0
  50. package/src/app/api/tasks/[id]/resume/route.ts +76 -0
  51. package/src/app/api/tasks/[id]/route.ts +77 -0
  52. package/src/app/api/tasks/assist/route.ts +35 -0
  53. package/src/app/api/tasks/route.ts +82 -0
  54. package/src/app/api/uploads/[id]/route.ts +81 -0
  55. package/src/app/api/uploads/cleanup/route.ts +7 -0
  56. package/src/app/api/uploads/route.ts +66 -0
  57. package/src/app/api/workflows/[id]/execute/route.ts +82 -0
  58. package/src/app/api/workflows/[id]/route.ts +133 -0
  59. package/src/app/api/workflows/[id]/status/route.ts +54 -0
  60. package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
  61. package/src/app/api/workflows/route.ts +61 -0
  62. package/src/app/apple-icon.tsx +31 -0
  63. package/src/app/costs/page.tsx +256 -0
  64. package/src/app/dashboard/page.tsx +44 -0
  65. package/src/app/documents/[id]/page.tsx +46 -0
  66. package/src/app/documents/page.tsx +45 -0
  67. package/src/app/error.tsx +26 -0
  68. package/src/app/global-error.tsx +23 -0
  69. package/src/app/globals.css +733 -0
  70. package/src/app/icon.tsx +30 -0
  71. package/src/app/inbox/loading.tsx +15 -0
  72. package/src/app/inbox/page.tsx +35 -0
  73. package/src/app/layout.tsx +78 -0
  74. package/src/app/manifest.ts +32 -0
  75. package/src/app/monitor/page.tsx +37 -0
  76. package/src/app/page.tsx +162 -0
  77. package/src/app/profiles/[id]/edit/page.tsx +39 -0
  78. package/src/app/profiles/[id]/page.tsx +33 -0
  79. package/src/app/profiles/new/page.tsx +22 -0
  80. package/src/app/profiles/page.tsx +19 -0
  81. package/src/app/projects/[id]/page.tsx +134 -0
  82. package/src/app/projects/loading.tsx +17 -0
  83. package/src/app/projects/page.tsx +32 -0
  84. package/src/app/schedules/[id]/page.tsx +47 -0
  85. package/src/app/schedules/page.tsx +18 -0
  86. package/src/app/settings/loading.tsx +24 -0
  87. package/src/app/settings/page.tsx +27 -0
  88. package/src/app/tasks/[id]/page.tsx +45 -0
  89. package/src/app/tasks/new/page.tsx +27 -0
  90. package/src/app/workflows/[id]/edit/page.tsx +66 -0
  91. package/src/app/workflows/[id]/page.tsx +37 -0
  92. package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
  93. package/src/app/workflows/blueprints/new/page.tsx +20 -0
  94. package/src/app/workflows/blueprints/page.tsx +11 -0
  95. package/src/app/workflows/new/page.tsx +36 -0
  96. package/src/app/workflows/page.tsx +18 -0
  97. package/src/components/charts/donut-ring.tsx +64 -0
  98. package/src/components/charts/mini-bar.tsx +75 -0
  99. package/src/components/charts/sparkline.tsx +107 -0
  100. package/src/components/costs/cost-dashboard.tsx +877 -0
  101. package/src/components/costs/cost-filters.tsx +179 -0
  102. package/src/components/dashboard/activity-feed.tsx +95 -0
  103. package/src/components/dashboard/greeting.tsx +30 -0
  104. package/src/components/dashboard/priority-queue.tsx +79 -0
  105. package/src/components/dashboard/quick-actions.tsx +62 -0
  106. package/src/components/dashboard/recent-projects.tsx +79 -0
  107. package/src/components/dashboard/stats-cards.tsx +114 -0
  108. package/src/components/documents/document-browser.tsx +235 -0
  109. package/src/components/documents/document-detail-view.tsx +367 -0
  110. package/src/components/documents/document-grid.tsx +78 -0
  111. package/src/components/documents/document-preview.tsx +68 -0
  112. package/src/components/documents/document-table.tsx +119 -0
  113. package/src/components/documents/document-upload-dialog.tsx +153 -0
  114. package/src/components/documents/types.ts +6 -0
  115. package/src/components/documents/utils.ts +57 -0
  116. package/src/components/monitoring/connection-indicator.tsx +14 -0
  117. package/src/components/monitoring/log-entry.tsx +79 -0
  118. package/src/components/monitoring/log-filters.tsx +57 -0
  119. package/src/components/monitoring/log-stream.tsx +144 -0
  120. package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
  121. package/src/components/monitoring/monitor-overview.tsx +119 -0
  122. package/src/components/notifications/failure-action.tsx +38 -0
  123. package/src/components/notifications/inbox-list.tsx +165 -0
  124. package/src/components/notifications/message-response.tsx +196 -0
  125. package/src/components/notifications/notification-item.tsx +250 -0
  126. package/src/components/notifications/pending-approval-host.tsx +478 -0
  127. package/src/components/notifications/permission-action.tsx +37 -0
  128. package/src/components/notifications/permission-response-actions.tsx +126 -0
  129. package/src/components/notifications/unread-badge.tsx +35 -0
  130. package/src/components/profiles/profile-browser.tsx +117 -0
  131. package/src/components/profiles/profile-card.tsx +78 -0
  132. package/src/components/profiles/profile-detail-view.tsx +564 -0
  133. package/src/components/profiles/profile-form-view.tsx +480 -0
  134. package/src/components/profiles/profile-import-dialog.tsx +113 -0
  135. package/src/components/projects/project-card.tsx +58 -0
  136. package/src/components/projects/project-create-dialog.tsx +140 -0
  137. package/src/components/projects/project-detail.tsx +68 -0
  138. package/src/components/projects/project-edit-dialog.tsx +219 -0
  139. package/src/components/projects/project-list.tsx +108 -0
  140. package/src/components/schedules/schedule-create-dialog.tsx +403 -0
  141. package/src/components/schedules/schedule-detail-view.tsx +274 -0
  142. package/src/components/schedules/schedule-list.tsx +242 -0
  143. package/src/components/schedules/schedule-status-badge.tsx +16 -0
  144. package/src/components/settings/api-key-form.tsx +141 -0
  145. package/src/components/settings/auth-config-section.tsx +141 -0
  146. package/src/components/settings/auth-method-selector.tsx +67 -0
  147. package/src/components/settings/auth-status-badge.tsx +40 -0
  148. package/src/components/settings/auth-status-dot.tsx +59 -0
  149. package/src/components/settings/budget-guardrails-section.tsx +842 -0
  150. package/src/components/settings/data-management-section.tsx +141 -0
  151. package/src/components/settings/openai-runtime-section.tsx +104 -0
  152. package/src/components/settings/permissions-section.tsx +91 -0
  153. package/src/components/shared/app-sidebar.tsx +123 -0
  154. package/src/components/shared/card-skeleton.tsx +42 -0
  155. package/src/components/shared/command-palette.tsx +250 -0
  156. package/src/components/shared/confirm-dialog.tsx +52 -0
  157. package/src/components/shared/empty-state.tsx +24 -0
  158. package/src/components/shared/error-state.tsx +32 -0
  159. package/src/components/shared/form-section-card.tsx +33 -0
  160. package/src/components/shared/section-heading.tsx +14 -0
  161. package/src/components/shared/stagent-logo.tsx +21 -0
  162. package/src/components/shared/theme-toggle.tsx +46 -0
  163. package/src/components/tasks/ai-assist-panel.tsx +210 -0
  164. package/src/components/tasks/content-preview.tsx +89 -0
  165. package/src/components/tasks/empty-board.tsx +12 -0
  166. package/src/components/tasks/file-upload.tsx +120 -0
  167. package/src/components/tasks/kanban-board.tsx +275 -0
  168. package/src/components/tasks/kanban-column.tsx +75 -0
  169. package/src/components/tasks/skeleton-board.tsx +21 -0
  170. package/src/components/tasks/task-attachments.tsx +114 -0
  171. package/src/components/tasks/task-card.tsx +101 -0
  172. package/src/components/tasks/task-create-panel.tsx +360 -0
  173. package/src/components/tasks/task-detail-view.tsx +356 -0
  174. package/src/components/ui/alert-dialog.tsx +196 -0
  175. package/src/components/ui/badge.tsx +50 -0
  176. package/src/components/ui/button.tsx +71 -0
  177. package/src/components/ui/card.tsx +92 -0
  178. package/src/components/ui/checkbox.tsx +32 -0
  179. package/src/components/ui/command.tsx +184 -0
  180. package/src/components/ui/dialog.tsx +158 -0
  181. package/src/components/ui/dropdown-menu.tsx +257 -0
  182. package/src/components/ui/form.tsx +167 -0
  183. package/src/components/ui/input.tsx +21 -0
  184. package/src/components/ui/label.tsx +24 -0
  185. package/src/components/ui/popover.tsx +89 -0
  186. package/src/components/ui/progress.tsx +31 -0
  187. package/src/components/ui/radio-group.tsx +45 -0
  188. package/src/components/ui/scroll-area.tsx +58 -0
  189. package/src/components/ui/select.tsx +190 -0
  190. package/src/components/ui/separator.tsx +28 -0
  191. package/src/components/ui/sheet.tsx +143 -0
  192. package/src/components/ui/sidebar.tsx +726 -0
  193. package/src/components/ui/skeleton.tsx +13 -0
  194. package/src/components/ui/slider.tsx +63 -0
  195. package/src/components/ui/sonner.tsx +36 -0
  196. package/src/components/ui/switch.tsx +35 -0
  197. package/src/components/ui/table.tsx +116 -0
  198. package/src/components/ui/tabs.tsx +91 -0
  199. package/src/components/ui/textarea.tsx +18 -0
  200. package/src/components/ui/tooltip.tsx +57 -0
  201. package/src/components/workflows/blueprint-editor.tsx +109 -0
  202. package/src/components/workflows/blueprint-gallery.tsx +155 -0
  203. package/src/components/workflows/blueprint-preview.tsx +240 -0
  204. package/src/components/workflows/loop-status-view.tsx +272 -0
  205. package/src/components/workflows/swarm-dashboard.tsx +185 -0
  206. package/src/components/workflows/workflow-form-view.tsx +1376 -0
  207. package/src/components/workflows/workflow-list.tsx +230 -0
  208. package/src/components/workflows/workflow-status-view.tsx +477 -0
  209. package/src/hooks/use-mobile.ts +19 -0
  210. package/src/instrumentation.ts +7 -0
  211. package/src/lib/agents/claude-agent.ts +737 -0
  212. package/src/lib/agents/execution-manager.ts +27 -0
  213. package/src/lib/agents/profiles/assignment-validation.ts +75 -0
  214. package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
  215. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
  216. package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
  217. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
  218. package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
  219. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
  220. package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
  221. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
  222. package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
  223. package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
  224. package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
  225. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
  226. package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
  227. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
  228. package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
  229. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
  230. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
  231. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
  232. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
  233. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
  234. package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
  235. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
  236. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
  237. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
  238. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
  239. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
  240. package/src/lib/agents/profiles/compatibility.ts +109 -0
  241. package/src/lib/agents/profiles/registry.ts +293 -0
  242. package/src/lib/agents/profiles/test-runner.ts +18 -0
  243. package/src/lib/agents/profiles/test-types.ts +20 -0
  244. package/src/lib/agents/profiles/types.ts +43 -0
  245. package/src/lib/agents/router.ts +56 -0
  246. package/src/lib/agents/runtime/catalog.ts +85 -0
  247. package/src/lib/agents/runtime/claude-sdk.ts +12 -0
  248. package/src/lib/agents/runtime/claude.ts +370 -0
  249. package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
  250. package/src/lib/agents/runtime/index.ts +167 -0
  251. package/src/lib/agents/runtime/openai-codex.ts +1089 -0
  252. package/src/lib/agents/runtime/task-assist-types.ts +8 -0
  253. package/src/lib/agents/runtime/types.ts +30 -0
  254. package/src/lib/constants/settings.ts +13 -0
  255. package/src/lib/constants/status-colors.ts +44 -0
  256. package/src/lib/constants/task-status.ts +49 -0
  257. package/src/lib/data/clear.ts +63 -0
  258. package/src/lib/data/seed-data/documents.ts +715 -0
  259. package/src/lib/data/seed-data/logs.ts +195 -0
  260. package/src/lib/data/seed-data/notifications.ts +141 -0
  261. package/src/lib/data/seed-data/profiles.ts +175 -0
  262. package/src/lib/data/seed-data/projects.ts +61 -0
  263. package/src/lib/data/seed-data/schedules.ts +108 -0
  264. package/src/lib/data/seed-data/tasks.ts +341 -0
  265. package/src/lib/data/seed-data/usage-ledger.ts +130 -0
  266. package/src/lib/data/seed-data/workflows.ts +213 -0
  267. package/src/lib/data/seed.ts +129 -0
  268. package/src/lib/db/index.ts +221 -0
  269. package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
  270. package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
  271. package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
  272. package/src/lib/db/migrations/0003_add_settings.sql +5 -0
  273. package/src/lib/db/migrations/0004_add_documents.sql +20 -0
  274. package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
  275. package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
  276. package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
  277. package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
  278. package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
  279. package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
  280. package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
  281. package/src/lib/db/migrations/meta/_journal.json +27 -0
  282. package/src/lib/db/schema.ts +227 -0
  283. package/src/lib/documents/cleanup.ts +50 -0
  284. package/src/lib/documents/context-builder.ts +75 -0
  285. package/src/lib/documents/output-scanner.ts +166 -0
  286. package/src/lib/documents/processor.ts +120 -0
  287. package/src/lib/documents/processors/image.ts +21 -0
  288. package/src/lib/documents/processors/office.ts +36 -0
  289. package/src/lib/documents/processors/pdf.ts +12 -0
  290. package/src/lib/documents/processors/spreadsheet.ts +18 -0
  291. package/src/lib/documents/processors/text.ts +8 -0
  292. package/src/lib/documents/registry.ts +25 -0
  293. package/src/lib/notifications/actionable.ts +108 -0
  294. package/src/lib/notifications/permissions.ts +169 -0
  295. package/src/lib/queries/chart-data.ts +184 -0
  296. package/src/lib/schedules/interval-parser.ts +110 -0
  297. package/src/lib/schedules/scheduler.ts +220 -0
  298. package/src/lib/settings/auth.ts +98 -0
  299. package/src/lib/settings/budget-guardrails.ts +590 -0
  300. package/src/lib/settings/helpers.ts +23 -0
  301. package/src/lib/settings/openai-auth.ts +80 -0
  302. package/src/lib/settings/permissions.ts +102 -0
  303. package/src/lib/usage/ledger.ts +489 -0
  304. package/src/lib/usage/pricing.ts +68 -0
  305. package/src/lib/utils/crypto.ts +90 -0
  306. package/src/lib/utils/format-timestamp.ts +46 -0
  307. package/src/lib/utils/session-cleanup.ts +26 -0
  308. package/src/lib/utils/stagent-paths.ts +18 -0
  309. package/src/lib/utils.ts +6 -0
  310. package/src/lib/validators/blueprint.ts +43 -0
  311. package/src/lib/validators/profile.ts +64 -0
  312. package/src/lib/validators/project.ts +17 -0
  313. package/src/lib/validators/settings.ts +57 -0
  314. package/src/lib/validators/task.ts +30 -0
  315. package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
  316. package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
  317. package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
  318. package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
  319. package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
  320. package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
  321. package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
  322. package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
  323. package/src/lib/workflows/blueprints/instantiator.ts +131 -0
  324. package/src/lib/workflows/blueprints/registry.ts +128 -0
  325. package/src/lib/workflows/blueprints/template.ts +58 -0
  326. package/src/lib/workflows/blueprints/types.ts +38 -0
  327. package/src/lib/workflows/definition-validation.ts +121 -0
  328. package/src/lib/workflows/engine.ts +1113 -0
  329. package/src/lib/workflows/loop-executor.ts +270 -0
  330. package/src/lib/workflows/parallel.ts +55 -0
  331. package/src/lib/workflows/swarm.ts +97 -0
  332. package/src/lib/workflows/types.ts +112 -0
  333. package/tsconfig.json +41 -0
@@ -0,0 +1,737 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import { db } from "@/lib/db";
3
+ import { tasks, projects, agentLogs, notifications } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { setExecution, removeExecution } from "./execution-manager";
6
+ import { MAX_RESUME_COUNT } from "@/lib/constants/task-status";
7
+ import { getAuthEnv, updateAuthStatus } from "@/lib/settings/auth";
8
+ import { buildDocumentContext } from "@/lib/documents/context-builder";
9
+ import {
10
+ buildTaskOutputInstructions,
11
+ prepareTaskOutputDirectory,
12
+ scanTaskOutputDocuments,
13
+ } from "@/lib/documents/output-scanner";
14
+ import { getProfile } from "./profiles/registry";
15
+ import { resolveProfileRuntimePayload } from "./profiles/compatibility";
16
+ import type { CanUseToolPolicy } from "./profiles/types";
17
+ import { buildClaudeSdkEnv } from "./runtime/claude-sdk";
18
+ import {
19
+ extractUsageSnapshot,
20
+ mergeUsageSnapshot,
21
+ recordUsageLedgerEntry,
22
+ resolveUsageActivityType,
23
+ type UsageActivityType,
24
+ type UsageSnapshot,
25
+ } from "@/lib/usage/ledger";
26
+
27
+ /** Typed representation of messages from the Agent SDK stream */
28
+ interface AgentStreamMessage {
29
+ type?: string;
30
+ subtype?: string;
31
+ session_id?: string;
32
+ api_key_source?: string;
33
+ event?: Record<string, unknown>;
34
+ message?: {
35
+ content?: Array<{ type: string; name?: string; input?: unknown }>;
36
+ };
37
+ result?: unknown;
38
+ }
39
+
40
+ interface TaskUsageState extends UsageSnapshot {
41
+ activityType: UsageActivityType;
42
+ startedAt: Date;
43
+ taskId: string;
44
+ projectId?: string | null;
45
+ workflowId?: string | null;
46
+ scheduleId?: string | null;
47
+ }
48
+
49
+ interface ToolPermissionResponse {
50
+ behavior: "allow" | "deny";
51
+ updatedInput?: unknown;
52
+ message?: string;
53
+ }
54
+
55
+ const inFlightPermissionRequests = new Map<
56
+ string,
57
+ Promise<ToolPermissionResponse>
58
+ >();
59
+ const settledPermissionRequests = new Map<string, ToolPermissionResponse>();
60
+
61
+ function buildAllowedToolPermissionResponse(
62
+ input: Record<string, unknown>
63
+ ): ToolPermissionResponse {
64
+ return {
65
+ behavior: "allow",
66
+ updatedInput: input,
67
+ };
68
+ }
69
+
70
+ function normalizeToolPermissionResponse(
71
+ response: ToolPermissionResponse,
72
+ input: Record<string, unknown>
73
+ ): ToolPermissionResponse {
74
+ if (response.behavior !== "allow" || response.updatedInput !== undefined) {
75
+ return response;
76
+ }
77
+
78
+ return {
79
+ ...response,
80
+ updatedInput: input,
81
+ };
82
+ }
83
+
84
+ function createTaskUsageState(
85
+ task: {
86
+ id: string;
87
+ projectId?: string | null;
88
+ workflowId?: string | null;
89
+ scheduleId?: string | null;
90
+ },
91
+ isResume = false
92
+ ): TaskUsageState {
93
+ return {
94
+ taskId: task.id,
95
+ projectId: task.projectId ?? null,
96
+ workflowId: task.workflowId ?? null,
97
+ scheduleId: task.scheduleId ?? null,
98
+ activityType: resolveUsageActivityType({
99
+ workflowId: task.workflowId,
100
+ scheduleId: task.scheduleId,
101
+ isResume,
102
+ }),
103
+ startedAt: new Date(),
104
+ };
105
+ }
106
+
107
+ function applyUsageSnapshot(state: TaskUsageState, source: unknown) {
108
+ Object.assign(state, mergeUsageSnapshot(state, extractUsageSnapshot(source)));
109
+ }
110
+
111
+ function buildPermissionCacheKey(
112
+ taskId: string,
113
+ toolName: string,
114
+ input: Record<string, unknown>
115
+ ): string {
116
+ return `${taskId}::${toolName}::${JSON.stringify(input)}`;
117
+ }
118
+
119
+ function clearPermissionCache(taskId: string) {
120
+ const prefix = `${taskId}::`;
121
+
122
+ for (const key of inFlightPermissionRequests.keys()) {
123
+ if (key.startsWith(prefix)) {
124
+ inFlightPermissionRequests.delete(key);
125
+ }
126
+ }
127
+
128
+ for (const key of settledPermissionRequests.keys()) {
129
+ if (key.startsWith(prefix)) {
130
+ settledPermissionRequests.delete(key);
131
+ }
132
+ }
133
+ }
134
+
135
+ async function waitForToolPermissionResponse(
136
+ notificationId: string
137
+ ): Promise<ToolPermissionResponse> {
138
+ const deadline = Date.now() + 55_000;
139
+ const pollInterval = 1500;
140
+
141
+ while (Date.now() < deadline) {
142
+ const [notification] = await db
143
+ .select()
144
+ .from(notifications)
145
+ .where(eq(notifications.id, notificationId));
146
+
147
+ if (notification?.response) {
148
+ try {
149
+ return JSON.parse(notification.response) as ToolPermissionResponse;
150
+ } catch {
151
+ return { behavior: "deny", message: "Invalid response format" };
152
+ }
153
+ }
154
+
155
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
156
+ }
157
+
158
+ return { behavior: "deny", message: "Permission request timed out" };
159
+ }
160
+
161
+ async function finalizeTaskUsage(
162
+ state: TaskUsageState,
163
+ status: "completed" | "failed" | "cancelled"
164
+ ) {
165
+ await recordUsageLedgerEntry({
166
+ taskId: state.taskId,
167
+ workflowId: state.workflowId ?? null,
168
+ scheduleId: state.scheduleId ?? null,
169
+ projectId: state.projectId ?? null,
170
+ activityType: state.activityType,
171
+ runtimeId: "claude-code",
172
+ providerId: "anthropic",
173
+ modelId: state.modelId ?? null,
174
+ inputTokens: state.inputTokens ?? null,
175
+ outputTokens: state.outputTokens ?? null,
176
+ totalTokens: state.totalTokens ?? null,
177
+ status,
178
+ startedAt: state.startedAt,
179
+ finishedAt: new Date(),
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Process the async message stream from the Agent SDK.
185
+ * Shared between executeClaudeTask and resumeClaudeTask to avoid duplication.
186
+ */
187
+ async function processAgentStream(
188
+ taskId: string,
189
+ taskTitle: string,
190
+ response: AsyncIterable<Record<string, unknown>>,
191
+ abortController: AbortController,
192
+ agentProfileId = "general",
193
+ usageState: TaskUsageState
194
+ ): Promise<void> {
195
+ let sessionId: string | null = null;
196
+ let receivedResult = false;
197
+
198
+ for await (const raw of response) {
199
+ const message = raw as AgentStreamMessage;
200
+ applyUsageSnapshot(usageState, raw);
201
+
202
+ // Capture session ID from init message
203
+ if (
204
+ message.type === "system" &&
205
+ message.subtype === "init" &&
206
+ message.session_id
207
+ ) {
208
+ sessionId = message.session_id;
209
+ await db
210
+ .update(tasks)
211
+ .set({ sessionId, updatedAt: new Date() })
212
+ .where(eq(tasks.id, taskId));
213
+
214
+ // Capture auth source from init message
215
+ if (message.api_key_source) {
216
+ updateAuthStatus(message.api_key_source as "db" | "env" | "oauth" | "unknown");
217
+ }
218
+
219
+ // Update execution manager with sessionId
220
+ setExecution(taskId, {
221
+ abortController,
222
+ sessionId,
223
+ taskId,
224
+ startedAt: new Date(),
225
+ });
226
+ }
227
+
228
+ // Log meaningful stream events
229
+ if (message.type === "stream_event" && message.event) {
230
+ const event = message.event;
231
+ const eventType = event.type as string;
232
+
233
+ if (
234
+ eventType === "content_block_start" ||
235
+ eventType === "content_block_delta" ||
236
+ eventType === "message_start"
237
+ ) {
238
+ await db.insert(agentLogs).values({
239
+ id: crypto.randomUUID(),
240
+ taskId,
241
+ agentType: agentProfileId,
242
+ event: eventType,
243
+ payload: JSON.stringify(event),
244
+ timestamp: new Date(),
245
+ });
246
+ }
247
+ }
248
+
249
+ // Handle assistant messages (tool use starts)
250
+ if (message.type === "assistant" && message.message?.content) {
251
+ for (const block of message.message.content) {
252
+ if (block.type === "tool_use") {
253
+ await db.insert(agentLogs).values({
254
+ id: crypto.randomUUID(),
255
+ taskId,
256
+ agentType: agentProfileId,
257
+ event: "tool_start",
258
+ payload: JSON.stringify({
259
+ tool: block.name,
260
+ input: block.input,
261
+ }),
262
+ timestamp: new Date(),
263
+ });
264
+ }
265
+ }
266
+ }
267
+
268
+ // Handle result — skip if task was cancelled mid-stream
269
+ if (message.type === "result" && "result" in raw) {
270
+ if (abortController.signal.aborted) {
271
+ await finalizeTaskUsage(usageState, "cancelled");
272
+ return;
273
+ }
274
+ receivedResult = true;
275
+ const resultText =
276
+ typeof message.result === "string"
277
+ ? message.result
278
+ : JSON.stringify(message.result);
279
+
280
+ await db
281
+ .update(tasks)
282
+ .set({
283
+ status: "completed",
284
+ result: resultText,
285
+ updatedAt: new Date(),
286
+ })
287
+ .where(eq(tasks.id, taskId));
288
+
289
+ await db.insert(notifications).values({
290
+ id: crypto.randomUUID(),
291
+ taskId,
292
+ type: "task_completed",
293
+ title: `Task completed: ${taskTitle}`,
294
+ body: resultText.slice(0, 500),
295
+ createdAt: new Date(),
296
+ });
297
+
298
+ await db.insert(agentLogs).values({
299
+ id: crypto.randomUUID(),
300
+ taskId,
301
+ agentType: agentProfileId,
302
+ event: "completed",
303
+ payload: JSON.stringify({ result: resultText.slice(0, 1000) }),
304
+ timestamp: new Date(),
305
+ });
306
+
307
+ try {
308
+ await scanTaskOutputDocuments(taskId);
309
+ } catch (error) {
310
+ await db.insert(agentLogs).values({
311
+ id: crypto.randomUUID(),
312
+ taskId,
313
+ agentType: agentProfileId,
314
+ event: "output_scan_failed",
315
+ payload: JSON.stringify({
316
+ error: error instanceof Error ? error.message : String(error),
317
+ }),
318
+ timestamp: new Date(),
319
+ });
320
+ }
321
+
322
+ await finalizeTaskUsage(usageState, "completed");
323
+ }
324
+ }
325
+
326
+ // Safety net: if stream ended without a result frame, fail the task
327
+ // instead of leaving it stuck in "running" forever
328
+ if (!receivedResult) {
329
+ await db
330
+ .update(tasks)
331
+ .set({
332
+ status: "failed",
333
+ result: "Agent stream ended without producing a result",
334
+ updatedAt: new Date(),
335
+ })
336
+ .where(eq(tasks.id, taskId));
337
+
338
+ await db.insert(notifications).values({
339
+ id: crypto.randomUUID(),
340
+ taskId,
341
+ type: "task_failed",
342
+ title: `Task failed: ${taskTitle}`,
343
+ body: "Agent stream ended unexpectedly without a result",
344
+ createdAt: new Date(),
345
+ });
346
+
347
+ await finalizeTaskUsage(usageState, "failed");
348
+ }
349
+ }
350
+
351
+ export async function executeClaudeTask(taskId: string): Promise<void> {
352
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
353
+ if (!task) throw new Error(`Task ${taskId} not found`);
354
+ const usageState = createTaskUsageState(task);
355
+
356
+ const abortController = new AbortController();
357
+
358
+ setExecution(taskId, {
359
+ abortController,
360
+ sessionId: null,
361
+ taskId,
362
+ startedAt: new Date(),
363
+ });
364
+
365
+ try {
366
+ await prepareTaskOutputDirectory(taskId, { clearExisting: true });
367
+ const profile = getProfile(task.agentProfile ?? "general");
368
+ const payload = profile
369
+ ? resolveProfileRuntimePayload(profile, "claude-code")
370
+ : null;
371
+ if (payload && !payload.supported) {
372
+ throw new Error(payload.reason ?? `Profile "${profile?.name}" is not supported on Claude Code`);
373
+ }
374
+ const systemPrompt = payload?.instructions ?? "";
375
+ const basePrompt = task.description || task.title;
376
+ const docContext = await buildDocumentContext(taskId);
377
+ const outputInstructions = buildTaskOutputInstructions(taskId);
378
+ const prompt = [systemPrompt, docContext, outputInstructions, basePrompt]
379
+ .filter(Boolean)
380
+ .join("\n\n");
381
+
382
+ // Resolve working directory: project's workingDirectory > process.cwd()
383
+ let cwd = process.cwd();
384
+ if (task.projectId) {
385
+ const [project] = await db
386
+ .select({ workingDirectory: projects.workingDirectory })
387
+ .from(projects)
388
+ .where(eq(projects.id, task.projectId));
389
+ if (project?.workingDirectory) {
390
+ cwd = project.workingDirectory;
391
+ }
392
+ }
393
+
394
+ const policyForTask = payload?.canUseToolPolicy;
395
+ const authEnv = await getAuthEnv();
396
+ const response = query({
397
+ prompt,
398
+ options: {
399
+ abortController,
400
+ includePartialMessages: true,
401
+ cwd,
402
+ env: buildClaudeSdkEnv(authEnv),
403
+ ...(payload?.allowedTools && { allowedTools: payload.allowedTools }),
404
+ ...(payload?.mcpServers &&
405
+ Object.keys(payload.mcpServers).length > 0 && {
406
+ mcpServers: payload.mcpServers,
407
+ }),
408
+ // @ts-expect-error Agent SDK canUseTool types are incomplete — our async handler is compatible at runtime
409
+ canUseTool: async (
410
+ toolName: string,
411
+ input: Record<string, unknown>
412
+ ) => {
413
+ return handleToolPermission(taskId, toolName, input, policyForTask);
414
+ },
415
+ },
416
+ });
417
+
418
+ await processAgentStream(
419
+ taskId,
420
+ task.title,
421
+ response as AsyncIterable<Record<string, unknown>>,
422
+ abortController,
423
+ task.agentProfile ?? "general",
424
+ usageState
425
+ );
426
+ } catch (error: unknown) {
427
+ await handleExecutionError(
428
+ taskId,
429
+ task.title,
430
+ error,
431
+ abortController,
432
+ task.agentProfile ?? "general",
433
+ usageState
434
+ );
435
+ } finally {
436
+ clearPermissionCache(taskId);
437
+ removeExecution(taskId);
438
+ }
439
+ }
440
+
441
+ export async function resumeClaudeTask(taskId: string): Promise<void> {
442
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
443
+ if (!task) throw new Error(`Task ${taskId} not found`);
444
+ const usageState = createTaskUsageState(task, true);
445
+
446
+ if (!task.sessionId) {
447
+ throw new Error("No session to resume — use Retry instead");
448
+ }
449
+
450
+ if (task.resumeCount >= MAX_RESUME_COUNT) {
451
+ throw new Error("Resume limit reached. Re-queue for fresh start.");
452
+ }
453
+
454
+ // Increment resume count
455
+ await db
456
+ .update(tasks)
457
+ .set({ resumeCount: task.resumeCount + 1, updatedAt: new Date() })
458
+ .where(eq(tasks.id, taskId));
459
+
460
+ const abortController = new AbortController();
461
+
462
+ setExecution(taskId, {
463
+ abortController,
464
+ sessionId: task.sessionId,
465
+ taskId,
466
+ startedAt: new Date(),
467
+ });
468
+
469
+ const profileId = task.agentProfile ?? "general";
470
+
471
+ await db.insert(agentLogs).values({
472
+ id: crypto.randomUUID(),
473
+ taskId,
474
+ agentType: profileId,
475
+ event: "session_resumed",
476
+ payload: JSON.stringify({
477
+ sessionId: task.sessionId,
478
+ resumeCount: task.resumeCount + 1,
479
+ profile: profileId,
480
+ }),
481
+ timestamp: new Date(),
482
+ });
483
+
484
+ try {
485
+ await prepareTaskOutputDirectory(taskId);
486
+ const profile = getProfile(profileId);
487
+ const payload = profile
488
+ ? resolveProfileRuntimePayload(profile, "claude-code")
489
+ : null;
490
+ if (payload && !payload.supported) {
491
+ throw new Error(payload.reason ?? `Profile "${profile?.name}" is not supported on Claude Code`);
492
+ }
493
+ const systemPrompt = payload?.instructions ?? "";
494
+ const basePrompt = task.description || task.title;
495
+ const docContext = await buildDocumentContext(taskId);
496
+ const outputInstructions = buildTaskOutputInstructions(taskId);
497
+ const prompt = [systemPrompt, docContext, outputInstructions, basePrompt]
498
+ .filter(Boolean)
499
+ .join("\n\n");
500
+
501
+ // Resolve working directory: project's workingDirectory > process.cwd()
502
+ let cwd = process.cwd();
503
+ if (task.projectId) {
504
+ const [project] = await db
505
+ .select({ workingDirectory: projects.workingDirectory })
506
+ .from(projects)
507
+ .where(eq(projects.id, task.projectId));
508
+ if (project?.workingDirectory) {
509
+ cwd = project.workingDirectory;
510
+ }
511
+ }
512
+
513
+ const policyForResume = payload?.canUseToolPolicy;
514
+ const authEnv = await getAuthEnv();
515
+ const response = query({
516
+ prompt,
517
+ options: {
518
+ resume: task.sessionId,
519
+ abortController,
520
+ includePartialMessages: true,
521
+ cwd,
522
+ env: buildClaudeSdkEnv(authEnv),
523
+ ...(payload?.allowedTools && { allowedTools: payload.allowedTools }),
524
+ ...(payload?.mcpServers &&
525
+ Object.keys(payload.mcpServers).length > 0 && {
526
+ mcpServers: payload.mcpServers,
527
+ }),
528
+ // @ts-expect-error Agent SDK canUseTool types are incomplete — our async handler is compatible at runtime
529
+ canUseTool: async (
530
+ toolName: string,
531
+ input: Record<string, unknown>
532
+ ) => {
533
+ return handleToolPermission(taskId, toolName, input, policyForResume);
534
+ },
535
+ },
536
+ });
537
+
538
+ await processAgentStream(
539
+ taskId,
540
+ task.title,
541
+ response as AsyncIterable<Record<string, unknown>>,
542
+ abortController,
543
+ profileId,
544
+ usageState
545
+ );
546
+ } catch (error: unknown) {
547
+ const errorMessage =
548
+ error instanceof Error ? error.message : String(error);
549
+
550
+ // Detect session expiry from the SDK
551
+ if (
552
+ errorMessage.includes("session") &&
553
+ (errorMessage.includes("expired") || errorMessage.includes("not found"))
554
+ ) {
555
+ await db
556
+ .update(tasks)
557
+ .set({
558
+ status: "failed",
559
+ result: "Session expired — re-queue for fresh start",
560
+ sessionId: null,
561
+ updatedAt: new Date(),
562
+ })
563
+ .where(eq(tasks.id, taskId));
564
+
565
+ await db.insert(notifications).values({
566
+ id: crypto.randomUUID(),
567
+ taskId,
568
+ type: "task_failed",
569
+ title: `Session expired: ${task.title}`,
570
+ body: "The agent session has expired. Re-queue this task for a fresh start.",
571
+ createdAt: new Date(),
572
+ });
573
+ await finalizeTaskUsage(usageState, "failed");
574
+ return;
575
+ }
576
+
577
+ await handleExecutionError(
578
+ taskId,
579
+ task.title,
580
+ error,
581
+ abortController,
582
+ profileId,
583
+ usageState
584
+ );
585
+ } finally {
586
+ clearPermissionCache(taskId);
587
+ removeExecution(taskId);
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Shared error handler for both execute and resume paths.
593
+ */
594
+ async function handleExecutionError(
595
+ taskId: string,
596
+ taskTitle: string,
597
+ error: unknown,
598
+ abortController: AbortController,
599
+ agentProfileId = "general",
600
+ usageState?: TaskUsageState
601
+ ): Promise<void> {
602
+ const errorMessage =
603
+ error instanceof Error ? error.message : String(error);
604
+
605
+ if (abortController.signal.aborted) {
606
+ await db
607
+ .update(tasks)
608
+ .set({ status: "cancelled", updatedAt: new Date() })
609
+ .where(eq(tasks.id, taskId));
610
+ if (usageState) {
611
+ await finalizeTaskUsage(usageState, "cancelled");
612
+ }
613
+ return;
614
+ }
615
+
616
+ await db
617
+ .update(tasks)
618
+ .set({
619
+ status: "failed",
620
+ result: errorMessage,
621
+ updatedAt: new Date(),
622
+ })
623
+ .where(eq(tasks.id, taskId));
624
+
625
+ await db.insert(notifications).values({
626
+ id: crypto.randomUUID(),
627
+ taskId,
628
+ type: "task_failed",
629
+ title: `Task failed: ${taskTitle}`,
630
+ body: errorMessage.slice(0, 500),
631
+ createdAt: new Date(),
632
+ });
633
+
634
+ await db.insert(agentLogs).values({
635
+ id: crypto.randomUUID(),
636
+ taskId,
637
+ agentType: agentProfileId,
638
+ event: "error",
639
+ payload: JSON.stringify({ error: errorMessage }),
640
+ timestamp: new Date(),
641
+ });
642
+
643
+ if (usageState) {
644
+ await finalizeTaskUsage(usageState, "failed");
645
+ }
646
+ }
647
+
648
+ /**
649
+ * Handle tool permission by inserting a notification and polling for response.
650
+ * Uses database polling pattern — the Inbox UI writes the response.
651
+ */
652
+ async function handleToolPermission(
653
+ taskId: string,
654
+ toolName: string,
655
+ input: Record<string, unknown>,
656
+ canUseToolPolicy?: CanUseToolPolicy
657
+ ): Promise<ToolPermissionResponse> {
658
+ const isQuestion = toolName === "AskUserQuestion";
659
+
660
+ // Layer 1: Profile-level canUseToolPolicy — fastest check, no I/O
661
+ if (!isQuestion && canUseToolPolicy) {
662
+ if (canUseToolPolicy.autoApprove?.includes(toolName)) {
663
+ return buildAllowedToolPermissionResponse(input);
664
+ }
665
+ if (canUseToolPolicy.autoDeny?.includes(toolName)) {
666
+ return { behavior: "deny", message: `Profile policy denies ${toolName}` };
667
+ }
668
+ }
669
+
670
+ // Layer 2: Saved user permissions — skip notification for pre-approved tools
671
+ if (!isQuestion) {
672
+ const { isToolAllowed } = await import("@/lib/settings/permissions");
673
+ if (await isToolAllowed(toolName, input)) {
674
+ return buildAllowedToolPermissionResponse(input);
675
+ }
676
+ }
677
+
678
+ if (!isQuestion) {
679
+ const cacheKey = buildPermissionCacheKey(taskId, toolName, input);
680
+ const settledResponse = settledPermissionRequests.get(cacheKey);
681
+ if (settledResponse) {
682
+ return normalizeToolPermissionResponse(settledResponse, input);
683
+ }
684
+
685
+ const pendingRequest = inFlightPermissionRequests.get(cacheKey);
686
+ if (pendingRequest) {
687
+ return pendingRequest;
688
+ }
689
+
690
+ const requestPromise = (async () => {
691
+ const notificationId = crypto.randomUUID();
692
+
693
+ await db.insert(notifications).values({
694
+ id: notificationId,
695
+ taskId,
696
+ type: "permission_required",
697
+ title: `Permission required: ${toolName}`,
698
+ body: JSON.stringify(input).slice(0, 1000),
699
+ toolName,
700
+ toolInput: JSON.stringify(input),
701
+ createdAt: new Date(),
702
+ });
703
+
704
+ const response = normalizeToolPermissionResponse(
705
+ await waitForToolPermissionResponse(notificationId),
706
+ input
707
+ );
708
+ settledPermissionRequests.set(cacheKey, response);
709
+ return response;
710
+ })();
711
+
712
+ inFlightPermissionRequests.set(cacheKey, requestPromise);
713
+
714
+ try {
715
+ return await requestPromise;
716
+ } finally {
717
+ inFlightPermissionRequests.delete(cacheKey);
718
+ }
719
+ }
720
+
721
+ const notificationId = crypto.randomUUID();
722
+
723
+ await db.insert(notifications).values({
724
+ id: notificationId,
725
+ taskId,
726
+ type: isQuestion ? "agent_message" : "permission_required",
727
+ title: isQuestion
728
+ ? "Agent has a question"
729
+ : `Permission required: ${toolName}`,
730
+ body: JSON.stringify(input).slice(0, 1000),
731
+ toolName,
732
+ toolInput: JSON.stringify(input),
733
+ createdAt: new Date(),
734
+ });
735
+
736
+ return waitForToolPermissionResponse(notificationId);
737
+ }