stagent 0.5.0 → 0.6.1

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 (256) hide show
  1. package/README.md +8 -8
  2. package/dist/cli.js +146 -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 +33 -56
  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 +70 -40
  21. package/docs/features/settings.md +74 -46
  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 +77 -45
  32. package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
  33. package/docs/use-cases/agency-operator.md +84 -0
  34. package/docs/use-cases/solo-founder.md +75 -0
  35. package/docs/why-stagent.md +59 -0
  36. package/package.json +10 -3
  37. package/src/app/api/channels/[id]/route.ts +104 -0
  38. package/src/app/api/channels/[id]/test/route.ts +52 -0
  39. package/src/app/api/channels/inbound/slack/route.ts +116 -0
  40. package/src/app/api/channels/inbound/telegram/poll/route.ts +140 -0
  41. package/src/app/api/channels/inbound/telegram/route.ts +87 -0
  42. package/src/app/api/channels/route.ts +72 -0
  43. package/src/app/api/chat/conversations/route.ts +15 -0
  44. package/src/app/api/chat/entities/search/route.ts +46 -31
  45. package/src/app/api/data/clear/route.ts +4 -0
  46. package/src/app/api/data/seed/route.ts +4 -0
  47. package/src/app/api/documents/route.ts +36 -6
  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/runtimes/ollama/route.ts +86 -0
  66. package/src/app/api/runtimes/suggest/route.ts +29 -0
  67. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  68. package/src/app/api/schedules/[id]/route.ts +41 -3
  69. package/src/app/api/schedules/parse/route.ts +66 -0
  70. package/src/app/api/schedules/route.ts +71 -12
  71. package/src/app/api/settings/author-default/route.ts +7 -0
  72. package/src/app/api/settings/learning/route.ts +41 -0
  73. package/src/app/api/settings/ollama/route.ts +34 -0
  74. package/src/app/api/settings/providers/route.ts +57 -0
  75. package/src/app/api/settings/routing/route.ts +24 -0
  76. package/src/app/api/settings/web-search/route.ts +28 -0
  77. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  78. package/src/app/api/tasks/[id]/respond/route.ts +23 -1
  79. package/src/app/documents/page.tsx +3 -0
  80. package/src/app/environment/page.tsx +8 -1
  81. package/src/app/settings/page.tsx +10 -4
  82. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  83. package/src/app/workflows/new/page.tsx +2 -0
  84. package/src/components/chat/chat-command-popover.tsx +22 -19
  85. package/src/components/chat/chat-input.tsx +5 -0
  86. package/src/components/chat/chat-model-selector.tsx +42 -1
  87. package/src/components/chat/chat-shell.tsx +2 -0
  88. package/src/components/dashboard/welcome-landing.tsx +9 -9
  89. package/src/components/environment/artifact-card.tsx +27 -1
  90. package/src/components/environment/environment-dashboard.tsx +50 -2
  91. package/src/components/environment/environment-summary-card.tsx +5 -2
  92. package/src/components/environment/suggested-profiles.tsx +117 -52
  93. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  94. package/src/components/memory/memory-browser.tsx +315 -0
  95. package/src/components/profiles/learned-context-panel.tsx +4 -4
  96. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  97. package/src/components/profiles/profile-browser.tsx +109 -8
  98. package/src/components/profiles/profile-card.tsx +29 -1
  99. package/src/components/profiles/profile-detail-view.tsx +200 -28
  100. package/src/components/profiles/profile-form-view.tsx +220 -82
  101. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  102. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  103. package/src/components/schedules/schedule-create-sheet.tsx +9 -1
  104. package/src/components/schedules/schedule-form.tsx +348 -9
  105. package/src/components/schedules/schedule-list.tsx +15 -2
  106. package/src/components/settings/auth-method-selector.tsx +7 -1
  107. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  108. package/src/components/settings/channels-section.tsx +526 -0
  109. package/src/components/settings/chat-settings-section.tsx +27 -1
  110. package/src/components/settings/data-management-section.tsx +8 -6
  111. package/src/components/settings/learning-context-section.tsx +124 -0
  112. package/src/components/settings/ollama-section.tsx +270 -0
  113. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  114. package/src/components/settings/web-search-section.tsx +101 -0
  115. package/src/components/shared/tag-input.tsx +156 -0
  116. package/src/components/tasks/kanban-board.tsx +32 -0
  117. package/src/components/tasks/kanban-column.tsx +4 -2
  118. package/src/components/tasks/task-card.tsx +1 -0
  119. package/src/components/tasks/task-chip-bar.tsx +6 -1
  120. package/src/components/tasks/task-create-panel.tsx +55 -5
  121. package/src/components/workflows/workflow-form-view.tsx +38 -3
  122. package/src/hooks/use-chat-autocomplete.ts +24 -26
  123. package/src/hooks/use-project-skills.ts +66 -0
  124. package/src/hooks/use-tag-suggestions.ts +31 -0
  125. package/src/instrumentation.ts +4 -1
  126. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  127. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  128. package/src/lib/agents/agentic-loop.ts +235 -0
  129. package/src/lib/agents/browser-mcp.ts +59 -4
  130. package/src/lib/agents/claude-agent.ts +27 -200
  131. package/src/lib/agents/handoff/bus.ts +164 -0
  132. package/src/lib/agents/handoff/governance.ts +47 -0
  133. package/src/lib/agents/handoff/types.ts +16 -0
  134. package/src/lib/agents/learned-context.ts +27 -7
  135. package/src/lib/agents/memory/decay.ts +61 -0
  136. package/src/lib/agents/memory/extractor.ts +181 -0
  137. package/src/lib/agents/memory/retrieval.ts +96 -0
  138. package/src/lib/agents/memory/types.ts +6 -0
  139. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  140. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  141. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  142. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  143. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  144. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  145. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  146. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  147. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  148. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  149. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  150. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  151. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  152. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  153. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  154. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  155. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  156. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  157. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  158. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  159. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  160. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  162. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  163. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  164. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  165. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  166. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  168. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  169. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  170. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  171. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  172. package/src/lib/agents/profiles/registry.ts +130 -6
  173. package/src/lib/agents/profiles/types.ts +28 -0
  174. package/src/lib/agents/router.ts +174 -2
  175. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  176. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  177. package/src/lib/agents/runtime/catalog.ts +57 -2
  178. package/src/lib/agents/runtime/claude.ts +205 -1
  179. package/src/lib/agents/runtime/index.ts +22 -0
  180. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  181. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  182. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  183. package/src/lib/agents/runtime/types.ts +2 -0
  184. package/src/lib/agents/tool-permissions.ts +203 -0
  185. package/src/lib/channels/gateway.ts +321 -0
  186. package/src/lib/channels/poller.ts +268 -0
  187. package/src/lib/channels/registry.ts +90 -0
  188. package/src/lib/channels/slack-adapter.ts +188 -0
  189. package/src/lib/channels/telegram-adapter.ts +218 -0
  190. package/src/lib/channels/types.ts +75 -0
  191. package/src/lib/channels/webhook-adapter.ts +74 -0
  192. package/src/lib/chat/context-builder.ts +22 -2
  193. package/src/lib/chat/engine.ts +95 -13
  194. package/src/lib/chat/ollama-engine.ts +198 -0
  195. package/src/lib/chat/stagent-tools.ts +106 -20
  196. package/src/lib/chat/tool-catalog.ts +24 -0
  197. package/src/lib/chat/tool-registry.ts +90 -0
  198. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  199. package/src/lib/chat/tools/document-tools.ts +7 -7
  200. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  201. package/src/lib/chat/tools/notification-tools.ts +4 -4
  202. package/src/lib/chat/tools/profile-tools.ts +3 -3
  203. package/src/lib/chat/tools/project-tools.ts +3 -3
  204. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  205. package/src/lib/chat/tools/settings-tools.ts +2 -2
  206. package/src/lib/chat/tools/task-tools.ts +66 -11
  207. package/src/lib/chat/tools/usage-tools.ts +2 -2
  208. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  209. package/src/lib/chat/types.ts +11 -5
  210. package/src/lib/constants/known-tools.ts +19 -0
  211. package/src/lib/constants/prose-styles.ts +1 -1
  212. package/src/lib/constants/settings.ts +7 -0
  213. package/src/lib/data/channel-bindings.ts +85 -0
  214. package/src/lib/data/clear.ts +22 -0
  215. package/src/lib/data/profile-test-results.ts +48 -0
  216. package/src/lib/data/seed-data/conversations.ts +196 -0
  217. package/src/lib/data/seed-data/learned-context.ts +99 -0
  218. package/src/lib/data/seed-data/notifications.ts +54 -1
  219. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  220. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  221. package/src/lib/data/seed-data/views.ts +60 -0
  222. package/src/lib/data/seed.ts +51 -0
  223. package/src/lib/db/bootstrap.ts +162 -0
  224. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  225. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  226. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  227. package/src/lib/db/schema.ts +190 -1
  228. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  229. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  230. package/src/lib/environment/auto-scan.ts +48 -0
  231. package/src/lib/environment/data.ts +25 -0
  232. package/src/lib/environment/profile-generator.ts +40 -10
  233. package/src/lib/environment/profile-linker.ts +143 -0
  234. package/src/lib/environment/profile-rules.ts +96 -0
  235. package/src/lib/import/dedup.ts +149 -0
  236. package/src/lib/import/format-adapter.ts +631 -0
  237. package/src/lib/import/github-api.ts +219 -0
  238. package/src/lib/import/repo-scanner.ts +251 -0
  239. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  240. package/src/lib/schedules/active-hours.ts +120 -0
  241. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  242. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  243. package/src/lib/schedules/nlp-parser.ts +357 -0
  244. package/src/lib/schedules/scheduler.ts +218 -3
  245. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  246. package/src/lib/settings/helpers.ts +6 -0
  247. package/src/lib/settings/routing.ts +24 -0
  248. package/src/lib/settings/runtime-setup.ts +28 -1
  249. package/src/lib/usage/ledger.ts +2 -1
  250. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  251. package/src/lib/validators/profile.ts +39 -0
  252. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  253. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  254. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  255. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  256. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -0,0 +1,644 @@
