stagent 0.4.0 → 0.6.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 (276) hide show
  1. package/README.md +67 -31
  2. package/dist/cli.js +151 -2
  3. package/docs/.coverage-gaps.json +21 -0
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +36 -14
  6. package/docs/features/chat.md +53 -71
  7. package/docs/features/cost-usage.md +14 -10
  8. package/docs/features/dashboard-kanban.md +30 -13
  9. package/docs/features/delivery-channels.md +198 -0
  10. package/docs/features/design-system.md +10 -10
  11. package/docs/features/documents.md +8 -8
  12. package/docs/features/home-workspace.md +20 -15
  13. package/docs/features/inbox-notifications.md +22 -10
  14. package/docs/features/keyboard-navigation.md +11 -11
  15. package/docs/features/monitoring.md +1 -1
  16. package/docs/features/playbook.md +30 -32
  17. package/docs/features/profiles.md +33 -11
  18. package/docs/features/projects.md +2 -2
  19. package/docs/features/provider-runtimes.md +58 -14
  20. package/docs/features/schedules.md +77 -41
  21. package/docs/features/settings.md +134 -51
  22. package/docs/features/shared-components.md +7 -15
  23. package/docs/features/tool-permissions.md +9 -9
  24. package/docs/features/workflows.md +32 -21
  25. package/docs/getting-started.md +33 -9
  26. package/docs/index.md +25 -16
  27. package/docs/journeys/developer.md +124 -207
  28. package/docs/journeys/personal-use.md +70 -79
  29. package/docs/journeys/power-user.md +107 -151
  30. package/docs/journeys/work-use.md +81 -113
  31. package/docs/manifest.json +79 -47
  32. package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
  33. package/docs/superpowers/specs/2026-03-27-chat-screenshot-display-design.md +303 -0
  34. package/docs/use-cases/agency-operator.md +84 -0
  35. package/docs/use-cases/solo-founder.md +75 -0
  36. package/docs/why-stagent.md +59 -0
  37. package/package.json +12 -3
  38. package/src/app/api/channels/[id]/route.ts +103 -0
  39. package/src/app/api/channels/[id]/test/route.ts +52 -0
  40. package/src/app/api/channels/inbound/slack/route.ts +109 -0
  41. package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
  42. package/src/app/api/channels/inbound/telegram/route.ts +76 -0
  43. package/src/app/api/channels/route.ts +71 -0
  44. package/src/app/api/chat/conversations/[id]/messages/route.ts +3 -2
  45. package/src/app/api/chat/conversations/route.ts +15 -0
  46. package/src/app/api/chat/entities/search/route.ts +112 -0
  47. package/src/app/api/documents/[id]/file/route.ts +4 -1
  48. package/src/app/api/environment/profiles/suggest/route.ts +19 -3
  49. package/src/app/api/environment/scan/route.ts +8 -1
  50. package/src/app/api/handoffs/[id]/route.ts +76 -0
  51. package/src/app/api/handoffs/route.ts +89 -0
  52. package/src/app/api/memory/route.ts +181 -0
  53. package/src/app/api/profiles/[id]/route.ts +16 -1
  54. package/src/app/api/profiles/[id]/test/route.ts +4 -0
  55. package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
  56. package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
  57. package/src/app/api/profiles/assist/route.ts +35 -0
  58. package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
  59. package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
  60. package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
  61. package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
  62. package/src/app/api/profiles/import-repo/route.ts +29 -0
  63. package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
  64. package/src/app/api/profiles/route.ts +73 -22
  65. package/src/app/api/projects/[id]/route.ts +119 -9
  66. package/src/app/api/projects/__tests__/delete-project.test.ts +170 -0
  67. package/src/app/api/runtimes/ollama/route.ts +86 -0
  68. package/src/app/api/runtimes/suggest/route.ts +29 -0
  69. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  70. package/src/app/api/schedules/[id]/route.ts +41 -3
  71. package/src/app/api/schedules/parse/route.ts +66 -0
  72. package/src/app/api/schedules/route.ts +71 -12
  73. package/src/app/api/settings/author-default/route.ts +7 -0
  74. package/src/app/api/settings/browser-tools/route.ts +68 -0
  75. package/src/app/api/settings/learning/route.ts +41 -0
  76. package/src/app/api/settings/ollama/route.ts +34 -0
  77. package/src/app/api/settings/providers/route.ts +57 -0
  78. package/src/app/api/settings/routing/route.ts +24 -0
  79. package/src/app/api/settings/web-search/route.ts +28 -0
  80. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  81. package/src/app/documents/page.tsx +3 -0
  82. package/src/app/environment/page.tsx +8 -1
  83. package/src/app/settings/page.tsx +12 -4
  84. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  85. package/src/app/workflows/new/page.tsx +2 -0
  86. package/src/components/chat/chat-command-popover.tsx +280 -0
  87. package/src/components/chat/chat-input.tsx +90 -10
  88. package/src/components/chat/chat-message.tsx +9 -3
  89. package/src/components/chat/chat-model-selector.tsx +42 -1
  90. package/src/components/chat/chat-shell.tsx +31 -5
  91. package/src/components/chat/screenshot-gallery.tsx +96 -0
  92. package/src/components/dashboard/welcome-landing.tsx +9 -9
  93. package/src/components/environment/artifact-card.tsx +27 -1
  94. package/src/components/environment/environment-dashboard.tsx +50 -2
  95. package/src/components/environment/environment-summary-card.tsx +5 -2
  96. package/src/components/environment/suggested-profiles.tsx +117 -52
  97. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  98. package/src/components/memory/memory-browser.tsx +315 -0
  99. package/src/components/monitoring/log-entry.tsx +61 -27
  100. package/src/components/profiles/learned-context-panel.tsx +4 -4
  101. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  102. package/src/components/profiles/profile-browser.tsx +109 -8
  103. package/src/components/profiles/profile-card.tsx +29 -1
  104. package/src/components/profiles/profile-detail-view.tsx +200 -28
  105. package/src/components/profiles/profile-form-view.tsx +220 -82
  106. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  107. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  108. package/src/components/projects/project-detail.tsx +15 -2
  109. package/src/components/schedules/schedule-create-sheet.tsx +32 -330
  110. package/src/components/schedules/schedule-detail-sheet.tsx +37 -21
  111. package/src/components/schedules/schedule-edit-sheet.tsx +159 -0
  112. package/src/components/schedules/schedule-form.tsx +749 -0
  113. package/src/components/schedules/schedule-list.tsx +31 -2
  114. package/src/components/settings/auth-method-selector.tsx +7 -1
  115. package/src/components/settings/browser-tools-section.tsx +247 -0
  116. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  117. package/src/components/settings/channels-section.tsx +526 -0
  118. package/src/components/settings/chat-settings-section.tsx +27 -1
  119. package/src/components/settings/data-management-section.tsx +8 -6
  120. package/src/components/settings/learning-context-section.tsx +124 -0
  121. package/src/components/settings/ollama-section.tsx +270 -0
  122. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  123. package/src/components/settings/runtime-timeout-section.tsx +4 -4
  124. package/src/components/settings/web-search-section.tsx +101 -0
  125. package/src/components/shared/command-palette.tsx +1 -30
  126. package/src/components/shared/screenshot-lightbox.tsx +151 -0
  127. package/src/components/shared/tag-input.tsx +156 -0
  128. package/src/components/tasks/kanban-board.tsx +32 -0
  129. package/src/components/tasks/kanban-column.tsx +4 -2
  130. package/src/components/tasks/task-card.tsx +1 -0
  131. package/src/components/tasks/task-chip-bar.tsx +6 -1
  132. package/src/components/tasks/task-create-panel.tsx +55 -5
  133. package/src/components/workflows/workflow-form-view.tsx +38 -3
  134. package/src/hooks/use-caret-position.ts +104 -0
  135. package/src/hooks/use-chat-autocomplete.ts +288 -0
  136. package/src/hooks/use-project-skills.ts +66 -0
  137. package/src/hooks/use-tag-suggestions.ts +31 -0
  138. package/src/instrumentation.ts +4 -1
  139. package/src/lib/agents/__tests__/browser-mcp.test.ts +175 -0
  140. package/src/lib/agents/__tests__/claude-agent.test.ts +6 -0
  141. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  142. package/src/lib/agents/agentic-loop.ts +235 -0
  143. package/src/lib/agents/browser-mcp.ts +174 -0
  144. package/src/lib/agents/claude-agent.ts +83 -198
  145. package/src/lib/agents/handoff/bus.ts +164 -0
  146. package/src/lib/agents/handoff/governance.ts +47 -0
  147. package/src/lib/agents/handoff/types.ts +16 -0
  148. package/src/lib/agents/learned-context.ts +27 -7
  149. package/src/lib/agents/memory/decay.ts +61 -0
  150. package/src/lib/agents/memory/extractor.ts +181 -0
  151. package/src/lib/agents/memory/retrieval.ts +96 -0
  152. package/src/lib/agents/memory/types.ts +6 -0
  153. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  154. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  155. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  156. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  157. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  158. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  159. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  160. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  162. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  163. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  164. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  165. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  166. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  168. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  169. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  170. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  171. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  172. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  173. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  174. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  175. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  176. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  177. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  178. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  179. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  180. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  181. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  182. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  183. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  184. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  185. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  186. package/src/lib/agents/profiles/registry.ts +130 -6
  187. package/src/lib/agents/profiles/types.ts +28 -0
  188. package/src/lib/agents/router.ts +174 -2
  189. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  190. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  191. package/src/lib/agents/runtime/catalog.ts +57 -2
  192. package/src/lib/agents/runtime/claude.ts +205 -1
  193. package/src/lib/agents/runtime/index.ts +22 -0
  194. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  195. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  196. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  197. package/src/lib/agents/runtime/types.ts +2 -0
  198. package/src/lib/agents/tool-permissions.ts +203 -0
  199. package/src/lib/channels/gateway.ts +321 -0
  200. package/src/lib/channels/poller.ts +268 -0
  201. package/src/lib/channels/registry.ts +90 -0
  202. package/src/lib/channels/slack-adapter.ts +188 -0
  203. package/src/lib/channels/telegram-adapter.ts +218 -0
  204. package/src/lib/channels/types.ts +43 -0
  205. package/src/lib/channels/webhook-adapter.ts +74 -0
  206. package/src/lib/chat/command-data.ts +50 -0
  207. package/src/lib/chat/context-builder.ts +147 -3
  208. package/src/lib/chat/engine.ts +182 -19
  209. package/src/lib/chat/ollama-engine.ts +198 -0
  210. package/src/lib/chat/slash-commands.ts +191 -0
  211. package/src/lib/chat/stagent-tools.ts +106 -20
  212. package/src/lib/chat/tool-catalog.ts +209 -0
  213. package/src/lib/chat/tool-registry.ts +90 -0
  214. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  215. package/src/lib/chat/tools/document-tools.ts +43 -6
  216. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  217. package/src/lib/chat/tools/notification-tools.ts +4 -4
  218. package/src/lib/chat/tools/profile-tools.ts +3 -3
  219. package/src/lib/chat/tools/project-tools.ts +3 -3
  220. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  221. package/src/lib/chat/tools/settings-tools.ts +2 -2
  222. package/src/lib/chat/tools/task-tools.ts +66 -11
  223. package/src/lib/chat/tools/usage-tools.ts +2 -2
  224. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  225. package/src/lib/chat/types.ts +22 -6
  226. package/src/lib/constants/known-tools.ts +19 -0
  227. package/src/lib/constants/prose-styles.ts +1 -1
  228. package/src/lib/constants/settings.ts +11 -0
  229. package/src/lib/data/channel-bindings.ts +85 -0
  230. package/src/lib/data/clear.ts +38 -4
  231. package/src/lib/data/profile-test-results.ts +48 -0
  232. package/src/lib/data/seed-data/conversations.ts +196 -0
  233. package/src/lib/data/seed-data/learned-context.ts +99 -0
  234. package/src/lib/data/seed-data/notifications.ts +54 -1
  235. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  236. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  237. package/src/lib/data/seed-data/views.ts +60 -0
  238. package/src/lib/data/seed.ts +51 -0
  239. package/src/lib/db/bootstrap.ts +167 -0
  240. package/src/lib/db/migrations/0012_add_screenshot_columns.sql +5 -0
  241. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  242. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  243. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  244. package/src/lib/db/schema.ts +192 -1
  245. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  246. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  247. package/src/lib/environment/auto-scan.ts +48 -0
  248. package/src/lib/environment/data.ts +25 -0
  249. package/src/lib/environment/profile-generator.ts +40 -10
  250. package/src/lib/environment/profile-linker.ts +143 -0
  251. package/src/lib/environment/profile-rules.ts +96 -0
  252. package/src/lib/import/dedup.ts +149 -0
  253. package/src/lib/import/format-adapter.ts +631 -0
  254. package/src/lib/import/github-api.ts +219 -0
  255. package/src/lib/import/repo-scanner.ts +251 -0
  256. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  257. package/src/lib/schedules/active-hours.ts +120 -0
  258. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  259. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  260. package/src/lib/schedules/nlp-parser.ts +357 -0
  261. package/src/lib/schedules/scheduler.ts +218 -3
  262. package/src/lib/screenshots/__tests__/persist.test.ts +104 -0
  263. package/src/lib/screenshots/persist.ts +114 -0
  264. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  265. package/src/lib/settings/helpers.ts +6 -0
  266. package/src/lib/settings/routing.ts +24 -0
  267. package/src/lib/settings/runtime-setup.ts +28 -1
  268. package/src/lib/usage/ledger.ts +2 -1
  269. package/src/lib/utils/stagent-paths.ts +4 -0
  270. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  271. package/src/lib/validators/profile.ts +39 -0
  272. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  273. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  274. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  275. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  276. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Provider-agnostic agentic loop for direct API runtimes.
