stagent 0.5.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 (252) 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 +103 -0
  38. package/src/app/api/channels/[id]/test/route.ts +52 -0
  39. package/src/app/api/channels/inbound/slack/route.ts +109 -0
  40. package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
  41. package/src/app/api/channels/inbound/telegram/route.ts +76 -0
  42. package/src/app/api/channels/route.ts +71 -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/environment/profiles/suggest/route.ts +19 -3
  46. package/src/app/api/environment/scan/route.ts +8 -1
  47. package/src/app/api/handoffs/[id]/route.ts +76 -0
  48. package/src/app/api/handoffs/route.ts +89 -0
  49. package/src/app/api/memory/route.ts +181 -0
  50. package/src/app/api/profiles/[id]/route.ts +16 -1
  51. package/src/app/api/profiles/[id]/test/route.ts +4 -0
  52. package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
  53. package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
  54. package/src/app/api/profiles/assist/route.ts +35 -0
  55. package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
  56. package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
  57. package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
  58. package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
  59. package/src/app/api/profiles/import-repo/route.ts +29 -0
  60. package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
  61. package/src/app/api/profiles/route.ts +73 -22
  62. package/src/app/api/runtimes/ollama/route.ts +86 -0
  63. package/src/app/api/runtimes/suggest/route.ts +29 -0
  64. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  65. package/src/app/api/schedules/[id]/route.ts +41 -3
  66. package/src/app/api/schedules/parse/route.ts +66 -0
  67. package/src/app/api/schedules/route.ts +71 -12
  68. package/src/app/api/settings/author-default/route.ts +7 -0
  69. package/src/app/api/settings/learning/route.ts +41 -0
  70. package/src/app/api/settings/ollama/route.ts +34 -0
  71. package/src/app/api/settings/providers/route.ts +57 -0
  72. package/src/app/api/settings/routing/route.ts +24 -0
  73. package/src/app/api/settings/web-search/route.ts +28 -0
  74. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  75. package/src/app/documents/page.tsx +3 -0
  76. package/src/app/environment/page.tsx +8 -1
  77. package/src/app/settings/page.tsx +10 -4
  78. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  79. package/src/app/workflows/new/page.tsx +2 -0
  80. package/src/components/chat/chat-command-popover.tsx +22 -19
  81. package/src/components/chat/chat-input.tsx +5 -0
  82. package/src/components/chat/chat-model-selector.tsx +42 -1
  83. package/src/components/chat/chat-shell.tsx +2 -0
  84. package/src/components/dashboard/welcome-landing.tsx +9 -9
  85. package/src/components/environment/artifact-card.tsx +27 -1
  86. package/src/components/environment/environment-dashboard.tsx +50 -2
  87. package/src/components/environment/environment-summary-card.tsx +5 -2
  88. package/src/components/environment/suggested-profiles.tsx +117 -52
  89. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  90. package/src/components/memory/memory-browser.tsx +315 -0
  91. package/src/components/profiles/learned-context-panel.tsx +4 -4
  92. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  93. package/src/components/profiles/profile-browser.tsx +109 -8
  94. package/src/components/profiles/profile-card.tsx +29 -1
  95. package/src/components/profiles/profile-detail-view.tsx +200 -28
  96. package/src/components/profiles/profile-form-view.tsx +220 -82
  97. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  98. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  99. package/src/components/schedules/schedule-create-sheet.tsx +9 -1
  100. package/src/components/schedules/schedule-form.tsx +348 -9
  101. package/src/components/schedules/schedule-list.tsx +15 -2
  102. package/src/components/settings/auth-method-selector.tsx +7 -1
  103. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  104. package/src/components/settings/channels-section.tsx +526 -0
  105. package/src/components/settings/chat-settings-section.tsx +27 -1
  106. package/src/components/settings/data-management-section.tsx +8 -6
  107. package/src/components/settings/learning-context-section.tsx +124 -0
  108. package/src/components/settings/ollama-section.tsx +270 -0
  109. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  110. package/src/components/settings/web-search-section.tsx +101 -0
  111. package/src/components/shared/tag-input.tsx +156 -0
  112. package/src/components/tasks/kanban-board.tsx +32 -0
  113. package/src/components/tasks/kanban-column.tsx +4 -2
  114. package/src/components/tasks/task-card.tsx +1 -0
  115. package/src/components/tasks/task-chip-bar.tsx +6 -1
  116. package/src/components/tasks/task-create-panel.tsx +55 -5
  117. package/src/components/workflows/workflow-form-view.tsx +38 -3
  118. package/src/hooks/use-chat-autocomplete.ts +24 -26
  119. package/src/hooks/use-project-skills.ts +66 -0
  120. package/src/hooks/use-tag-suggestions.ts +31 -0
  121. package/src/instrumentation.ts +4 -1
  122. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  123. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  124. package/src/lib/agents/agentic-loop.ts +235 -0
  125. package/src/lib/agents/browser-mcp.ts +59 -4
  126. package/src/lib/agents/claude-agent.ts +26 -199
  127. package/src/lib/agents/handoff/bus.ts +164 -0
  128. package/src/lib/agents/handoff/governance.ts +47 -0
  129. package/src/lib/agents/handoff/types.ts +16 -0
  130. package/src/lib/agents/learned-context.ts +27 -7
  131. package/src/lib/agents/memory/decay.ts +61 -0
  132. package/src/lib/agents/memory/extractor.ts +181 -0
  133. package/src/lib/agents/memory/retrieval.ts +96 -0
  134. package/src/lib/agents/memory/types.ts +6 -0
  135. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  136. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  137. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  138. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  139. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  140. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  141. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  142. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  143. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  144. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  145. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  146. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  147. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  148. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  149. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  150. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  151. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  152. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  153. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  154. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  155. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  156. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  157. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  158. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  159. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  160. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  162. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  163. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  164. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  165. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  166. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  168. package/src/lib/agents/profiles/registry.ts +130 -6
  169. package/src/lib/agents/profiles/types.ts +28 -0
  170. package/src/lib/agents/router.ts +174 -2
  171. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  172. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  173. package/src/lib/agents/runtime/catalog.ts +57 -2
  174. package/src/lib/agents/runtime/claude.ts +205 -1
  175. package/src/lib/agents/runtime/index.ts +22 -0
  176. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  177. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  178. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  179. package/src/lib/agents/runtime/types.ts +2 -0
  180. package/src/lib/agents/tool-permissions.ts +203 -0
  181. package/src/lib/channels/gateway.ts +321 -0
  182. package/src/lib/channels/poller.ts +268 -0
  183. package/src/lib/channels/registry.ts +90 -0
  184. package/src/lib/channels/slack-adapter.ts +188 -0
  185. package/src/lib/channels/telegram-adapter.ts +218 -0
  186. package/src/lib/channels/types.ts +43 -0
  187. package/src/lib/channels/webhook-adapter.ts +74 -0
  188. package/src/lib/chat/context-builder.ts +22 -2
  189. package/src/lib/chat/engine.ts +95 -13
  190. package/src/lib/chat/ollama-engine.ts +198 -0
  191. package/src/lib/chat/stagent-tools.ts +106 -20
  192. package/src/lib/chat/tool-catalog.ts +24 -0
  193. package/src/lib/chat/tool-registry.ts +90 -0
  194. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  195. package/src/lib/chat/tools/document-tools.ts +7 -7
  196. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  197. package/src/lib/chat/tools/notification-tools.ts +4 -4
  198. package/src/lib/chat/tools/profile-tools.ts +3 -3
  199. package/src/lib/chat/tools/project-tools.ts +3 -3
  200. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  201. package/src/lib/chat/tools/settings-tools.ts +2 -2
  202. package/src/lib/chat/tools/task-tools.ts +66 -11
  203. package/src/lib/chat/tools/usage-tools.ts +2 -2
  204. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  205. package/src/lib/chat/types.ts +11 -5
  206. package/src/lib/constants/known-tools.ts +19 -0
  207. package/src/lib/constants/prose-styles.ts +1 -1
  208. package/src/lib/constants/settings.ts +7 -0
  209. package/src/lib/data/channel-bindings.ts +85 -0
  210. package/src/lib/data/clear.ts +22 -0
  211. package/src/lib/data/profile-test-results.ts +48 -0
  212. package/src/lib/data/seed-data/conversations.ts +196 -0
  213. package/src/lib/data/seed-data/learned-context.ts +99 -0
  214. package/src/lib/data/seed-data/notifications.ts +54 -1
  215. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  216. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  217. package/src/lib/data/seed-data/views.ts +60 -0
  218. package/src/lib/data/seed.ts +51 -0
  219. package/src/lib/db/bootstrap.ts +162 -0
  220. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  221. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  222. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  223. package/src/lib/db/schema.ts +187 -1
  224. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  225. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  226. package/src/lib/environment/auto-scan.ts +48 -0
  227. package/src/lib/environment/data.ts +25 -0
  228. package/src/lib/environment/profile-generator.ts +40 -10
  229. package/src/lib/environment/profile-linker.ts +143 -0
  230. package/src/lib/environment/profile-rules.ts +96 -0
  231. package/src/lib/import/dedup.ts +149 -0
  232. package/src/lib/import/format-adapter.ts +631 -0
  233. package/src/lib/import/github-api.ts +219 -0
  234. package/src/lib/import/repo-scanner.ts +251 -0
  235. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  236. package/src/lib/schedules/active-hours.ts +120 -0
  237. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  238. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  239. package/src/lib/schedules/nlp-parser.ts +357 -0
  240. package/src/lib/schedules/scheduler.ts +218 -3
  241. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  242. package/src/lib/settings/helpers.ts +6 -0
  243. package/src/lib/settings/routing.ts +24 -0
  244. package/src/lib/settings/runtime-setup.ts +28 -1
  245. package/src/lib/usage/ledger.ts +2 -1
  246. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  247. package/src/lib/validators/profile.ts +39 -0
  248. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  249. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  250. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  251. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  252. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Ollama chat engine — streams messages via the Ollama /api/chat endpoint.