1
+ /**
2
+ * Anthropic Direct API runtime adapter.
3
+ *
4
+ * Calls the Anthropic Messages API directly via `@anthropic-ai/sdk`
5
+ * (no subprocess spawning). Supports streaming, tool use, session
6
+ * resume, and budget enforcement.
7
+ */
8
+
9
+ import { db } from "@/lib/db";
10
+ import { tasks, agentLogs, notifications } from "@/lib/db/schema";
11
+ import { eq } from "drizzle-orm";
12
+ import { setExecution, removeExecution, getExecution } from "../execution-manager";
13
+ import { DEFAULT_MAX_TURNS, DEFAULT_MAX_BUDGET_USD } from "@/lib/constants/task-status";
14
+ import {
15
+ buildTaskQueryContext,
16
+ createTaskUsageState,
17
+ } from "../claude-agent";
18
+ import { createToolServer } from "@/lib/chat/stagent-tools";
19
+ import type { AnthropicToolDef } from "@/lib/chat/tool-registry";
20
+ import { handleToolPermission, clearPermissionCache } from "../tool-permissions";
21
+ import {
22
+ runAgenticLoop,
23
+ type LoopMessage,
24
+ type ModelTurnResult,
25
+ type TurnUsage,
26
+ type AgentStreamEvent,
27
+ } from "../agentic-loop";
28
+ import { getRuntimeCatalogEntry } from "./catalog";
29
+ import type {
30
+ AgentRuntimeAdapter,
31
+ RuntimeConnectionResult,
32
+ TaskAssistInput,
33
+ } from "./types";
34
+ import type { TaskAssistResponse } from "./task-assist-types";
35
+ import type { ProfileAssistRequest, ProfileAssistResponse } from "./profile-assist-types";
36
+ import type { ProfileTestReport } from "../profiles/test-types";
37
+ import { getProfile, listProfiles } from "../profiles/registry";
38
+ import {
39
+ recordUsageLedgerEntry,
40
+ resolveUsageActivityType,
41
+ } from "@/lib/usage/ledger";
42
+
43
+ // ── SDK lazy import ──────────────────────────────────────────────────
44
+
45
+ type AnthropicSDK = typeof import("@anthropic-ai/sdk");
46
+ let _sdk: AnthropicSDK | null = null;
47
+
48
+ async function getAnthropicSDK(): Promise<AnthropicSDK> {
49
+ if (!_sdk) {
50
+ _sdk = await import("@anthropic-ai/sdk");
51
+ }
52
+ return _sdk;
53
+ }
54
+
55
+ // ── API key resolution ───────────────────────────────────────────────
56
+
57
+ async function getAnthropicApiKey(): Promise<string> {
58
+ // Try DB-stored key first
59
+ const { getSetting } = await import("@/lib/settings/helpers");
60
+ const { SETTINGS_KEYS } = await import("@/lib/constants/settings");
61
+ const { decrypt } = await import("@/lib/utils/crypto");
62
+
63
+ const encryptedKey = await getSetting(SETTINGS_KEYS.AUTH_API_KEY);
64
+ if (encryptedKey) {
65
+ try {
66
+ return decrypt(encryptedKey);
67
+ } catch {
68
+ // Fall through to env
69
+ }
70
+ }
71
+
72
+ // Fall back to env var
73
+ if (process.env.ANTHROPIC_API_KEY) {
74
+ return process.env.ANTHROPIC_API_KEY;
75
+ }
76
+
77
+ throw new Error("No Anthropic API key configured. Set one in Settings > Authentication.");
78
+ }
79
+
80
+ // ── Streaming model call ─────────────────────────────────────────────
81
+
82
+ /** Options for prompt caching and advanced capabilities. */
83
+ interface AnthropicCallOptions {
84
+ modelId?: string;
85
+ maxTokens?: number;
86
+ /** Enable prompt caching — splits system prompt into cacheable blocks. */
87
+ enableCaching?: boolean;
88
+ /** Profile instructions block (cached separately from base system prompt). */
89
+ profileInstructions?: string;
90
+ /** Extended thinking config (Anthropic only). */
91
+ extendedThinking?: { enabled: boolean; budgetTokens?: number };
92
+ }
93
+
94
+ /**
95
+ * Build system content blocks with optional prompt caching.
96
+ *
97
+ * When caching is enabled, stable content (base prompt, profile instructions)
98
+ * gets `cache_control: { type: "ephemeral" }` so repeated calls reuse cached
99
+ * token processing at 90% lower cost.
100
+ */
101
+ function buildSystemBlocks(
102
+ basePrompt: string,
103
+ profileInstructions: string | undefined,
104
+ enableCaching: boolean,
105
+ ): unknown {
106
+ if (!enableCaching) return basePrompt;
107
+
108
+ const blocks: unknown[] = [];
109
+
110
+ if (profileInstructions) {
111
+ // Cache the base prompt separately from profile instructions
112
+ blocks.push({
113
+ type: "text",
114
+ text: basePrompt,
115
+ cache_control: { type: "ephemeral" },
116
+ });
117
+ blocks.push({
118
+ type: "text",
119
+ text: profileInstructions,
120
+ cache_control: { type: "ephemeral" },
121
+ });
122
+ } else {
123
+ // Single cached block
124
+ blocks.push({
125
+ type: "text",
126
+ text: basePrompt,
127
+ cache_control: { type: "ephemeral" },
128
+ });
129
+ }
130
+
131
+ return blocks;
132
+ }
133
+
134
+ async function callAnthropicModel(
135
+ client: InstanceType<AnthropicSDK["default"]>,
136
+ systemPrompt: string,
137
+ messages: LoopMessage[],
138
+ tools: AnthropicToolDef[],
139
+ signal: AbortSignal,
140
+ emitEvent: (event: AgentStreamEvent) => void,
141
+ options: AnthropicCallOptions = {},
142
+ ): Promise<ModelTurnResult> {
143
+ const modelId = options.modelId ?? "claude-sonnet-4-20250514";
144
+ const maxTokens = options.maxTokens ?? 8192;
145
+
146
+ // Build system content with optional caching
147
+ const systemContent = buildSystemBlocks(
148
+ systemPrompt,
149
+ options.profileInstructions,
150
+ options.enableCaching ?? false,
151
+ );
152
+
153
+ // Build request params
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ const params: any = {
156
+ model: modelId,
157
+ system: systemContent,
158
+ messages: messages as any,
159
+ tools: tools as any,
160
+ max_tokens: maxTokens,
161
+ };
162
+
163
+ // Add extended thinking if enabled
164
+ if (options.extendedThinking?.enabled) {
165
+ params.thinking = {
166
+ type: "enabled",
167
+ budget_tokens: options.extendedThinking.budgetTokens ?? 10000,
168
+ };
169
+ }
170
+
171
+ const stream = client.messages.stream(params, { signal });
172
+
173
+ let text = "";
174
+ let thinkingText = "";
175
+ const toolCalls: ModelTurnResult["toolCalls"] = [];
176
+ let stopReason = "";
177
+ const usage: TurnUsage = { modelId };
178
+
179
+ emitEvent({ type: "status", phase: "running" });
180
+
181
+ const response = await stream.finalMessage();
182
+
183
+ // Extract usage (including cache metrics)
184
+ if (response.usage) {
185
+ usage.inputTokens = response.usage.input_tokens;
186
+ usage.outputTokens = response.usage.output_tokens;
187
+ usage.totalTokens = response.usage.input_tokens + response.usage.output_tokens;
188
+ // Cache metrics are available as cache_creation_input_tokens / cache_read_input_tokens
189
+ const usageAny = response.usage as unknown as Record<string, unknown>;
190
+ if (usageAny.cache_creation_input_tokens || usageAny.cache_read_input_tokens) {
191
+ (usage as Record<string, unknown>).cacheCreationTokens = usageAny.cache_creation_input_tokens ?? 0;
192
+ (usage as Record<string, unknown>).cacheReadTokens = usageAny.cache_read_input_tokens ?? 0;
193
+ }
194
+ }
195
+
196
+ stopReason = response.stop_reason ?? "";
197
+
198
+ // Process content blocks
199
+ for (const block of response.content) {
200
+ if (block.type === "text") {
201
+ text += block.text;
202
+ emitEvent({ type: "delta", content: block.text });
203
+ } else if (block.type === "thinking") {
204
+ thinkingText += (block as { thinking: string }).thinking ?? "";
205
+ emitEvent({ type: "status", phase: "thinking" });
206
+ } else if (block.type === "tool_use") {
207
+ toolCalls.push({
208
+ id: block.id,
209
+ name: block.name,
210
+ arguments: block.input as Record<string, unknown>,
211
+ });
212
+ emitEvent({ type: "status", phase: "tool_use", message: block.name });
213
+ }
214
+ }
215
+
216
+ return {
217
+ text: text || thinkingText, // Fall back to thinking if no text
218
+ toolCalls,
219
+ isComplete: stopReason === "end_turn",
220
+ needsContinuation: stopReason === "max_tokens",
221
+ usage,
222
+ };
223
+ }
224
+
225
+ // ── Session persistence ──────────────────────────────────────────────
226
+
227
+ async function saveSessionSnapshot(
228
+ taskId: string,
229
+ profileId: string,
230
+ messages: LoopMessage[],
231
+ ) {
232
+ await db.insert(agentLogs).values({
233
+ id: crypto.randomUUID(),
234
+ taskId,
235
+ agentType: profileId,
236
+ event: "session_snapshot",
237
+ payload: JSON.stringify({ messages }),
238
+ timestamp: new Date(),
239
+ });
240
+ }
241
+
242
+ async function loadSessionSnapshot(taskId: string): Promise<LoopMessage[] | null> {
243
+ const [log] = await db
244
+ .select()
245
+ .from(agentLogs)
246
+ .where(eq(agentLogs.taskId, taskId))
247
+ .orderBy(agentLogs.timestamp)
248
+ .limit(1);
249
+
250
+ if (!log?.payload) return null;
251
+
252
+ try {
253
+ const data = JSON.parse(log.payload);
254
+ if (Array.isArray(data.messages)) return data.messages;
255
+ } catch {
256
+ // Corrupted snapshot
257
+ }
258
+ return null;
259
+ }
260
+
261
+ // ── Core task execution ──────────────────────────────────────────────
262
+
263
+ async function executeAnthropicDirectTask(taskId: string, isResume = false): Promise<void> {
264
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
265
+ if (!task) throw new Error(`Task ${taskId} not found`);
266
+
267
+ const agentProfileId = task.agentProfile ?? "general";
268
+ const usageState = createTaskUsageState(task, isResume);
269
+ const abortController = new AbortController();
270
+
271
+ setExecution(taskId, {
272
+ abortController,
273
+ sessionId: null,
274
+ taskId,
275
+ startedAt: new Date(),
276
+ });
277
+
278
+ try {
279
+ // Mark as running
280
+ await db
281
+ .update(tasks)
282
+ .set({ status: "running", updatedAt: new Date() })
283
+ .where(eq(tasks.id, taskId));
284
+
285
+ const ctx = await buildTaskQueryContext(task, agentProfileId);
286
+ const apiKey = await getAnthropicApiKey();
287
+ const sdk = await getAnthropicSDK();
288
+ const client = new sdk.default({ apiKey });
289
+
290
+ // Get tools in Anthropic format
291
+ const toolServer = createToolServer(task.projectId);
292
+ const { tools, executeHandler } = toolServer.forProvider("anthropic");
293
+
294
+ // Build initial messages or restore from snapshot
295
+ let initialMessages: LoopMessage[];
296
+ if (isResume) {
297
+ const snapshot = await loadSessionSnapshot(taskId);
298
+ initialMessages = snapshot ?? [{ role: "user", content: ctx.userPrompt }];
299
+ } else {
300
+ initialMessages = [{ role: "user", content: ctx.userPrompt }];
301
+ }
302
+
303
+ // Resolve model from settings
304
+ const { getSetting } = await import("@/lib/settings/helpers");
305
+ const modelId = (await getSetting("anthropic_direct_model")) ?? "claude-sonnet-4-20250514";
306
+
307
+ const maxTurns = ctx.maxTurns ?? DEFAULT_MAX_TURNS;
308
+
309
+ // Log start
310
+ await db.insert(agentLogs).values({
311
+ id: crypto.randomUUID(),
312
+ taskId,
313
+ agentType: agentProfileId,
314
+ event: isResume ? "resumed" : "started",
315
+ payload: JSON.stringify({
316
+ runtime: "anthropic-direct",
317
+ model: modelId,
318
+ maxTurns,
319
+ }),
320
+ timestamp: new Date(),
321
+ });
322
+
323
+ // Run the agentic loop
324
+ const result = await runAgenticLoop(initialMessages, {
325
+ async callModel(messages, signal) {
326
+ // Resolve capability overrides from profile
327
+ const profile = getProfile(agentProfileId);
328
+ const capOverrides = profile?.capabilityOverrides?.["anthropic-direct"];
329
+
330
+ const turnResult = await callAnthropicModel(
331
+ client,
332
+ ctx.systemInstructions,
333
+ messages,
334
+ tools as AnthropicToolDef[],
335
+ signal,
336
+ (evt) => {
337
+ void db.insert(agentLogs).values({
338
+ id: crypto.randomUUID(),
339
+ taskId,
340
+ agentType: agentProfileId,
341
+ event: "stream",
342
+ payload: JSON.stringify(evt),
343
+ timestamp: new Date(),
344
+ });
345
+ },
346
+ {
347
+ modelId: capOverrides?.modelId ?? modelId,
348
+ enableCaching: true,
349
+ profileInstructions: profile?.skillMd,
350
+ extendedThinking: capOverrides?.extendedThinking,
351
+ },
352
+ );
353
+
354
+ // Save session snapshot after each model turn
355
+ await saveSessionSnapshot(taskId, agentProfileId, [
356
+ ...messages,
357
+ { role: "assistant", content: turnResult.text },
358
+ ]);
359
+
360
+ return turnResult;
361
+ },
362
+
363
+ formatToolResult(toolCallId, _toolName, result) {
364
+ return {
365
+ role: "user",
366
+ content: [
367
+ {
368
+ type: "tool_result",
369
+ tool_use_id: toolCallId,
370
+ content: result.content.map((c) => ({
371
+ type: "text",
372
+ text: c.text,
373
+ })),
374
+ is_error: result.isError ?? false,
375
+ },
376
+ ],
377
+ };
378
+ },
379
+
380
+ formatContinuation() {
381
+ return { role: "user", content: "Please continue." };
382
+ },
383
+
384
+ async executeTool(name, args) {
385
+ return executeHandler(name, args);
386
+ },
387
+
388
+ async checkPermission(toolName, args) {
389
+ return handleToolPermission(taskId, toolName, args, ctx.canUseToolPolicy);
390
+ },
391
+
392
+ emitEvent(event) {
393
+ // Events are logged in callModel; no-op here
394
+ },
395
+
396
+ maxTurns,
397
+ maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
398
+ signal: abortController.signal,
399
+ });
400
+
401
+ // Finalize task based on result
402
+ const finalStatus = result.stopReason === "complete" ? "completed" : "failed";
403
+ const resultText = result.stopReason === "complete"
404
+ ? result.finalText
405
+ : `Task stopped: ${result.stopReason}`;
406
+
407
+ await db
408
+ .update(tasks)
409
+ .set({ status: finalStatus, result: resultText, updatedAt: new Date() })
410
+ .where(eq(tasks.id, taskId));
411
+
412
+ await db.insert(notifications).values({
413
+ id: crypto.randomUUID(),
414
+ taskId,
415
+ type: finalStatus === "completed" ? "task_completed" : "task_failed",
416
+ title: `Task ${finalStatus}: ${task.title}`,
417
+ body: resultText.slice(0, 500),
418
+ createdAt: new Date(),
419
+ });
420
+
421
+ await db.insert(agentLogs).values({
422
+ id: crypto.randomUUID(),
423
+ taskId,
424
+ agentType: agentProfileId,
425
+ event: finalStatus,
426
+ payload: JSON.stringify({
427
+ result: resultText.slice(0, 1000),
428
+ turns: result.turnCount,
429
+ usage: result.totalUsage,
430
+ }),
431
+ timestamp: new Date(),
432
+ });
433
+
434
+ // Record usage
435
+ await recordUsageLedgerEntry({
436
+ taskId,
437
+ workflowId: task.workflowId ?? null,
438
+ scheduleId: task.scheduleId ?? null,
439
+ projectId: task.projectId ?? null,
440
+ activityType: resolveUsageActivityType({
441
+ workflowId: task.workflowId,
442
+ scheduleId: task.scheduleId,
443
+ isResume,
444
+ }),
445
+ runtimeId: "anthropic-direct",
446
+ providerId: "anthropic",
447
+ modelId: result.totalUsage.modelId ?? modelId,
448
+ inputTokens: result.totalUsage.inputTokens ?? null,
449
+ outputTokens: result.totalUsage.outputTokens ?? null,
450
+ totalTokens: result.totalUsage.totalTokens ?? null,
451
+ status: finalStatus,
452
+ startedAt: usageState.startedAt,
453
+ finishedAt: new Date(),
454
+ });
455
+ } catch (err) {
456
+ if (!abortController.signal.aborted) {
457
+ const errorMsg = err instanceof Error ? err.message : String(err);
458
+ await db
459
+ .update(tasks)
460
+ .set({ status: "failed", result: errorMsg, updatedAt: new Date() })
461
+ .where(eq(tasks.id, taskId));
462
+
463
+ await db.insert(notifications).values({
464
+ id: crypto.randomUUID(),
465
+ taskId,
466
+ type: "task_failed",
467
+ title: `Task failed: ${task.title}`,
468
+ body: errorMsg.slice(0, 500),
469
+ createdAt: new Date(),
470
+ });
471
+ }
472
+ } finally {
473
+ clearPermissionCache(taskId);
474
+ removeExecution(taskId);
475
+ }
476
+ }
477
+
478
+ // ── Task Assist ──────────────────────────────────────────────────────
479
+
480
+ async function runAnthropicTaskAssist(input: TaskAssistInput): Promise<TaskAssistResponse> {
481
+ const apiKey = await getAnthropicApiKey();
482
+ const sdk = await getAnthropicSDK();
483
+ const client = new sdk.default({ apiKey });
484
+
485
+ const profileIds = listProfiles().map((p) => p.id);
486
+ const profileList = profileIds.length > 0
487
+ ? `Available agent profiles: ${profileIds.join(", ")}`
488
+ : "No explicit profiles available.";
489
+
490
+ const systemPrompt = `You are an AI task definition assistant. Analyze the given task and return ONLY a JSON object (no markdown) with:
491
+ - "improvedDescription": A clearer version of the task
492
+ - "breakdown": Array of step objects if complex (empty array if simple)
493
+ - "recommendedPattern": one of "single", "sequence", "planner-executor", "checkpoint", "parallel", "loop", "swarm"
494
+ - "complexity": "simple", "moderate", or "complex"
495
+ - "needsCheckpoint": boolean
496
+ - "reasoning": Brief explanation
497
+
498
+ ${profileList}`;
499
+
500
+ const userContent = [
501
+ input.title ? `Task title: ${input.title}` : "",
502
+ input.description ? `Description: ${input.description}` : "",
503
+ ].filter(Boolean).join("\n");
504
+
505
+ const response = await client.messages.create({
506
+ model: "claude-sonnet-4-20250514",
507
+ system: systemPrompt,
508
+ messages: [{ role: "user", content: userContent || "Analyze this task" }],
509
+ max_tokens: 2048,
510
+ });
511
+
512
+ const text = response.content
513
+ .filter((b) => b.type === "text")
514
+ .map((b) => (b as { type: "text"; text: string }).text)
515
+ .join("");
516
+
517
+ try {
518
+ return JSON.parse(text);
519
+ } catch {
520
+ return {
521
+ improvedDescription: text,
522
+ breakdown: [],
523
+ recommendedPattern: "single",
524
+ complexity: "simple",
525
+ needsCheckpoint: false,
526
+ reasoning: "Failed to parse structured response",
527
+ };
528
+ }
529
+ }
530
+
531
+ // ── Profile Assist ───────────────────────────────────────────────────
532
+
533
+ async function runAnthropicProfileAssist(input: ProfileAssistRequest): Promise<ProfileAssistResponse> {
534
+ const apiKey = await getAnthropicApiKey();
535
+ const sdk = await getAnthropicSDK();
536
+ const client = new sdk.default({ apiKey });
537
+
538
+ const response = await client.messages.create({
539
+ model: "claude-sonnet-4-20250514",
540
+ system: `You are an AI assistant that helps configure agent profiles. Return ONLY a JSON object with the requested fields.`,
541
+ messages: [{ role: "user", content: JSON.stringify(input) }],
542
+ max_tokens: 2048,
543
+ });
544
+
545
+ const text = response.content
546
+ .filter((b) => b.type === "text")
547
+ .map((b) => (b as { type: "text"; text: string }).text)
548
+ .join("");
549
+
550
+ try {
551
+ return JSON.parse(text);
552
+ } catch {
553
+ return {
554
+ name: "unknown",
555
+ description: text,
556
+ domain: "work" as const,
557
+ tags: [],
558
+ skillMd: "",
559
+ allowedTools: [],
560
+ canUseToolPolicy: { autoApprove: [], autoDeny: [] },
561
+ maxTurns: 10,
562
+ outputFormat: "",
563
+ supportedRuntimes: ["anthropic-direct"],
564
+ tests: [],
565
+ reasoning: "Failed to parse structured response",
566
+ };
567
+ }
568
+ }
569
+
570
+ // ── Connection Test ──────────────────────────────────────────────────
571
+
572
+ async function testAnthropicConnection(): Promise<RuntimeConnectionResult> {
573
+ try {
574
+ const apiKey = await getAnthropicApiKey();
575
+ const sdk = await getAnthropicSDK();
576
+ const client = new sdk.default({ apiKey });
577
+
578
+ // Simple validation: create a minimal request
579
+ await client.messages.create({
580
+ model: "claude-sonnet-4-20250514",
581
+ messages: [{ role: "user", content: "ping" }],
582
+ max_tokens: 1,
583
+ });
584
+
585
+ return { connected: true, apiKeySource: "db" };
586
+ } catch (err) {
587
+ return {
588
+ connected: false,
589
+ error: err instanceof Error ? err.message : "Connection failed",
590
+ };
591
+ }
592
+ }
593
+
594
+ // ── Adapter export ───────────────────────────────────────────────────
595
+
596
+ export const anthropicDirectRuntimeAdapter: AgentRuntimeAdapter = {
597
+ metadata: getRuntimeCatalogEntry("anthropic-direct" as never), // Registered in catalog.ts
598
+
599
+ async executeTask(taskId: string) {
600
+ return executeAnthropicDirectTask(taskId, false);
601
+ },
602
+
603
+ async resumeTask(taskId: string) {
604
+ return executeAnthropicDirectTask(taskId, true);
605
+ },
606
+
607
+ async cancelTask(taskId: string) {
608
+ const execution = getExecution(taskId);
609
+ if (execution?.abortController) {
610
+ execution.abortController.abort();
611
+ }
612
+ await db
613
+ .update(tasks)
614
+ .set({ status: "cancelled", updatedAt: new Date() })
615
+ .where(eq(tasks.id, taskId));
616
+ removeExecution(taskId);
617
+ },
618
+
619
+ async runTaskAssist(input: TaskAssistInput) {
620
+ return runAnthropicTaskAssist(input);
621
+ },
622
+
623
+ async runProfileAssist(input: ProfileAssistRequest) {
624
+ return runAnthropicProfileAssist(input);
625
+ },
626
+
627
+ async runProfileTests(profileId: string): Promise<ProfileTestReport> {
628
+ const profile = getProfile(profileId);
629
+ return {
630
+ profileId,
631
+ profileName: profile?.name ?? profileId,
632
+ runtimeId: "anthropic-direct",
633
+ results: [],
634
+ totalPassed: 0,
635
+ totalFailed: 0,
636
+ unsupported: true,
637
+ unsupportedReason: "Profile smoke tests not yet implemented for Anthropic Direct runtime",
638
+ };
639
+ },
640
+
641
+ async testConnection() {
642
+ return testAnthropicConnection();
643
+ },
644
+ };