3
+ *
4
+ * The loop handles turn counting, budget tracking, abort signaling,
5
+ * and HITL tool permission checks. Provider-specific logic (API calls,
6
+ * event mapping, tool result formatting) is injected via callbacks.
7
+ */
8
+
9
+ import type { ToolResult } from "@/lib/chat/tool-registry";
10
+ import type { ToolPermissionResponse } from "./tool-permissions";
11
+
12
+ // ── Types ────────────────────────────────────────────────────────────
13
+
14
+ /** A single tool call extracted from the model response. */
15
+ export interface ToolCall {
16
+ id: string;
17
+ name: string;
18
+ arguments: Record<string, unknown>;
19
+ }
20
+
21
+ /** Usage snapshot from a single model turn. */
22
+ export interface TurnUsage {
23
+ inputTokens?: number;
24
+ outputTokens?: number;
25
+ totalTokens?: number;
26
+ modelId?: string;
27
+ costUsd?: number;
28
+ }
29
+
30
+ /** Events emitted during the loop for SSE streaming. */
31
+ export type AgentStreamEvent =
32
+ | { type: "status"; phase: "running" | "tool_use" | "thinking"; message?: string }
33
+ | { type: "delta"; content: string }
34
+ | { type: "done"; finalText: string }
35
+ | { type: "error"; message: string };
36
+
37
+ /** Result of a single model API call (accumulated from stream). */
38
+ export interface ModelTurnResult {
39
+ /** Concatenated text output from the model. */
40
+ text: string;
41
+ /** Tool calls requested by the model. */
42
+ toolCalls: ToolCall[];
43
+ /** Whether the model indicated it is done (end_turn / stop). */
44
+ isComplete: boolean;
45
+ /** Whether output was truncated by max_tokens. */
46
+ needsContinuation: boolean;
47
+ /** Usage for this turn. */
48
+ usage: TurnUsage;
49
+ }
50
+
51
+ /** Message in the conversation history (provider-agnostic shape). */
52
+ export type LoopMessage = Record<string, unknown>;
53
+
54
+ /** Configuration for the agentic loop — provider injects callbacks. */
55
+ export interface AgenticLoopConfig {
56
+ /**
57
+ * Call the model API with the current messages. Must stream events
58
+ * via `emitEvent` and return the accumulated turn result.
59
+ */
60
+ callModel: (
61
+ messages: LoopMessage[],
62
+ signal: AbortSignal,
63
+ ) => Promise<ModelTurnResult>;
64
+
65
+ /** Format a tool result for appending to the message history. */
66
+ formatToolResult: (
67
+ toolCallId: string,
68
+ toolName: string,
69
+ result: ToolResult,
70
+ ) => LoopMessage;
71
+
72
+ /** Format a continuation message (e.g. after max_tokens truncation). */
73
+ formatContinuation: () => LoopMessage;
74
+
75
+ /** Execute a Stagent tool by name. */
76
+ executeTool: (
77
+ name: string,
78
+ args: Record<string, unknown>,
79
+ ) => Promise<ToolResult>;
80
+
81
+ /** HITL permission check. Return allow/deny. */
82
+ checkPermission: (
83
+ toolName: string,
84
+ args: Record<string, unknown>,
85
+ ) => Promise<ToolPermissionResponse>;
86
+
87
+ /** Emit SSE event for real-time UI streaming. */
88
+ emitEvent: (event: AgentStreamEvent) => void;
89
+
90
+ /** Maximum model turns before stopping. */
91
+ maxTurns: number;
92
+
93
+ /** Maximum budget in USD before stopping. */
94
+ maxBudgetUsd?: number;
95
+
96
+ /** Abort signal for cancellation. */
97
+ signal: AbortSignal;
98
+ }
99
+
100
+ /** Result of the agentic loop. */
101
+ export interface AgenticLoopResult {
102
+ finalText: string;
103
+ turnCount: number;
104
+ totalUsage: TurnUsage;
105
+ stopReason: "complete" | "max_turns" | "budget_exceeded" | "cancelled" | "error";
106
+ }
107
+
108
+ // ── Loop implementation ──────────────────────────────────────────────
109
+
110
+ function mergeTurnUsage(total: TurnUsage, turn: TurnUsage): TurnUsage {
111
+ return {
112
+ inputTokens: (total.inputTokens ?? 0) + (turn.inputTokens ?? 0),
113
+ outputTokens: (total.outputTokens ?? 0) + (turn.outputTokens ?? 0),
114
+ totalTokens: (total.totalTokens ?? 0) + (turn.totalTokens ?? 0),
115
+ modelId: turn.modelId ?? total.modelId,
116
+ costUsd: (total.costUsd ?? 0) + (turn.costUsd ?? 0),
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Run a provider-agnostic agentic loop.
122
+ *
123
+ * Repeatedly calls the model, handles tool execution with HITL checks,
124
+ * and enforces turn/budget limits until the model completes or a limit
125
+ * is reached.
126
+ */
127
+ export async function runAgenticLoop(
128
+ initialMessages: LoopMessage[],
129
+ config: AgenticLoopConfig,
130
+ ): Promise<AgenticLoopResult> {
131
+ const messages = [...initialMessages];
132
+ let turnCount = 0;
133
+ let totalUsage: TurnUsage = {};
134
+ let lastText = "";
135
+
136
+ while (turnCount < config.maxTurns) {
137
+ // Check cancellation
138
+ if (config.signal.aborted) {
139
+ return { finalText: lastText, turnCount, totalUsage, stopReason: "cancelled" };
140
+ }
141
+
142
+ // Check budget
143
+ if (config.maxBudgetUsd && (totalUsage.costUsd ?? 0) >= config.maxBudgetUsd) {
144
+ config.emitEvent({ type: "error", message: "Budget limit exceeded" });
145
+ return { finalText: lastText, turnCount, totalUsage, stopReason: "budget_exceeded" };
146
+ }
147
+
148
+ // Call model
149
+ turnCount++;
150
+ let turnResult: ModelTurnResult;
151
+
152
+ try {
153
+ turnResult = await config.callModel(messages, config.signal);
154
+ } catch (err) {
155
+ if (config.signal.aborted) {
156
+ return { finalText: lastText, turnCount, totalUsage, stopReason: "cancelled" };
157
+ }
158
+ const message = err instanceof Error ? err.message : "Model API call failed";
159
+ config.emitEvent({ type: "error", message });
160
+ return { finalText: lastText, turnCount, totalUsage, stopReason: "error" };
161
+ }
162
+
163
+ totalUsage = mergeTurnUsage(totalUsage, turnResult.usage);
164
+ if (turnResult.text) lastText = turnResult.text;
165
+
166
+ // Handle completion
167
+ if (turnResult.isComplete && turnResult.toolCalls.length === 0) {
168
+ config.emitEvent({ type: "done", finalText: lastText });
169
+ return { finalText: lastText, turnCount, totalUsage, stopReason: "complete" };
170
+ }
171
+
172
+ // Handle tool calls
173
+ if (turnResult.toolCalls.length > 0) {
174
+ for (const toolCall of turnResult.toolCalls) {
175
+ if (config.signal.aborted) {
176
+ return { finalText: lastText, turnCount, totalUsage, stopReason: "cancelled" };
177
+ }
178
+
179
+ config.emitEvent({
180
+ type: "status",
181
+ phase: "tool_use",
182
+ message: toolCall.name,
183
+ });
184
+
185
+ // HITL permission check
186
+ const permission = await config.checkPermission(
187
+ toolCall.name,
188
+ toolCall.arguments,
189
+ );
190
+
191
+ let result: ToolResult;
192
+ if (permission.behavior === "deny") {
193
+ result = {
194
+ content: [{ type: "text", text: JSON.stringify({ error: permission.message ?? "Tool denied by user" }) }],
195
+ isError: true,
196
+ };
197
+ } else {
198
+ try {
199
+ result = await config.executeTool(
200
+ toolCall.name,
201
+ (permission.updatedInput as Record<string, unknown>) ?? toolCall.arguments,
202
+ );
203
+ } catch (err) {
204
+ result = {
205
+ content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : "Tool execution failed" }) }],
206
+ isError: true,
207
+ };
208
+ }
209
+ }
210
+
211
+ // Append tool result to messages
212
+ messages.push(
213
+ config.formatToolResult(toolCall.id, toolCall.name, result),
214
+ );
215
+ }
216
+
217
+ // Continue loop — model needs to process tool results
218
+ continue;
219
+ }
220
+
221
+ // Handle max_tokens continuation
222
+ if (turnResult.needsContinuation) {
223
+ messages.push(config.formatContinuation());
224
+ continue;
225
+ }
226
+
227
+ // Shouldn't reach here — safeguard
228
+ config.emitEvent({ type: "done", finalText: lastText });
229
+ return { finalText: lastText, turnCount, totalUsage, stopReason: "complete" };
230
+ }
231
+
232
+ // Max turns exceeded
233
+ config.emitEvent({ type: "error", message: `Max turns (${config.maxTurns}) reached` });
234
+ return { finalText: lastText, turnCount, totalUsage, stopReason: "max_turns" };
235
+ }
@@ -0,0 +1,174 @@
1
+ import { getSetting } from "@/lib/settings/helpers";
2
+ import { SETTINGS_KEYS } from "@/lib/constants/settings";
3
+
4
+ // ── MCP server config types (matches Claude Agent SDK shape) ─────────
5
+
6
+ interface McpStdioConfig {
7
+ type?: "stdio";
8
+ command: string;
9
+ args: string[];
10
+ }
11
+
12
+ interface McpHttpConfig {
13
+ type: "http";
14
+ url: string;
15
+ headers?: Record<string, string>;
16
+ }
17
+
18
+ type AnyMcpServerConfig = McpStdioConfig | McpHttpConfig;
19
+
20
+ // ── Read-only browser tools — auto-approved in chat & task permission callbacks
21
+
22
+ export const BROWSER_READ_ONLY_TOOLS = new Set([
23
+ // Chrome DevTools MCP — read-only
24
+ "mcp__chrome-devtools__take_screenshot",
25
+ "mcp__chrome-devtools__take_snapshot",
26
+ "mcp__chrome-devtools__take_memory_snapshot",
27
+ "mcp__chrome-devtools__list_pages",
28
+ "mcp__chrome-devtools__list_console_messages",
29
+ "mcp__chrome-devtools__list_network_requests",
30
+ "mcp__chrome-devtools__get_console_message",
31
+ "mcp__chrome-devtools__get_network_request",
32
+ "mcp__chrome-devtools__lighthouse_audit",
33
+ "mcp__chrome-devtools__performance_start_trace",
34
+ "mcp__chrome-devtools__performance_stop_trace",
35
+ "mcp__chrome-devtools__performance_analyze_insight",
36
+ // Playwright MCP — read-only
37
+ "mcp__playwright__browser_snapshot",
38
+ "mcp__playwright__browser_console_messages",
39
+ "mcp__playwright__browser_network_requests",
40
+ "mcp__playwright__browser_tabs",
41
+ "mcp__playwright__browser_take_screenshot",
42
+ ]);
43
+
44
+ // ── Helper: check if a tool name belongs to a browser MCP server ─────
45
+
46
+ export function isBrowserTool(toolName: string): boolean {
47
+ return (
48
+ toolName.startsWith("mcp__chrome-devtools__") ||
49
+ toolName.startsWith("mcp__playwright__")
50
+ );
51
+ }
52
+
53
+ export function isBrowserReadOnly(toolName: string): boolean {
54
+ return BROWSER_READ_ONLY_TOOLS.has(toolName);
55
+ }
56
+
57
+ // ── Config builder ───────────────────────────────────────────────────
58
+
59
+ function parseExtraArgs(config: string | null): string[] {
60
+ if (!config) return [];
61
+ const trimmed = config.trim();
62
+ if (!trimmed) return [];
63
+
64
+ // Try JSON array first (e.g. '["--browser", "firefox"]')
65
+ if (trimmed.startsWith("[")) {
66
+ try {
67
+ const parsed = JSON.parse(trimmed);
68
+ if (Array.isArray(parsed)) return parsed.filter((a): a is string => typeof a === "string");
69
+ } catch {
70
+ // Fall through to space-split
71
+ }
72
+ }
73
+
74
+ // Plain string: split on whitespace (e.g. "--headless --browser-url http://localhost:9222")
75
+ return trimmed.split(/\s+/).filter(Boolean);
76
+ }
77
+
78
+ /**
79
+ * Read browser MCP settings from DB and return MCP server configs
80
+ * for any enabled browser servers.
81
+ *
82
+ * Returns `{}` when neither server is enabled — zero overhead.
83
+ */
84
+ export async function getBrowserMcpServers(): Promise<Record<string, McpStdioConfig>> {
85
+ const [chromeEnabled, playwrightEnabled, chromeConfig, playwrightConfig] =
86
+ await Promise.all([
87
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED),
88
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED),
89
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_CONFIG),
90
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_CONFIG),
91
+ ]);
92
+
93
+ const servers: Record<string, McpStdioConfig> = {};
94
+
95
+ if (chromeEnabled === "true") {
96
+ const extraArgs = parseExtraArgs(chromeConfig);
97
+ servers["chrome-devtools"] = {
98
+ command: "npx",
99
+ args: ["-y", "chrome-devtools-mcp@latest", ...extraArgs],
100
+ };
101
+ }
102
+
103
+ if (playwrightEnabled === "true") {
104
+ const extraArgs = parseExtraArgs(playwrightConfig);
105
+ servers.playwright = {
106
+ command: "npx",
107
+ args: ["-y", "@playwright/mcp@latest", ...extraArgs],
108
+ };
109
+ }
110
+
111
+ return servers;
112
+ }
113
+
114
+ /**
115
+ * Build the allowedTools glob patterns for enabled browser MCP servers.
116
+ * Returns an empty array when no browser servers are enabled.
117
+ */
118
+ export async function getBrowserAllowedToolPatterns(): Promise<string[]> {
119
+ const [chromeEnabled, playwrightEnabled] = await Promise.all([
120
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED),
121
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED),
122
+ ]);
123
+
124
+ const patterns: string[] = [];
125
+ if (chromeEnabled === "true") patterns.push("mcp__chrome-devtools__*");
126
+ if (playwrightEnabled === "true") patterns.push("mcp__playwright__*");
127
+ return patterns;
128
+ }
129
+
130
+ // ── Exa Search MCP — semantic web search ────────────────────────────
131
+
132
+ /** All Exa tools are read-only (search, similarity, content fetch) */
133
+ export const EXA_READ_ONLY_TOOLS = new Set([
134
+ "mcp__exa__web_search_exa",
135
+ "mcp__exa__find_similar",
136
+ "mcp__exa__get_contents",
137
+ ]);
138
+
139
+ export function isExaTool(toolName: string): boolean {
140
+ return toolName.startsWith("mcp__exa__");
141
+ }
142
+
143
+ export function isExaReadOnly(toolName: string): boolean {
144
+ return EXA_READ_ONLY_TOOLS.has(toolName);
145
+ }
146
+
147
+ /**
148
+ * Read external MCP server settings from DB and return configs
149
+ * for any enabled servers. Currently supports Exa Search.
150
+ *
151
+ * Returns `{}` when nothing is enabled — zero overhead.
152
+ */
153
+ export async function getExternalMcpServers(): Promise<Record<string, AnyMcpServerConfig>> {
154
+ const exaEnabled = await getSetting(SETTINGS_KEYS.EXA_SEARCH_MCP_ENABLED);
155
+
156
+ const servers: Record<string, AnyMcpServerConfig> = {};
157
+
158
+ if (exaEnabled === "true") {
159
+ servers.exa = { type: "http", url: "https://mcp.exa.ai/mcp" };
160
+ }
161
+
162
+ return servers;
163
+ }
164
+
165
+ /**
166
+ * Build the allowedTools glob patterns for enabled external MCP servers.
167
+ */
168
+ export async function getExternalAllowedToolPatterns(): Promise<string[]> {
169
+ const exaEnabled = await getSetting(SETTINGS_KEYS.EXA_SEARCH_MCP_ENABLED);
170
+
171
+ const patterns: string[] = [];
172
+ if (exaEnabled === "true") patterns.push("mcp__exa__*");
173
+ return patterns;
174
+ }