3
+ *
4
+ * Follows the same ChatStreamEvent protocol as the main engine
5
+ * so the chat UI can render Ollama responses identically.
6
+ */
7
+
8
+ import { db } from "@/lib/db";
9
+ import { chatMessages, projects } from "@/lib/db/schema";
10
+ import { eq } from "drizzle-orm";
11
+ import { getSetting } from "@/lib/settings/helpers";
12
+ import { SETTINGS_KEYS } from "@/lib/constants/settings";
13
+ import {
14
+ getConversation,
15
+ addMessage,
16
+ updateMessageStatus,
17
+ updateMessageContent,
18
+ } from "@/lib/data/chat";
19
+ import { buildChatContext } from "./context-builder";
20
+ import { getWorkspaceContext } from "@/lib/environment/workspace-context";
21
+ import type { ChatStreamEvent } from "./types";
22
+
23
+ /**
24
+ * Send a user message to Ollama and stream the response.
25
+ */
26
+ export async function* sendOllamaMessage(
27
+ conversationId: string,
28
+ userContent: string,
29
+ signal?: AbortSignal
30
+ ): AsyncGenerator<ChatStreamEvent> {
31
+ const conversation = await getConversation(conversationId);
32
+ if (!conversation) {
33
+ yield { type: "error", message: "Conversation not found" };
34
+ return;
35
+ }
36
+
37
+ yield { type: "status", phase: "preparing", message: "Connecting to Ollama..." };
38
+
39
+ // Resolve Ollama base URL and model
40
+ const baseUrl =
41
+ (await getSetting(SETTINGS_KEYS.OLLAMA_BASE_URL)) || "http://localhost:11434";
42
+ const modelId =
43
+ conversation.modelId?.replace(/^ollama:/, "") ||
44
+ (await getSetting(SETTINGS_KEYS.OLLAMA_DEFAULT_MODEL)) ||
45
+ "llama3.2";
46
+
47
+ // Build context
48
+ let projectName: string | null = null;
49
+ let projectCwd: string | null = null;
50
+ if (conversation.projectId) {
51
+ const project = db
52
+ .select()
53
+ .from(projects)
54
+ .where(eq(projects.id, conversation.projectId))
55
+ .get();
56
+ if (project) {
57
+ projectName = project.name;
58
+ projectCwd = project.workingDirectory ?? null;
59
+ }
60
+ }
61
+
62
+ const workspace = getWorkspaceContext();
63
+ if (projectCwd) workspace.cwd = projectCwd;
64
+
65
+ const context = await buildChatContext({
66
+ conversationId,
67
+ projectId: conversation.projectId,
68
+ projectName,
69
+ workspace,
70
+ });
71
+
72
+ // Persist user message
73
+ await addMessage({
74
+ conversationId,
75
+ role: "user",
76
+ content: userContent,
77
+ status: "complete",
78
+ });
79
+
80
+ // Create assistant message placeholder
81
+ const assistantMsg = await addMessage({
82
+ conversationId,
83
+ role: "assistant",
84
+ content: "",
85
+ status: "streaming",
86
+ });
87
+
88
+ // Build message history for Ollama
89
+ const history = db
90
+ .select()
91
+ .from(chatMessages)
92
+ .where(eq(chatMessages.conversationId, conversationId))
93
+ .orderBy(chatMessages.createdAt)
94
+ .all();
95
+
96
+ const messages = [
97
+ // System prompt from context
98
+ ...(context.systemPrompt
99
+ ? [{ role: "system" as const, content: context.systemPrompt }]
100
+ : []),
101
+ // Conversation history (exclude the placeholder assistant msg)
102
+ ...history
103
+ .filter((m) => m.id !== assistantMsg.id && m.content)
104
+ .map((m) => ({
105
+ role: m.role as "user" | "assistant" | "system",
106
+ content: m.content!,
107
+ })),
108
+ ];
109
+
110
+ // Stream from Ollama
111
+ let accumulated = "";
112
+ try {
113
+ const response = await fetch(`${baseUrl}/api/chat`, {
114
+ method: "POST",
115
+ headers: { "Content-Type": "application/json" },
116
+ body: JSON.stringify({
117
+ model: modelId,
118
+ messages,
119
+ stream: true,
120
+ }),
121
+ signal,
122
+ });
123
+
124
+ if (!response.ok) {
125
+ const errorText = await response.text().catch(() => "Unknown error");
126
+ yield { type: "error", message: `Ollama error (${response.status}): ${errorText}` };
127
+ await updateMessageStatus(assistantMsg.id, "complete");
128
+ return;
129
+ }
130
+
131
+ const reader = response.body?.getReader();
132
+ if (!reader) {
133
+ yield { type: "error", message: "No response stream from Ollama" };
134
+ await updateMessageStatus(assistantMsg.id, "complete");
135
+ return;
136
+ }
137
+
138
+ yield { type: "status", phase: "streaming", message: "Streaming response..." };
139
+
140
+ const decoder = new TextDecoder();
141
+ let buffer = "";
142
+
143
+ while (true) {
144
+ const { done, value } = await reader.read();
145
+ if (done) break;
146
+
147
+ buffer += decoder.decode(value, { stream: true });
148
+ const lines = buffer.split("\n");
149
+ buffer = lines.pop() ?? "";
150
+
151
+ for (const line of lines) {
152
+ if (!line.trim()) continue;
153
+ try {
154
+ const parsed = JSON.parse(line);
155
+ const delta = parsed.message?.content ?? "";
156
+ if (delta) {
157
+ accumulated += delta;
158
+ yield { type: "delta", content: delta };
159
+ }
160
+ if (parsed.done) break;
161
+ } catch {
162
+ // Skip malformed lines
163
+ }
164
+ }
165
+ }
166
+
167
+ // Process any remaining buffer
168
+ if (buffer.trim()) {
169
+ try {
170
+ const parsed = JSON.parse(buffer);
171
+ const delta = parsed.message?.content ?? "";
172
+ if (delta) {
173
+ accumulated += delta;
174
+ yield { type: "delta", content: delta };
175
+ }
176
+ } catch {
177
+ // ignore
178
+ }
179
+ }
180
+
181
+ // Persist the complete response
182
+ await updateMessageContent(assistantMsg.id, accumulated);
183
+ await updateMessageStatus(assistantMsg.id, "complete");
184
+
185
+ yield { type: "done", messageId: assistantMsg.id, quickAccess: [] };
186
+ } catch (err) {
187
+ if (signal?.aborted) {
188
+ yield { type: "error", message: "Request cancelled" };
189
+ } else {
190
+ const msg = err instanceof Error ? err.message : "Ollama streaming failed";
191
+ yield { type: "error", message: msg };
192
+ }
193
+ if (accumulated) {
194
+ await updateMessageContent(assistantMsg.id, accumulated);
195
+ }
196
+ await updateMessageStatus(assistantMsg.id, "complete");
197
+ }
198
+ }
@@ -1,5 +1,13 @@
1
- import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
1
+ import { tool as sdkTool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { type ToolContext } from "./tools/helpers";
3
+ import {
4
+ type ToolDefinition,
5
+ type ToolResult,
6
+ toAnthropicToolDef,
7
+ toOpenAIFunctionDef,
8
+ type AnthropicToolDef,
9
+ type OpenAIFunctionDef,
10
+ } from "./tool-registry";
3
11
  import { projectTools } from "./tools/project-tools";
4
12
  import { taskTools } from "./tools/task-tools";
5
13
  import { workflowTools } from "./tools/workflow-tools";
@@ -10,6 +18,98 @@ import { profileTools } from "./tools/profile-tools";
10
18
  import { usageTools } from "./tools/usage-tools";
11
19
  import { settingsTools } from "./tools/settings-tools";
12
20
  import { chatHistoryTools } from "./tools/chat-history-tools";
21
+ import { handoffTools } from "./tools/handoff-tools";
22
+
23
+ // ── Tool server types ────────────────────────────────────────────────
24
+
25
+ export interface ProviderToolKit {
26
+ tools: AnthropicToolDef[] | OpenAIFunctionDef[];
27
+ /** Execute a tool handler by name. Throws if tool not found. */
28
+ executeHandler(name: string, args: Record<string, unknown>): Promise<ToolResult>;
29
+ }
30
+
31
+ export interface ToolServer {
32
+ /** Backward-compatible SDK MCP server for the chat engine. */
33
+ asMcpServer(): ReturnType<typeof createSdkMcpServer>;
34
+ /** Provider-formatted tool arrays + handler lookup for direct API runtimes. */
35
+ forProvider(provider: "anthropic" | "openai"): ProviderToolKit;
36
+ /** Raw tool definitions for inspection / testing. */
37
+ readonly definitions: ToolDefinition[];
38
+ }
39
+
40
+ // ── Factory ──────────────────────────────────────────────────────────
41
+
42
+ function collectAllTools(ctx: ToolContext): ToolDefinition[] {
43
+ return [
44
+ ...projectTools(ctx),
45
+ ...taskTools(ctx),
46
+ ...workflowTools(ctx),
47
+ ...scheduleTools(ctx),
48
+ ...documentTools(ctx),
49
+ ...notificationTools(ctx),
50
+ ...profileTools(ctx),
51
+ ...usageTools(ctx),
52
+ ...settingsTools(ctx),
53
+ ...chatHistoryTools(ctx),
54
+ ...handoffTools(ctx),
55
+ ];
56
+ }
57
+
58
+ /**
59
+ * Create a tool server that supports both SDK MCP mode and direct API mode.
60
+ *
61
+ * - `.asMcpServer()` re-wraps definitions into SDK `tool()` calls for
62
+ * backward compatibility with the chat engine.
63
+ * - `.forProvider("anthropic" | "openai")` returns provider-formatted
64
+ * tool definitions + a handler lookup for direct API runtimes.
65
+ */
66
+ export function createToolServer(
67
+ projectId?: string | null,
68
+ onToolResult?: (toolName: string, result: unknown) => void,
69
+ ): ToolServer {
70
+ const ctx: ToolContext = { projectId, onToolResult };
71
+ const allTools = collectAllTools(ctx);
72
+
73
+ // Handler lookup map (built once, shared across modes)
74
+ const handlerMap = new Map<string, ToolDefinition["handler"]>(
75
+ allTools.map((t) => [t.name, t.handler]),
76
+ );
77
+
78
+ async function executeHandler(
79
+ name: string,
80
+ args: Record<string, unknown>,
81
+ ): Promise<ToolResult> {
82
+ const handler = handlerMap.get(name);
83
+ if (!handler) throw new Error(`Unknown tool: ${name}`);
84
+ return handler(args);
85
+ }
86
+
87
+ return {
88
+ asMcpServer() {
89
+ // Re-wrap ToolDefinitions into SDK tool() format
90
+ const sdkTools = allTools.map((def) =>
91
+ sdkTool(def.name, def.description, def.zodShape, def.handler),
92
+ );
93
+ return createSdkMcpServer({
94
+ name: "stagent",
95
+ version: "1.0.0",
96
+ tools: sdkTools,
97
+ });
98
+ },
99
+
100
+ forProvider(provider) {
101
+ const tools =
102
+ provider === "anthropic"
103
+ ? allTools.map(toAnthropicToolDef)
104
+ : allTools.map(toOpenAIFunctionDef);
105
+ return { tools, executeHandler };
106
+ },
107
+
108
+ definitions: allTools,
109
+ };
110
+ }
111
+
112
+ // ── Backward-compatible export ───────────────────────────────────────
13
113
 
14
114
  /**
15
115
  * Create an in-process MCP server exposing all Stagent tools.
@@ -17,27 +117,13 @@ import { chatHistoryTools } from "./tools/chat-history-tools";
17
117
  * `onToolResult` is called after each successful CRUD operation with the
18
118
  * tool name and returned entity data — used by the entity detector to
19
119
  * generate deterministic Quick Access navigation links.
120
+ *
121
+ * @deprecated Use `createToolServer()` for new code. This wrapper exists
122
+ * for backward compatibility with the chat engine.
20
123
  */
21
124
  export function createStagentMcpServer(
22
125
  projectId?: string | null,
23
- onToolResult?: (toolName: string, result: unknown) => void
126
+ onToolResult?: (toolName: string, result: unknown) => void,
24
127
  ) {
25
- const ctx: ToolContext = { projectId, onToolResult };
26
-
27
- return createSdkMcpServer({
28
- name: "stagent",
29
- version: "1.0.0",
30
- tools: [
31
- ...projectTools(ctx),
32
- ...taskTools(ctx),
33
- ...workflowTools(ctx),
34
- ...scheduleTools(ctx),
35
- ...documentTools(ctx),
36
- ...notificationTools(ctx),
37
- ...profileTools(ctx),
38
- ...usageTools(ctx),
39
- ...settingsTools(ctx),
40
- ...chatHistoryTools(ctx),
41
- ],
42
- });
128
+ return createToolServer(projectId, onToolResult).asMcpServer();
43
129
  }
@@ -12,6 +12,7 @@ import {
12
12
  Globe,
13
13
  Sun,
14
14
  CheckCheck,
15
+ Sparkles,
15
16
  } from "lucide-react";
16
17
  import type { LucideIcon } from "lucide-react";
17
18
 
@@ -25,6 +26,7 @@ export type ToolGroup =
25
26
  | "Documents"
26
27
  | "Notifications"
27
28
  | "Profiles"
29
+ | "Skills"
28
30
  | "Usage"
29
31
  | "Settings"
30
32
  | "Chat"
@@ -54,6 +56,7 @@ export const TOOL_GROUP_ICONS: Record<ToolGroup, LucideIcon> = {
54
56
  Documents: FileText,
55
57
  Notifications: Bell,
56
58
  Profiles: Bot,
59
+ Skills: Sparkles,
57
60
  Usage: Wallet,
58
61
  Settings: Settings,
59
62
  Chat: MessageSquare,
@@ -69,6 +72,7 @@ export const TOOL_GROUP_ORDER: ToolGroup[] = [
69
72
  "Documents",
70
73
  "Schedules",
71
74
  "Profiles",
75
+ "Skills",
72
76
  "Browser",
73
77
  "Notifications",
74
78
  "Chat",
@@ -174,6 +178,26 @@ export function getToolCatalog(opts?: { includeBrowser?: boolean }): ToolCatalog
174
178
  return cachedCatalog;
175
179
  }
176
180
 
181
+ /**
182
+ * Get the tool catalog with dynamic project skills appended.
183
+ * NOT cached at module level because it depends on the active project.
184
+ */
185
+ export function getToolCatalogWithSkills(opts?: {
186
+ includeBrowser?: boolean;
187
+ projectProfiles?: Array<{ id: string; name: string; description: string }>;
188
+ }): ToolCatalogEntry[] {
189
+ const base = getToolCatalog(opts);
190
+ if (!opts?.projectProfiles?.length) return base;
191
+
192
+ const skillEntries: ToolCatalogEntry[] = opts.projectProfiles.map((p) => ({
193
+ name: p.id,
194
+ description: p.description,
195
+ group: "Skills" as ToolGroup,
196
+ }));
197
+
198
+ return [...base, ...skillEntries];
199
+ }
200
+
177
201
  /** Group catalog entries by their ToolGroup */
178
202
  export function groupToolCatalog(entries: ToolCatalogEntry[]): Record<string, ToolCatalogEntry[]> {
179
203
  const groups: Record<string, ToolCatalogEntry[]> = {};
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Provider-agnostic tool definition registry.
3
+ *
4
+ * `defineTool()` replaces the SDK's `tool()` so that tool definitions
5
+ * are decoupled from any specific provider SDK. The resulting
6
+ * `ToolDefinition` objects carry both the original Zod schema (for
7
+ * backward-compatible SDK wrapping) and a pre-computed JSON Schema
8
+ * (for direct API runtimes).
9
+ */
10
+
11
+ import { z } from "zod";
12
+
13
+ // ── Types ────────────────────────────────────────────────────────────
14
+
15
+ /** MCP-compatible tool result (matches ok/err helper output). */
16
+ export interface ToolResult {
17
+ content: Array<{ type: "text"; text: string }>;
18
+ isError?: boolean;
19
+ }
20
+
21
+ /** Provider-neutral tool definition (type-erased for storage in arrays). */
22
+ export interface ToolDefinition {
23
+ name: string;
24
+ description: string;
25
+ /** Original Zod shape — retained for SDK bridge wrapping. */
26
+ zodShape: z.ZodRawShape;
27
+ /** Pre-computed JSON Schema (Draft 2020-12) for direct API runtimes. */
28
+ inputSchema: Record<string, unknown>;
29
+ /** Async handler that receives validated args and returns MCP content. */
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ handler: (args: any) => Promise<ToolResult>;
32
+ }
33
+
34
+ // ── Factory ──────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Define a provider-agnostic tool.
38
+ *
39
+ * Signature mirrors the SDK's `tool(name, description, zodShape, handler)`
40
+ * so that converting existing tool files is a simple import swap.
41
+ */
42
+ export function defineTool<T extends z.ZodRawShape>(
43
+ name: string,
44
+ description: string,
45
+ inputShape: T,
46
+ handler: (args: z.infer<z.ZodObject<T>>) => Promise<ToolResult>,
47
+ ): ToolDefinition {
48
+ const zodObject = z.object(inputShape);
49
+ const inputSchema = z.toJSONSchema(zodObject) as Record<string, unknown>;
50
+
51
+ return { name, description, zodShape: inputShape, inputSchema, handler };
52
+ }
53
+
54
+ // ── Provider formatters ──────────────────────────────────────────────
55
+
56
+ /** Format for Anthropic Messages API `tools` parameter. */
57
+ export interface AnthropicToolDef {
58
+ name: string;
59
+ description: string;
60
+ input_schema: Record<string, unknown>;
61
+ }
62
+
63
+ export function toAnthropicToolDef(def: ToolDefinition): AnthropicToolDef {
64
+ return {
65
+ name: def.name,
66
+ description: def.description,
67
+ input_schema: def.inputSchema,
68
+ };
69
+ }
70
+
71
+ /** Format for OpenAI Responses API function tool. */
72
+ export interface OpenAIFunctionDef {
73
+ type: "function";
74
+ function: {
75
+ name: string;
76
+ description: string;
77
+ parameters: Record<string, unknown>;
78
+ };
79
+ }
80
+
81
+ export function toOpenAIFunctionDef(def: ToolDefinition): OpenAIFunctionDef {
82
+ return {
83
+ type: "function",
84
+ function: {
85
+ name: def.name,
86
+ description: def.description,
87
+ parameters: def.inputSchema,
88
+ },
89
+ };
90
+ }
@@ -1,4 +1,4 @@
1
- import { tool } from "@anthropic-ai/claude-agent-sdk";
1
+ import { defineTool } from "../tool-registry";
2
2
  import { z } from "zod";
3
3
  import { ok, err, type ToolContext } from "./helpers";
4
4
  import {
@@ -11,7 +11,7 @@ import {
11
11
  export function chatHistoryTools(ctx: ToolContext) {
12
12
  return [
13
13
  // ── list_conversations ──────────────────────────────────────────
14
- tool(
14
+ defineTool(
15
15
  "list_conversations",
16
16
  "List recent chat conversations. Use to find past discussions, filter by project or status, or search titles.",
17
17
  {
@@ -59,7 +59,7 @@ export function chatHistoryTools(ctx: ToolContext) {
59
59
  ),
60
60
 
61
61
  // ── get_conversation_messages ────────────────────────────────────
62
- tool(
62
+ defineTool(
63
63
  "get_conversation_messages",
64
64
  "Get message history from a past conversation. Use to recall what was discussed, review decisions, or find specific information from a prior chat.",
65
65
  {
@@ -119,7 +119,7 @@ export function chatHistoryTools(ctx: ToolContext) {
119
119
  ),
120
120
 
121
121
  // ── search_messages ─────────────────────────────────────────────
122
- tool(
122
+ defineTool(
123
123
  "search_messages",
124
124
  "Search across all conversations for specific content. Use when the user asks about prior discussions, decisions, or any topic from past chats.",
125
125
  {
@@ -1,4 +1,4 @@
1
- import { tool } from "@anthropic-ai/claude-agent-sdk";
1
+ import { defineTool } from "../tool-registry";
2
2
  import { z } from "zod";
3
3
  import { db } from "@/lib/db";
4
4
  import { documents } from "@/lib/db/schema";
@@ -44,7 +44,7 @@ function resolveMimeType(filename: string): string {
44
44
 
45
45
  export function documentTools(ctx: ToolContext) {
46
46
  return [
47
- tool(
47
+ defineTool(
48
48
  "list_documents",
49
49
  "List documents, optionally filtered by project, task, direction, or status.",
50
50
  {
@@ -97,7 +97,7 @@ export function documentTools(ctx: ToolContext) {
97
97
  }
98
98
  ),
99
99
 
100
- tool(
100
+ defineTool(
101
101
  "get_document",
102
102
  "Get metadata for a specific document (does not return file content).",
103
103
  {
@@ -134,7 +134,7 @@ export function documentTools(ctx: ToolContext) {
134
134
  }
135
135
  ),
136
136
 
137
- tool(
137
+ defineTool(
138
138
  "upload_document",
139
139
  "Upload a file from the filesystem as a document. Use this to register files you create as documents in the Documents library. The file is copied to Stagent storage and queued for preprocessing (text extraction).",
140
140
  {
@@ -194,7 +194,7 @@ export function documentTools(ctx: ToolContext) {
194
194
  }
195
195
  ),
196
196
 
197
- tool(
197
+ defineTool(
198
198
  "update_document",
199
199
  "Update document metadata or trigger reprocessing. Metadata is merged with existing values, not replaced.",
200
200
  {
@@ -255,7 +255,7 @@ export function documentTools(ctx: ToolContext) {
255
255
  }
256
256
  ),
257
257
 
258
- tool(
258
+ defineTool(
259
259
  "delete_document",
260
260
  "Delete a document. If the document is linked to a task, you must set cascadeDelete to true to confirm deletion.",
261
261
  {
@@ -300,7 +300,7 @@ export function documentTools(ctx: ToolContext) {
300
300
  }
301
301
  }
302
302
  ),
303
- tool(
303
+ defineTool(
304
304
  "read_document_content",
305
305
  "Read the full extracted text content of a document. Use this when you need to analyze, summarize, or answer questions about a document's contents.",
306
306
  {
@@ -0,0 +1,70 @@
1
+ import { defineTool } from "../tool-registry";
2
+ import { z } from "zod";
3
+ import { ok, err, type ToolContext } from "./helpers";
4
+ import { sendHandoff } from "@/lib/agents/handoff/bus";
5
+
6
+ export function handoffTools(_ctx: ToolContext) {
7
+ return [
8
+ defineTool(
9
+ "send_handoff",
10
+ "Hand off a task from one agent profile to another. Creates an async handoff request that the target agent will pick up. Use this when a task requires expertise from a different agent profile.",
11
+ {
12
+ toProfile: z
13
+ .string()
14
+ .describe("Target agent profile ID (e.g. code-reviewer, researcher, document-writer)"),
15
+ subject: z
16
+ .string()
17
+ .min(1)
18
+ .max(200)
19
+ .describe("Brief subject line for the handoff"),
20
+ body: z
21
+ .string()
22
+ .min(1)
23
+ .max(4000)
24
+ .describe("Detailed description of what the target agent should do"),
25
+ fromProfile: z
26
+ .string()
27
+ .optional()
28
+ .describe("Source agent profile ID. Defaults to 'general'."),
29
+ sourceTaskId: z
30
+ .string()
31
+ .optional()
32
+ .describe("Source task ID that initiated this handoff"),
33
+ priority: z
34
+ .number()
35
+ .min(0)
36
+ .max(3)
37
+ .optional()
38
+ .describe("Priority: 0 = critical, 1 = high, 2 = medium (default), 3 = low"),
39
+ requiresApproval: z
40
+ .boolean()
41
+ .optional()
42
+ .describe("Whether human approval is required before the handoff is processed. Defaults to false."),
43
+ },
44
+ async (args) => {
45
+ try {
46
+ const messageId = await sendHandoff({
47
+ fromProfileId: args.fromProfile ?? "general",
48
+ toProfileId: args.toProfile,
49
+ sourceTaskId: args.sourceTaskId ?? "",
50
+ subject: args.subject,
51
+ body: args.body,
52
+ priority: args.priority,
53
+ requiresApproval: args.requiresApproval,
54
+ });
55
+
56
+ return ok({
57
+ message: "Handoff created successfully",
58
+ messageId,
59
+ from: args.fromProfile ?? "general",
60
+ to: args.toProfile,
61
+ subject: args.subject,
62
+ requiresApproval: args.requiresApproval ?? false,
63
+ });
64
+ } catch (e) {
65
+ return err(e instanceof Error ? e.message : "Failed to create handoff");
66
+ }
67
+ }
68
+ ),
69
+ ];
70
+ }
@@ -1,4 +1,4 @@
1
- import { tool } from "@anthropic-ai/claude-agent-sdk";
1
+ import { defineTool } from "../tool-registry";
2
2
  import { z } from "zod";
3
3
  import { db } from "@/lib/db";
4
4
  import { notifications } from "@/lib/db/schema";
@@ -7,7 +7,7 @@ import { ok, err, type ToolContext } from "./helpers";
7
7
 
8
8
  export function notificationTools(_ctx: ToolContext) {
9
9
  return [
10
- tool(
10
+ defineTool(
11
11
  "list_notifications",
12
12
  "List notifications. By default shows only pending approval requests. Set pendingOnly to false for all recent notifications.",
13
13
  {
@@ -58,7 +58,7 @@ export function notificationTools(_ctx: ToolContext) {
58
58
  }
59
59
  ),
60
60
 
61
- tool(
61
+ defineTool(
62
62
  "respond_notification",
63
63
  "Respond to a pending permission or approval notification. Use 'allow' to approve or 'deny' to reject.",
64
64
  {
@@ -124,7 +124,7 @@ export function notificationTools(_ctx: ToolContext) {
124
124
  }
125
125
  ),
126
126
 
127
- tool(
127
+ defineTool(
128
128
  "mark_notifications_read",
129
129
  "Mark all unread notifications as read.",
130
130
  {},