@4onstudios/iris-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +690 -0
  3. package/dist/api/acp/acpServer.d.ts +23 -0
  4. package/dist/api/acp/acpServer.js +814 -0
  5. package/dist/api/acp/index.d.ts +2 -0
  6. package/dist/api/acp/index.js +2 -0
  7. package/dist/api/acp/irisClient.d.ts +58 -0
  8. package/dist/api/acp/irisClient.js +214 -0
  9. package/dist/api/agent.d.ts +14 -0
  10. package/dist/api/agent.js +4171 -0
  11. package/dist/api/backendService.d.ts +4 -0
  12. package/dist/api/backendService.js +14 -0
  13. package/dist/api/core/agent/host/AgentContract.d.ts +82 -0
  14. package/dist/api/core/agent/host/AgentContract.js +1 -0
  15. package/dist/api/core/agent/host/AgentRegistry.d.ts +24 -0
  16. package/dist/api/core/agent/host/AgentRegistry.js +125 -0
  17. package/dist/api/core/agent/host/defaultRegistry.d.ts +15 -0
  18. package/dist/api/core/agent/host/defaultRegistry.js +46 -0
  19. package/dist/api/core/agent/host/externalAgentLifecycle.d.ts +65 -0
  20. package/dist/api/core/agent/host/externalAgentLifecycle.js +117 -0
  21. package/dist/api/core/agent/host/hostSessionManager.d.ts +111 -0
  22. package/dist/api/core/agent/host/hostSessionManager.js +352 -0
  23. package/dist/api/core/agent/host/index.d.ts +5 -0
  24. package/dist/api/core/agent/host/index.js +5 -0
  25. package/dist/api/core/agent/index.d.ts +87 -0
  26. package/dist/api/core/agent/index.js +1024 -0
  27. package/dist/api/core/agent/publicContracts.d.ts +35 -0
  28. package/dist/api/core/agent/publicContracts.js +103 -0
  29. package/dist/api/core/agent/tools/applyDiff.d.ts +51 -0
  30. package/dist/api/core/agent/tools/applyDiff.js +131 -0
  31. package/dist/api/core/agent/tools/backgroundTasks.d.ts +240 -0
  32. package/dist/api/core/agent/tools/backgroundTasks.js +313 -0
  33. package/dist/api/core/agent/tools/createDirectory.d.ts +43 -0
  34. package/dist/api/core/agent/tools/createDirectory.js +77 -0
  35. package/dist/api/core/agent/tools/deleteFile.d.ts +37 -0
  36. package/dist/api/core/agent/tools/deleteFile.js +66 -0
  37. package/dist/api/core/agent/tools/editFile.d.ts +73 -0
  38. package/dist/api/core/agent/tools/editFile.js +173 -0
  39. package/dist/api/core/agent/tools/executeCommand.d.ts +59 -0
  40. package/dist/api/core/agent/tools/executeCommand.js +250 -0
  41. package/dist/api/core/agent/tools/fileContent.d.ts +54 -0
  42. package/dist/api/core/agent/tools/fileContent.js +227 -0
  43. package/dist/api/core/agent/tools/findDefinition.d.ts +72 -0
  44. package/dist/api/core/agent/tools/findDefinition.js +109 -0
  45. package/dist/api/core/agent/tools/findReferences.d.ts +86 -0
  46. package/dist/api/core/agent/tools/findReferences.js +135 -0
  47. package/dist/api/core/agent/tools/formatDocument.d.ts +67 -0
  48. package/dist/api/core/agent/tools/formatDocument.js +90 -0
  49. package/dist/api/core/agent/tools/getCodeActions.d.ts +77 -0
  50. package/dist/api/core/agent/tools/getCodeActions.js +97 -0
  51. package/dist/api/core/agent/tools/getCodeCompletion.d.ts +77 -0
  52. package/dist/api/core/agent/tools/getCodeCompletion.js +174 -0
  53. package/dist/api/core/agent/tools/getCodeContext.d.ts +14 -0
  54. package/dist/api/core/agent/tools/getCodeContext.js +282 -0
  55. package/dist/api/core/agent/tools/getSignatureHelp.d.ts +61 -0
  56. package/dist/api/core/agent/tools/getSignatureHelp.js +85 -0
  57. package/dist/api/core/agent/tools/getSymbols.d.ts +50 -0
  58. package/dist/api/core/agent/tools/getSymbols.js +164 -0
  59. package/dist/api/core/agent/tools/getSymbolsLSP.d.ts +58 -0
  60. package/dist/api/core/agent/tools/getSymbolsLSP.js +147 -0
  61. package/dist/api/core/agent/tools/getTypeInfo.d.ts +70 -0
  62. package/dist/api/core/agent/tools/getTypeInfo.js +84 -0
  63. package/dist/api/core/agent/tools/getWorkspaceInfo.d.ts +80 -0
  64. package/dist/api/core/agent/tools/getWorkspaceInfo.js +281 -0
  65. package/dist/api/core/agent/tools/getWorkspaceSymbols.d.ts +53 -0
  66. package/dist/api/core/agent/tools/getWorkspaceSymbols.js +97 -0
  67. package/dist/api/core/agent/tools/grepSearch.d.ts +100 -0
  68. package/dist/api/core/agent/tools/grepSearch.js +211 -0
  69. package/dist/api/core/agent/tools/languageModelToolsIntegration.d.ts +13 -0
  70. package/dist/api/core/agent/tools/languageModelToolsIntegration.js +22 -0
  71. package/dist/api/core/agent/tools/listDirectory.d.ts +66 -0
  72. package/dist/api/core/agent/tools/listDirectory.js +161 -0
  73. package/dist/api/core/agent/tools/mcpTools.d.ts +48 -0
  74. package/dist/api/core/agent/tools/mcpTools.js +442 -0
  75. package/dist/api/core/agent/tools/queryKnowledgeGraph.d.ts +120 -0
  76. package/dist/api/core/agent/tools/queryKnowledgeGraph.js +306 -0
  77. package/dist/api/core/agent/tools/readFile.d.ts +92 -0
  78. package/dist/api/core/agent/tools/readFile.js +337 -0
  79. package/dist/api/core/agent/tools/renameFile.d.ts +49 -0
  80. package/dist/api/core/agent/tools/renameFile.js +86 -0
  81. package/dist/api/core/agent/tools/renameSymbol.d.ts +70 -0
  82. package/dist/api/core/agent/tools/renameSymbol.js +106 -0
  83. package/dist/api/core/agent/tools/runTerminalCommand.d.ts +72 -0
  84. package/dist/api/core/agent/tools/runTerminalCommand.js +69 -0
  85. package/dist/api/core/agent/tools/searchFiles.d.ts +82 -0
  86. package/dist/api/core/agent/tools/searchFiles.js +280 -0
  87. package/dist/api/core/agent/tools/terminalAutoApprove.d.ts +3 -0
  88. package/dist/api/core/agent/tools/terminalAutoApprove.js +676 -0
  89. package/dist/api/core/agent/tools/treeTraversal.d.ts +60 -0
  90. package/dist/api/core/agent/tools/treeTraversal.js +217 -0
  91. package/dist/api/core/agent/tools/webSearch.d.ts +37 -0
  92. package/dist/api/core/agent/tools/webSearch.js +80 -0
  93. package/dist/api/core/agent/tools/writeFile.d.ts +59 -0
  94. package/dist/api/core/agent/tools/writeFile.js +97 -0
  95. package/dist/api/core/agent/utils/capturedWorkspaceMutationBridge.d.ts +12 -0
  96. package/dist/api/core/agent/utils/capturedWorkspaceMutationBridge.js +67 -0
  97. package/dist/api/core/agent/utils/diffUtils.d.ts +14 -0
  98. package/dist/api/core/agent/utils/diffUtils.js +39 -0
  99. package/dist/api/core/agent/utils/environmentSnapshot.d.ts +42 -0
  100. package/dist/api/core/agent/utils/environmentSnapshot.js +213 -0
  101. package/dist/api/core/agent/utils/errorRecovery.d.ts +49 -0
  102. package/dist/api/core/agent/utils/errorRecovery.js +229 -0
  103. package/dist/api/core/agent/utils/multimodalTokenLimiter.d.ts +8 -0
  104. package/dist/api/core/agent/utils/multimodalTokenLimiter.js +238 -0
  105. package/dist/api/core/agent/utils/openRouterModelSettings.d.ts +12 -0
  106. package/dist/api/core/agent/utils/openRouterModelSettings.js +34 -0
  107. package/dist/api/core/agent/utils/pathRecovery.d.ts +1 -0
  108. package/dist/api/core/agent/utils/pathRecovery.js +29 -0
  109. package/dist/api/core/agent/utils/repoMapIndex.d.ts +3 -0
  110. package/dist/api/core/agent/utils/repoMapIndex.js +197 -0
  111. package/dist/api/core/agent/utils/skillsDiscovery.d.ts +10 -0
  112. package/dist/api/core/agent/utils/skillsDiscovery.js +50 -0
  113. package/dist/api/core/agent/utils/toolCallBudget.d.ts +9 -0
  114. package/dist/api/core/agent/utils/toolCallBudget.js +55 -0
  115. package/dist/api/core/agent/utils/toolLifecycle.d.ts +30 -0
  116. package/dist/api/core/agent/utils/toolLifecycle.js +361 -0
  117. package/dist/api/core/agent/utils/toolResultSafetyProcessor.d.ts +12 -0
  118. package/dist/api/core/agent/utils/toolResultSafetyProcessor.js +57 -0
  119. package/dist/api/core/agent/utils/workspaceMutationBridge.d.ts +13 -0
  120. package/dist/api/core/agent/utils/workspaceMutationBridge.js +149 -0
  121. package/dist/api/core/agent/utils/workspacePathGuard.d.ts +7 -0
  122. package/dist/api/core/agent/utils/workspacePathGuard.js +79 -0
  123. package/dist/api/core/containers/chat/toolResultSerialization.d.ts +8 -0
  124. package/dist/api/core/containers/chat/toolResultSerialization.js +71 -0
  125. package/dist/api/core/library/BrowserManager.d.ts +11 -0
  126. package/dist/api/core/library/BrowserManager.js +66 -0
  127. package/dist/api/core/library/desktopWorkspace.d.ts +1 -0
  128. package/dist/api/core/library/desktopWorkspace.js +3 -0
  129. package/dist/api/core/library/extensionManager.d.ts +10 -0
  130. package/dist/api/core/library/extensionManager.js +27 -0
  131. package/dist/api/core/library/knowledgeGraph.d.ts +268 -0
  132. package/dist/api/core/library/knowledgeGraph.js +989 -0
  133. package/dist/api/core/library/languageModelTools.d.ts +141 -0
  134. package/dist/api/core/library/languageModelTools.js +207 -0
  135. package/dist/api/core/library/localRuntime.d.ts +23 -0
  136. package/dist/api/core/library/localRuntime.js +144 -0
  137. package/dist/api/core/library/lsp/coreLsp.d.ts +367 -0
  138. package/dist/api/core/library/lsp/coreLsp.js +2076 -0
  139. package/dist/api/core/library/lsp/protocol.d.ts +25 -0
  140. package/dist/api/core/library/lsp/protocol.js +1 -0
  141. package/dist/api/core/library/lsp/serverManager.d.ts +37 -0
  142. package/dist/api/core/library/lsp/serverManager.js +427 -0
  143. package/dist/api/core/library/mcpServerProvider.d.ts +69 -0
  144. package/dist/api/core/library/mcpServerProvider.js +152 -0
  145. package/dist/api/core/library/mcpSettings.d.ts +20 -0
  146. package/dist/api/core/library/mcpSettings.js +131 -0
  147. package/dist/api/core/library/patternMatcher.d.ts +66 -0
  148. package/dist/api/core/library/patternMatcher.js +284 -0
  149. package/dist/api/core/library/regexEscape.d.ts +4 -0
  150. package/dist/api/core/library/regexEscape.js +4 -0
  151. package/dist/api/core/library/runtimeEventBus.d.ts +27 -0
  152. package/dist/api/core/library/runtimeEventBus.js +78 -0
  153. package/dist/api/core/library/safetyMiddleware.d.ts +98 -0
  154. package/dist/api/core/library/safetyMiddleware.js +215 -0
  155. package/dist/api/core/library/tauri.d.ts +71 -0
  156. package/dist/api/core/library/tauri.js +222 -0
  157. package/dist/api/core/library/tauriImport.d.ts +5 -0
  158. package/dist/api/core/library/tauriImport.js +7 -0
  159. package/dist/api/core/library/terminalAutoApproveSettings.d.ts +32 -0
  160. package/dist/api/core/library/terminalAutoApproveSettings.js +529 -0
  161. package/dist/api/core/library/workspaceIdentity.d.ts +10 -0
  162. package/dist/api/core/library/workspaceIdentity.js +49 -0
  163. package/dist/api/core/library/workspaceSummary.d.ts +19 -0
  164. package/dist/api/core/library/workspaceSummary.js +35 -0
  165. package/dist/api/core/skills/agent-customization/SKILL.md +27 -0
  166. package/dist/api/core/skills/bug-fix/SKILL.md +21 -0
  167. package/dist/api/core/skills/create-pr/SKILL.md +32 -0
  168. package/dist/api/core/skills/dev-server/SKILL.md +20 -0
  169. package/dist/api/core/skills/fix-suggestions/SKILL.md +21 -0
  170. package/dist/api/core/skills/github-search/SKILL.md +21 -0
  171. package/dist/api/core/skills/github-summary/SKILL.md +25 -0
  172. package/dist/api/core/skills/integration-tests/SKILL.md +29 -0
  173. package/dist/api/core/skills/pr-comments/SKILL.md +23 -0
  174. package/dist/api/core/skills/project-setup/SKILL.md +20 -0
  175. package/dist/api/core/skills/search-integration/SKILL.md +19 -0
  176. package/dist/api/core/skills/search-results/SKILL.md +19 -0
  177. package/dist/api/core/skills/typescript-upgrade/SKILL.md +21 -0
  178. package/dist/api/data/runStore.d.ts +46 -0
  179. package/dist/api/data/runStore.js +244 -0
  180. package/dist/api/helpers/agentUtils.d.ts +11 -0
  181. package/dist/api/helpers/agentUtils.js +23 -0
  182. package/dist/api/helpers/modelTokenLimits.d.ts +2 -0
  183. package/dist/api/helpers/modelTokenLimits.js +158 -0
  184. package/dist/api/helpers/observationalMemory.d.ts +10 -0
  185. package/dist/api/helpers/observationalMemory.js +36 -0
  186. package/dist/api/helpers/promptBudget.d.ts +30 -0
  187. package/dist/api/helpers/promptBudget.js +216 -0
  188. package/dist/api/helpers/resolveImageMessageParts.d.ts +9 -0
  189. package/dist/api/helpers/resolveImageMessageParts.js +140 -0
  190. package/dist/api/helpers/slashCommands.d.ts +31 -0
  191. package/dist/api/helpers/slashCommands.js +129 -0
  192. package/dist/api/helpers/tokenUsage.d.ts +19 -0
  193. package/dist/api/helpers/tokenUsage.js +81 -0
  194. package/dist/api/routes/fileRoutes.d.ts +2 -0
  195. package/dist/api/routes/fileRoutes.js +108 -0
  196. package/dist/api/routes/lspDocumentRoutes.d.ts +4 -0
  197. package/dist/api/routes/lspDocumentRoutes.js +84 -0
  198. package/dist/api/routes/lspHierarchyRoutes.d.ts +4 -0
  199. package/dist/api/routes/lspHierarchyRoutes.js +122 -0
  200. package/dist/api/routes/lspPositionRoutes.d.ts +4 -0
  201. package/dist/api/routes/lspPositionRoutes.js +263 -0
  202. package/dist/api/routes/lspQueryRoutes.d.ts +4 -0
  203. package/dist/api/routes/lspQueryRoutes.js +679 -0
  204. package/dist/api/routes/lspResolveRoutes.d.ts +4 -0
  205. package/dist/api/routes/lspResolveRoutes.js +143 -0
  206. package/dist/api/routes/lspSemanticDocumentRoutes.d.ts +4 -0
  207. package/dist/api/routes/lspSemanticDocumentRoutes.js +98 -0
  208. package/dist/api/routes/semanticRoutes.d.ts +2 -0
  209. package/dist/api/routes/semanticRoutes.js +126 -0
  210. package/dist/cli.d.ts +8 -0
  211. package/dist/cli.js +160 -0
  212. package/dist/index.d.ts +1 -0
  213. package/dist/index.js +1 -0
  214. package/dist/server.d.ts +2 -0
  215. package/dist/server.js +38 -0
  216. package/package.json +100 -0
@@ -0,0 +1,1024 @@
1
+ import { createCodingAgent as createMastraCodingAgent } from "@mastra/core/coding-agent";
2
+ import { webFetchTool as mastraWebFetchTool, webSearchTool as mastraWebSearchTool, } from "@mastra/core/tools";
3
+ import { openai, createOpenAI } from "@ai-sdk/openai";
4
+ import { anthropic, createAnthropic } from "@ai-sdk/anthropic";
5
+ import { google } from "@ai-sdk/google";
6
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
7
+ import fsNative from "fs";
8
+ import fs from "fs/promises";
9
+ import path from "path";
10
+ import os from "os";
11
+ import { exec } from "child_process";
12
+ import { fileURLToPath } from "url";
13
+ import { promisify } from "util";
14
+ export const getSkillsDir = () => {
15
+ if (typeof __dirname === "string") {
16
+ return path.resolve(__dirname, "..", "skills");
17
+ }
18
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "skills");
19
+ };
20
+ import { LibSQLStore, LibSQLVector } from "@mastra/libsql";
21
+ import { Memory } from "@mastra/memory";
22
+ import { DEFAULT_LOCAL_RUNTIME_BASE_URL, DEFAULT_LOCAL_RUNTIME_API_KEY, isLocalModelId, } from "../library/localRuntime.js";
23
+ // MY TOOLS
24
+ import readFileTool from "./tools/readFile.js";
25
+ import searchFilesTool from "./tools/searchFiles.js";
26
+ import writeFileTool from "./tools/writeFile.js";
27
+ import editFileTool from "./tools/editFile.js";
28
+ import listDirectoryTool from "./tools/listDirectory.js";
29
+ import grepSearchTool from "./tools/grepSearch.js";
30
+ import runTerminalCommandTool from "./tools/runTerminalCommand.js";
31
+ import deleteFileTool from "./tools/deleteFile.js";
32
+ import createDirectoryTool from "./tools/createDirectory.js";
33
+ import renameFileTool from "./tools/renameFile.js";
34
+ import getWorkspaceInfoTool from "./tools/getWorkspaceInfo.js";
35
+ import getSymbolsTool from "./tools/getSymbols.js";
36
+ import applyDiffTool from "./tools/applyDiff.js";
37
+ import findFileContentTool from "./tools/fileContent.js";
38
+ import queryKnowledgeGraphTool from "./tools/queryKnowledgeGraph.js";
39
+ // LSP-based tools for code intelligence
40
+ import getTypeInfoTool from "./tools/getTypeInfo.js";
41
+ import findDefinitionTool from "./tools/findDefinition.js";
42
+ import findReferencesTool from "./tools/findReferences.js";
43
+ import getSymbolsLSPTool from "./tools/getSymbolsLSP.js";
44
+ import getCodeCompletionTool from "./tools/getCodeCompletion.js";
45
+ import getSignatureHelpTool from "./tools/getSignatureHelp.js";
46
+ import getCodeActionsTool from "./tools/getCodeActions.js";
47
+ import renameSymbolTool from "./tools/renameSymbol.js";
48
+ import formatDocumentTool from "./tools/formatDocument.js";
49
+ import getWorkspaceSymbolsTool from "./tools/getWorkspaceSymbols.js";
50
+ import { taskListTool, taskOutputTool, taskStopTool, } from "./tools/backgroundTasks.js";
51
+ // Code context tool for VS Code Copilot-style responses
52
+ import getCodeContextTool from "./tools/getCodeContext.js";
53
+ import { StreamErrorRetryProcessor, } from "@mastra/core/processors";
54
+ import { MultimodalTokenLimiterProcessor } from "./utils/multimodalTokenLimiter.js";
55
+ import { coreLsp } from "../library/lsp/coreLsp.js";
56
+ import { executeToolWithRecovery } from "./utils/errorRecovery.js";
57
+ import { loadSkillsFromDirectories, } from "./utils/skillsDiscovery.js";
58
+ import { resolveModelInputTokenLimit, resolveModelSupportsVision } from "../../helpers/promptBudget.js";
59
+ // Browser-backed fallback for providers without native web search.
60
+ import { webSearchTool as browserWebSearchTool } from "./tools/webSearch.js";
61
+ //for skills to be called by the workspace
62
+ import { Workspace, LocalFilesystem, } from "@mastra/core/workspace";
63
+ import { RequestContext } from "@mastra/core/request-context";
64
+ import { buildMcpTools, generateMcpToolsDocs } from "./tools/mcpTools.js";
65
+ import { enforceToolCallBudgetForTools, } from "./utils/toolCallBudget.js";
66
+ import { resolveOpenRouterModelSettings } from "./utils/openRouterModelSettings.js";
67
+ import { ToolResultSafetyProcessor } from "./utils/toolResultSafetyProcessor.js";
68
+ import { CapturedWorkspaceMutationBridge } from "./utils/capturedWorkspaceMutationBridge.js";
69
+ import { rejectUnsafeWorkspaceMutation } from "./utils/workspacePathGuard.js";
70
+ import { CODING_AGENT_INSTRUCTIONS, createIrisWorkspaceToolsConfig, } from "./publicContracts.js";
71
+ export { CODING_AGENT_INSTRUCTIONS, createIrisWorkspaceToolsConfig, } from "./publicContracts.js";
72
+ const withRuntimePreToolHook = (tools) => Object.fromEntries(Object.entries(tools).map(([toolName, tool]) => {
73
+ if (!tool ||
74
+ typeof tool !== "object" ||
75
+ typeof tool.execute !== "function") {
76
+ return [toolName, tool];
77
+ }
78
+ const originalExecute = tool.execute;
79
+ return [toolName, {
80
+ ...tool,
81
+ execute: async (input, context) => {
82
+ const requestContext = context?.requestContext;
83
+ const onPreToolUse = requestContext?.get?.("onPreToolUse");
84
+ await onPreToolUse?.({
85
+ toolName,
86
+ toolCallId: context?.toolCallId,
87
+ toolArgs: input && typeof input === "object"
88
+ ? input
89
+ : undefined,
90
+ });
91
+ return originalExecute(input, context);
92
+ },
93
+ }];
94
+ }));
95
+ const execAsync = promisify(exec);
96
+ const validationConfigCache = new Map();
97
+ const isEnabledByEnv = (value, defaultValue) => {
98
+ if (value === undefined)
99
+ return defaultValue;
100
+ const normalized = value.trim().toLowerCase();
101
+ if (["1", "true", "yes", "on"].includes(normalized))
102
+ return true;
103
+ if (["0", "false", "no", "off"].includes(normalized))
104
+ return false;
105
+ return defaultValue;
106
+ };
107
+ const DEFAULT_OBSERVATIONAL_MODEL = "google/gemini-2.5-flash";
108
+ const DEFAULT_OBSERVATIONAL_SCOPE = "resource";
109
+ const DEFAULT_OBSERVATION_MESSAGE_TOKENS = 4_000;
110
+ const DEFAULT_REFLECTION_OBSERVATION_TOKENS = 8_000;
111
+ const isMemoryEnabled = (value) => value !== false;
112
+ const asBoundedInteger = (value, fallback, min, max) => {
113
+ const parsed = typeof value === "number"
114
+ ? value
115
+ : typeof value === "string"
116
+ ? Number.parseInt(value, 10)
117
+ : Number.NaN;
118
+ if (!Number.isFinite(parsed))
119
+ return fallback;
120
+ return Math.max(min, Math.min(max, Math.trunc(parsed)));
121
+ };
122
+ const resolveObservationalMemoryOption = (enabledOverride, settingsOverride) => {
123
+ // Observational-memory mode is controlled by app settings forwarded per request.
124
+ // We intentionally avoid raw env fallback here to keep behavior user-scoped.
125
+ const enabled = typeof enabledOverride === "boolean" ? enabledOverride : false;
126
+ if (!enabled)
127
+ return false;
128
+ const modelRaw = settingsOverride?.model;
129
+ const model = typeof modelRaw === "string" && modelRaw.trim().length > 0
130
+ ? modelRaw.trim()
131
+ : DEFAULT_OBSERVATIONAL_MODEL;
132
+ const scope = settingsOverride?.scope === "thread" ? "thread" : DEFAULT_OBSERVATIONAL_SCOPE;
133
+ const messageTokens = asBoundedInteger(settingsOverride?.observationMessageTokens, DEFAULT_OBSERVATION_MESSAGE_TOKENS, 4_000, 200_000);
134
+ const observationTokens = asBoundedInteger(settingsOverride?.reflectionObservationTokens, DEFAULT_REFLECTION_OBSERVATION_TOKENS, 8_000, 400_000);
135
+ const previousObserverTokens = asBoundedInteger(settingsOverride?.observationPreviousTokens, 0, 0, 100_000);
136
+ const activateAfterIdle = typeof settingsOverride?.activateAfterIdle === "string"
137
+ ? settingsOverride.activateAfterIdle.trim()
138
+ : "";
139
+ const activateOnProviderChange = typeof settingsOverride?.activateOnProviderChange === "boolean"
140
+ ? settingsOverride.activateOnProviderChange
141
+ : true;
142
+ const observationalMemory = {
143
+ model,
144
+ scope,
145
+ activateOnProviderChange,
146
+ };
147
+ observationalMemory.observation = {
148
+ messageTokens,
149
+ previousObserverTokens,
150
+ };
151
+ observationalMemory.reflection = {
152
+ observationTokens,
153
+ };
154
+ if (activateAfterIdle) {
155
+ observationalMemory.activateAfterIdle = activateAfterIdle;
156
+ }
157
+ return observationalMemory;
158
+ };
159
+ const resolvePackageManager = async (basePath) => {
160
+ try {
161
+ await fs.access(path.join(basePath, "pnpm-lock.yaml"));
162
+ return "pnpm";
163
+ }
164
+ catch {
165
+ // Keep checking other lockfiles.
166
+ }
167
+ try {
168
+ await fs.access(path.join(basePath, "yarn.lock"));
169
+ return "yarn";
170
+ }
171
+ catch {
172
+ // Default fallback.
173
+ }
174
+ return "npm";
175
+ };
176
+ const buildScriptCommand = (packageManager, script) => {
177
+ if (packageManager === "yarn") {
178
+ return `yarn -s ${script}`;
179
+ }
180
+ if (packageManager === "pnpm") {
181
+ return `pnpm -s run ${script}`;
182
+ }
183
+ return `npm run --silent ${script}`;
184
+ };
185
+ const resolveValidationConfig = async (basePath) => {
186
+ if (validationConfigCache.has(basePath)) {
187
+ return validationConfigCache.get(basePath);
188
+ }
189
+ const config = {
190
+ autoLint: isEnabledByEnv(process.env.IRIS_AGENT_AUTO_LINT, false),
191
+ autoTest: isEnabledByEnv(process.env.IRIS_AGENT_AUTO_TEST, false),
192
+ lintCommand: process.env.IRIS_AGENT_LINT_CMD || undefined,
193
+ testCommand: process.env.IRIS_AGENT_TEST_CMD || undefined,
194
+ };
195
+ if ((!config.lintCommand && config.autoLint) || (!config.testCommand && config.autoTest)) {
196
+ try {
197
+ const packageManager = await resolvePackageManager(basePath);
198
+ const packageJsonPath = path.join(basePath, "package.json");
199
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf8");
200
+ const packageJson = JSON.parse(packageJsonContent);
201
+ const scripts = packageJson.scripts || {};
202
+ if (!config.lintCommand && config.autoLint && typeof scripts.lint === "string") {
203
+ config.lintCommand = buildScriptCommand(packageManager, "lint");
204
+ }
205
+ if (!config.testCommand && config.autoTest && typeof scripts.test === "string") {
206
+ config.testCommand = buildScriptCommand(packageManager, "test");
207
+ }
208
+ }
209
+ catch {
210
+ // No package.json or unreadable config; keep explicit env commands only.
211
+ }
212
+ }
213
+ validationConfigCache.set(basePath, config);
214
+ return config;
215
+ };
216
+ const runValidationCommand = async (command, cwd) => {
217
+ const start = Date.now();
218
+ try {
219
+ const { stdout, stderr } = await execAsync(command, {
220
+ cwd,
221
+ timeout: 120000,
222
+ maxBuffer: 1024 * 1024 * 8,
223
+ env: process.env,
224
+ });
225
+ return {
226
+ enabled: true,
227
+ command,
228
+ success: true,
229
+ stdout: String(stdout || "").trim(),
230
+ stderr: String(stderr || "").trim(),
231
+ durationMs: Date.now() - start,
232
+ };
233
+ }
234
+ catch (error) {
235
+ const err = error;
236
+ return {
237
+ enabled: true,
238
+ command,
239
+ success: false,
240
+ stdout: String(err.stdout || "").trim(),
241
+ stderr: String(err.stderr || "").trim(),
242
+ error: err.message,
243
+ durationMs: Date.now() - start,
244
+ };
245
+ }
246
+ };
247
+ const runPostEditValidation = async (basePath) => {
248
+ const config = await resolveValidationConfig(basePath);
249
+ const result = {};
250
+ if (config.autoLint) {
251
+ if (config.lintCommand) {
252
+ result.lint = await runValidationCommand(config.lintCommand, basePath);
253
+ }
254
+ else {
255
+ result.lint = {
256
+ enabled: true,
257
+ success: false,
258
+ stdout: "",
259
+ stderr: "",
260
+ error: "Auto lint enabled but no lint command could be resolved",
261
+ };
262
+ }
263
+ }
264
+ if (config.autoTest) {
265
+ if (config.testCommand) {
266
+ result.test = await runValidationCommand(config.testCommand, basePath);
267
+ }
268
+ else {
269
+ result.test = {
270
+ enabled: true,
271
+ success: false,
272
+ stdout: "",
273
+ stderr: "",
274
+ error: "Auto test enabled but no test command could be resolved",
275
+ };
276
+ }
277
+ }
278
+ return Object.keys(result).length > 0 ? result : null;
279
+ };
280
+ /**
281
+ * Get the appropriate AI model provider based on model ID
282
+ * @param {string} modelId - The model identifier (e.g., 'gpt-4o', 'claude-3-5-sonnet', 'gemini-pro')
283
+ * @returns {Object} AI SDK model instance
284
+ */
285
+ const getModel = (modelId) => {
286
+ const anthropicBetaEnv = process.env.ANTHROPIC_BETA || process.env.ANTHROPIC_BETAS || "";
287
+ const anthropicBetaHeader = anthropicBetaEnv
288
+ .split(",")
289
+ .map((entry) => entry.trim())
290
+ .filter(Boolean)
291
+ .join(",");
292
+ const anthropicProvider = anthropicBetaHeader
293
+ ? createAnthropic({
294
+ headers: {
295
+ "anthropic-beta": anthropicBetaHeader,
296
+ },
297
+ })
298
+ : null;
299
+ const normalizedModelId = modelId.endsWith("-other")
300
+ ? modelId.slice(0, -"-other".length)
301
+ : modelId;
302
+ const localBaseUrl = process.env.OLLAMA_BASE_URL || DEFAULT_LOCAL_RUNTIME_BASE_URL;
303
+ const localApiKey = process.env.OLLAMA_API_KEY || DEFAULT_LOCAL_RUNTIME_API_KEY;
304
+ const localOpenAICompatible = createOpenAI({
305
+ baseURL: localBaseUrl,
306
+ apiKey: localApiKey,
307
+ });
308
+ const isHuggingFaceModelId = (candidateModelId) => {
309
+ const normalized = (candidateModelId || "").toLowerCase();
310
+ if (!normalized)
311
+ return false;
312
+ if (normalized.startsWith("huggingface/"))
313
+ return true;
314
+ // Common HuggingFace router provider suffix (e.g. "...:fireworks-ai")
315
+ if (normalized.endsWith(":fireworks-ai"))
316
+ return true;
317
+ return false;
318
+ };
319
+ // HuggingFace Inference Router (OpenAI-compatible)
320
+ if (isHuggingFaceModelId(normalizedModelId)) {
321
+ const hfToken = process.env.HF_TOKEN;
322
+ if (!hfToken) {
323
+ throw new Error("HuggingFace token is not configured. Set HF_TOKEN in Settings and click Save API Keys.");
324
+ }
325
+ const hfClient = createOpenAI({
326
+ baseURL: "https://router.huggingface.co/v1",
327
+ apiKey: hfToken,
328
+ });
329
+ const cleanId = normalizedModelId.startsWith("huggingface/")
330
+ ? normalizedModelId.slice("huggingface/".length)
331
+ : normalizedModelId;
332
+ return hfClient.chat(cleanId);
333
+ }
334
+ const resolveOpenrouterModel = (candidateModelId) => {
335
+ const openrouterApiKey = process.env.OPENROUTER_API_KEY;
336
+ if (!openrouterApiKey) {
337
+ throw new Error("OpenRouter API key is not configured. Set OPENROUTER_API_KEY in Settings and click Save API Keys.");
338
+ }
339
+ const isOpenRouterDebugEnabled = process.env.NODE_ENV !== "production" &&
340
+ process.env.OPENROUTER_DEBUG_ECHO_UPSTREAM_BODY === "1";
341
+ const openrouter = createOpenRouter({
342
+ baseURL: process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1",
343
+ apiKey: openrouterApiKey,
344
+ // Optional attribution headers surface the app on OpenRouter leaderboards.
345
+ headers: {
346
+ "HTTP-Referer": process.env.OPENROUTER_SITE_URL || "https://github.com/4onstudios/iris-agent/",
347
+ "X-Title": process.env.OPENROUTER_SITE_NAME || "Iris",
348
+ },
349
+ ...(isOpenRouterDebugEnabled
350
+ ? {
351
+ fetch: async (input, init) => {
352
+ const body = init?.body;
353
+ if (typeof body === "string") {
354
+ try {
355
+ const payload = JSON.parse(body);
356
+ console.debug("[openrouter] validated outgoing request", {
357
+ model: payload.model,
358
+ messageCount: payload.messages?.length ?? 0,
359
+ toolCount: payload.tools?.length ?? 0,
360
+ });
361
+ }
362
+ catch (error) {
363
+ console.error("[openrouter] invalid outgoing JSON request", error);
364
+ }
365
+ }
366
+ return fetch(input, init);
367
+ },
368
+ }
369
+ : {}),
370
+ });
371
+ const openrouterAliasMap = {
372
+ "openai/gpt-latest": "openrouter/free",
373
+ "~openai/gpt-latest": "openrouter/free",
374
+ };
375
+ let resolvedOpenrouterModelId = openrouterAliasMap[candidateModelId] || candidateModelId;
376
+ const debugSettings = isOpenRouterDebugEnabled
377
+ ? { debug: { echo_upstream_body: true } }
378
+ : undefined;
379
+ // Free provider-specific routes are frequently burst-limited upstream.
380
+ // Route them through OpenRouter's free router to improve availability.
381
+ if (resolvedOpenrouterModelId.endsWith(":free")) {
382
+ resolvedOpenrouterModelId = "openrouter/free";
383
+ }
384
+ if (resolvedOpenrouterModelId !== candidateModelId) {
385
+ console.warn(`[agent] mapped OpenRouter model alias '${candidateModelId}' -> '${resolvedOpenrouterModelId}'`);
386
+ }
387
+ return openrouter.chat(resolvedOpenrouterModelId, {
388
+ ...(resolveOpenRouterModelSettings(resolvedOpenrouterModelId) ?? {}),
389
+ ...(debugSettings ?? {}),
390
+ });
391
+ };
392
+ // OpenRouter unified gateway (OpenAI-compatible). Model IDs are explicitly
393
+ // namespaced with an `openrouter/` prefix to avoid colliding with the
394
+ // provider-routing heuristics below (e.g. an OpenRouter slug like
395
+ // `google/gemini-2.0-flash` would otherwise be routed to Google directly).
396
+ if (normalizedModelId.startsWith("openrouter/")) {
397
+ const cleanId = normalizedModelId.slice("openrouter/".length);
398
+ return resolveOpenrouterModel(cleanId);
399
+ }
400
+ // Explicit direct-provider namespace support.
401
+ // These slugs should route to the corresponding provider rather than the
402
+ // OpenRouter slash-model fallback below.
403
+ if (normalizedModelId.startsWith("openai/")) {
404
+ const cleanId = normalizedModelId.slice("openai/".length);
405
+ return openai(cleanId);
406
+ }
407
+ if (normalizedModelId.startsWith("anthropic/")) {
408
+ const cleanId = normalizedModelId.slice("anthropic/".length);
409
+ if (anthropicBetaHeader) {
410
+ console.warn(`[agent] using anthropic-beta header: ${anthropicBetaHeader}`);
411
+ return anthropicProvider(cleanId);
412
+ }
413
+ return anthropic(cleanId);
414
+ }
415
+ // Some callers may already pass a raw OpenRouter slug (e.g. xiaomi/mimo-v2.5)
416
+ // without the `openrouter/` namespace. Treat these as OpenRouter models.
417
+ if (normalizedModelId.includes("/") &&
418
+ !isHuggingFaceModelId(normalizedModelId) &&
419
+ !normalizedModelId.startsWith("openai/") &&
420
+ !normalizedModelId.startsWith("anthropic/") &&
421
+ !normalizedModelId.startsWith("google/") &&
422
+ !normalizedModelId.startsWith("ollama/") &&
423
+ !normalizedModelId.startsWith("local/")) {
424
+ return resolveOpenrouterModel(normalizedModelId);
425
+ }
426
+ // USB-local or self-hosted OpenAI-compatible models (e.g. Ollama)
427
+ if (isLocalModelId(normalizedModelId)) {
428
+ const cleanId = normalizedModelId.startsWith("ollama/")
429
+ ? normalizedModelId.slice("ollama/".length)
430
+ : normalizedModelId.slice("local/".length);
431
+ return localOpenAICompatible(cleanId);
432
+ }
433
+ // Anthropic Claude models
434
+ if (normalizedModelId.startsWith("claude")) {
435
+ if (!process.env.ANTHROPIC_API_KEY && process.env.OPENROUTER_API_KEY) {
436
+ return resolveOpenrouterModel(`anthropic/${normalizedModelId}`);
437
+ }
438
+ if (anthropicBetaHeader) {
439
+ console.warn(`[agent] using anthropic-beta header: ${anthropicBetaHeader}`);
440
+ return anthropicProvider(normalizedModelId);
441
+ }
442
+ return anthropic(normalizedModelId);
443
+ }
444
+ // Google Gemini models
445
+ if (normalizedModelId.startsWith("gemini") || normalizedModelId.startsWith("google/")) {
446
+ if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY && !process.env.GEMINI_API_KEY && process.env.OPENROUTER_API_KEY) {
447
+ const cleanId = normalizedModelId.replace("google/", "");
448
+ return resolveOpenrouterModel(`google/${cleanId}`);
449
+ }
450
+ const cleanId = normalizedModelId.replace("google/", "");
451
+ return google(cleanId);
452
+ }
453
+ // OpenAI models (default)
454
+ if (normalizedModelId.startsWith("gpt") ||
455
+ normalizedModelId.startsWith("o1") ||
456
+ normalizedModelId.startsWith("o3")) {
457
+ if (!process.env.OPENAI_API_KEY && process.env.OPENROUTER_API_KEY) {
458
+ return resolveOpenrouterModel(`openai/${normalizedModelId}`);
459
+ }
460
+ return openai(normalizedModelId);
461
+ }
462
+ // Default to OpenRouter if OPENROUTER_API_KEY is present, else OpenAI
463
+ if (!process.env.OPENAI_API_KEY && process.env.OPENROUTER_API_KEY) {
464
+ return resolveOpenrouterModel(normalizedModelId);
465
+ }
466
+ console.warn(`Unknown model provider for '${normalizedModelId}', defaulting to OpenAI`);
467
+ return openai(normalizedModelId);
468
+ };
469
+ const supportsMastraProviderWebSearch = (modelId) => {
470
+ const normalizedModelId = modelId.endsWith("-other")
471
+ ? modelId.slice(0, -"-other".length)
472
+ : modelId;
473
+ if (isLocalModelId(normalizedModelId) ||
474
+ normalizedModelId.startsWith("openrouter/") ||
475
+ normalizedModelId.startsWith("hf/") ||
476
+ normalizedModelId.startsWith("huggingface/")) {
477
+ return false;
478
+ }
479
+ return (normalizedModelId.startsWith("gpt") ||
480
+ normalizedModelId.startsWith("o1") ||
481
+ normalizedModelId.startsWith("o3") ||
482
+ normalizedModelId.startsWith("openai/") ||
483
+ normalizedModelId.startsWith("claude") ||
484
+ normalizedModelId.startsWith("anthropic/") ||
485
+ normalizedModelId.startsWith("gemini") ||
486
+ normalizedModelId.startsWith("google/"));
487
+ };
488
+ const toFinitePositiveInt = (value, fallback) => {
489
+ if (typeof value !== "number" || !Number.isFinite(value)) {
490
+ return fallback;
491
+ }
492
+ const normalized = Math.floor(value);
493
+ return normalized > 0 ? normalized : fallback;
494
+ };
495
+ const parseOptionalBooleanEnv = (value) => {
496
+ if (value === undefined)
497
+ return undefined;
498
+ const normalized = value.trim().toLowerCase();
499
+ if (["1", "true", "yes", "on"].includes(normalized))
500
+ return true;
501
+ if (["0", "false", "no", "off"].includes(normalized))
502
+ return false;
503
+ return undefined;
504
+ };
505
+ const parseOptionalPositiveIntEnv = (value) => {
506
+ if (value === undefined)
507
+ return undefined;
508
+ const parsed = Number.parseInt(value, 10);
509
+ if (!Number.isFinite(parsed))
510
+ return undefined;
511
+ const normalized = Math.floor(parsed);
512
+ return normalized > 0 ? normalized : undefined;
513
+ };
514
+ const resolveStreamErrorRetryConfig = (retryConfig) => {
515
+ if (retryConfig !== undefined) {
516
+ return retryConfig;
517
+ }
518
+ const enabled = parseOptionalBooleanEnv(process.env.IRIS_AGENT_STREAM_RETRY_ENABLED);
519
+ const retryUnknownErrors = parseOptionalBooleanEnv(process.env.IRIS_AGENT_STREAM_RETRY_UNKNOWN_ERRORS);
520
+ const maxRetries = parseOptionalPositiveIntEnv(process.env.IRIS_AGENT_STREAM_RETRY_MAX_RETRIES);
521
+ const baseDelayMs = parseOptionalPositiveIntEnv(process.env.IRIS_AGENT_STREAM_RETRY_BASE_DELAY_MS);
522
+ const maxDelayMs = parseOptionalPositiveIntEnv(process.env.IRIS_AGENT_STREAM_RETRY_MAX_DELAY_MS);
523
+ const fromEnv = {
524
+ enabled,
525
+ retryUnknownErrors,
526
+ maxRetries,
527
+ baseDelayMs,
528
+ maxDelayMs,
529
+ };
530
+ return Object.values(fromEnv).some((value) => value !== undefined)
531
+ ? fromEnv
532
+ : undefined;
533
+ };
534
+ const buildStreamErrorRetryProcessors = (retryConfig) => {
535
+ if (retryConfig === undefined) {
536
+ return undefined;
537
+ }
538
+ if (retryConfig.enabled === false) {
539
+ return [];
540
+ }
541
+ const maxRetries = toFinitePositiveInt(retryConfig.maxRetries, 3);
542
+ const baseDelayMs = toFinitePositiveInt(retryConfig.baseDelayMs, 800);
543
+ const maxDelayMs = toFinitePositiveInt(retryConfig.maxDelayMs, 30_000);
544
+ return [
545
+ new StreamErrorRetryProcessor({
546
+ maxRetries,
547
+ retryUnknownErrors: retryConfig.retryUnknownErrors === true,
548
+ delayMs: ({ retryCount }) => Math.min(baseDelayMs * 2 ** retryCount, maxDelayMs),
549
+ }),
550
+ ];
551
+ };
552
+ /**
553
+ * Module-level cache of all skill entries, populated lazily on the first call
554
+ * to `loadAllSkills` and retained for the lifetime of the process.
555
+ *
556
+ * **Intentional trade-off**: skills are bundled assets that change only on
557
+ * deploy, so re-reading the directory on every request would add unnecessary
558
+ * I/O with no practical benefit in production.
559
+ *
560
+ * If you need to invalidate the cache (e.g. in tests or during development
561
+ * hot-reload) call the exported `resetSkillsCache()` helper.
562
+ */
563
+ let _allSkillsCache = null;
564
+ /**
565
+ * Invalidate the in-process skills cache so that the next call to
566
+ * `loadAllSkills` / `buildSkillsBlock` re-reads the skills directory.
567
+ * Intended for use in tests and development hot-reload scenarios.
568
+ */
569
+ export function resetSkillsCache() {
570
+ _allSkillsCache = null;
571
+ }
572
+ async function loadAllSkills() {
573
+ if (process.env.NODE_ENV === "production" && _allSkillsCache !== null)
574
+ return _allSkillsCache;
575
+ const skills = await loadSkillsFromDirectories([getSkillsDir()]);
576
+ _allSkillsCache = skills;
577
+ return skills;
578
+ }
579
+ const normalizeEnabledSkills = (enabledSkills) => {
580
+ if (!Array.isArray(enabledSkills)) {
581
+ return [];
582
+ }
583
+ const normalized = enabledSkills
584
+ .map((value) => (typeof value === "string" ? value.trim() : ""))
585
+ .filter((value) => /^[a-zA-Z0-9._-]{1,80}$/.test(value));
586
+ return Array.from(new Set(normalized));
587
+ };
588
+ const resolveAgentSkillPaths = async (enabledSkills) => {
589
+ const normalizedEnabledSkills = normalizeEnabledSkills(enabledSkills);
590
+ if (normalizedEnabledSkills.length === 0) {
591
+ return [];
592
+ }
593
+ const all = await loadAllSkills();
594
+ const pathsByName = new Map(all.map((skill) => [
595
+ skill.name,
596
+ path.join(getSkillsDir(), path.basename(path.dirname(skill.location))),
597
+ ]));
598
+ return normalizedEnabledSkills
599
+ .map((name) => pathsByName.get(name))
600
+ .filter((skillPath) => Boolean(skillPath));
601
+ };
602
+ export const createAgentRequestContext = (enabledSkills, requestValues) => {
603
+ const normalizedEnabledSkills = normalizeEnabledSkills(enabledSkills);
604
+ const context = new RequestContext([["enabledSkills", normalizedEnabledSkills]]);
605
+ // Request-scoped values (workspace, model, git state) belong here rather
606
+ // than in the prompt: they influence behavior without polluting memory.
607
+ for (const [key, value] of Object.entries(requestValues || {})) {
608
+ if (value !== undefined) {
609
+ context.set(key, value);
610
+ }
611
+ }
612
+ return context;
613
+ };
614
+ /**
615
+ * Return the canonical list of available skills for use by the
616
+ * `GET /api/agent/skills` endpoint. Each entry exposes the directory-name slug
617
+ * as `id` (matching what the client stores in preferences) and the description
618
+ * sourced directly from the SKILL.md frontmatter.
619
+ */
620
+ export async function getSkillsList() {
621
+ const all = await loadAllSkills();
622
+ return all.map((s) => ({ id: s.name, description: s.description }));
623
+ }
624
+ /**
625
+ * Determine the appropriate database URL for Mastra Memory storage.
626
+ * - Desktop (Tauri): platform-specific app data directory (macOS: ~/Library/Application Support/AIRIS)
627
+ * - Web/Server: external database URL from env or shared data directory
628
+ * - Development: ./mastra.db (relative to cwd)
629
+ */
630
+ function getDefaultDbUrl() {
631
+ const isTauriBundled = (() => {
632
+ const rawValue = process.env.TAURI_BUNDLED;
633
+ if (!rawValue)
634
+ return false;
635
+ const normalized = rawValue.trim().toLowerCase();
636
+ return normalized === "1" || normalized === "true" || normalized === "yes";
637
+ })();
638
+ // Desktop Tauri app: use platform-specific app data directory
639
+ if (isTauriBundled) {
640
+ let appDataDir;
641
+ if (process.platform === "darwin") {
642
+ // macOS: ~/Library/Application Support/AIRIS
643
+ appDataDir = path.join(os.homedir(), "Library", "Application Support", "AIRIS");
644
+ }
645
+ else if (process.platform === "win32") {
646
+ // Windows: %APPDATA%\AIRIS
647
+ appDataDir = path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "AIRIS");
648
+ }
649
+ else {
650
+ // Linux: ~/.local/share/airis
651
+ appDataDir = path.join(os.homedir(), ".local", "share", "airis");
652
+ }
653
+ // Ensure parent directory exists before SQLite opens/creates the DB file.
654
+ fsNative.mkdirSync(appDataDir, { recursive: true });
655
+ return `file:${path.join(appDataDir, "mastra.db")}`;
656
+ }
657
+ // Web/Server deployment: check for external database
658
+ if (process.env.DATABASE_URL) {
659
+ return process.env.DATABASE_URL;
660
+ }
661
+ // Development fallback
662
+ return "file:./mastra.db";
663
+ }
664
+ export const createCodingAgent = async (modelId = "gpt-4o", workspacePath = null, options = {}) => {
665
+ // Skip filesystem workspace init for virtual/web paths (e.g. /workspace/*)
666
+ // that don't exist on the server's filesystem. The web client provides file
667
+ // context for those sessions, while agent-level skills still come from the
668
+ // app-owned skills directory.
669
+ const isVirtualPath = workspacePath && workspacePath.startsWith("/workspace/");
670
+ const basePath = workspacePath || process.cwd();
671
+ const mcpServers = options.mcpServers || [];
672
+ const terminalAutoApproveRules = options.terminalAutoApproveRules;
673
+ const modelInputTokenLimit = resolveModelInputTokenLimit(modelId);
674
+ const modelSupportsVision = resolveModelSupportsVision(modelId);
675
+ const resolvedStreamErrorRetryConfig = resolveStreamErrorRetryConfig(options.streamErrorRetry);
676
+ const configuredErrorProcessors = buildStreamErrorRetryProcessors(resolvedStreamErrorRetryConfig);
677
+ const errorProcessorOverrides = configuredErrorProcessors === undefined
678
+ ? {}
679
+ : { errorProcessors: configuredErrorProcessors };
680
+ const observationalMemoryOption = resolveObservationalMemoryOption(options.useMastraObservationalMemory, options.observationalMemorySettings);
681
+ const goalJudgeModelId = options.goalJudgeModelId || process.env.IRIS_AGENT_GOAL_JUDGE_MODEL || modelId;
682
+ const workspaceMutationBridge = new CapturedWorkspaceMutationBridge(basePath, () => runPostEditValidation(basePath));
683
+ let workspace;
684
+ if (!isVirtualPath) {
685
+ try {
686
+ workspace = new Workspace({
687
+ // Provide concise, token-efficient filesystem guidance.
688
+ // Keep filesystem guidance focused on the available capabilities.
689
+ filesystem: new LocalFilesystem({
690
+ basePath,
691
+ instructions: "Use workspaceSearch for conceptual discovery, grepSearch for exact text, and readFile before editing. Use editFile with filePath plus oldContent/newContent for small exact replacements, writeFile for full-file changes, and applyDiff for multi-hunk patches. File mutations return reversible diff metadata for user review; validate focused changes before finishing.",
692
+ }),
693
+ bm25: true,
694
+ tools: createIrisWorkspaceToolsConfig(workspaceMutationBridge.hooks),
695
+ });
696
+ await workspace.init();
697
+ console.log("Workspace initialized");
698
+ }
699
+ catch (err) {
700
+ const error = err;
701
+ console.warn("⚠️ Workspace init failed, continuing without workspace:", error.message);
702
+ }
703
+ }
704
+ else {
705
+ console.log("⚠️ Virtual/web workspace detected — skipping filesystem initialization");
706
+ }
707
+ // Create a wrapped version of getWorkspaceInfo with the correct default path
708
+ const wrappedGetWorkspaceInfo = {
709
+ ...getWorkspaceInfoTool,
710
+ execute: async (params) => {
711
+ // FORCE the workspace path if it was provided to the factory
712
+ // The agent often hallucinates a path like "social-bot" or "my-project"
713
+ // We ignore params.workspacePath to prevent these errors
714
+ const actualParams = {
715
+ ...params,
716
+ workspacePath: workspacePath || process.cwd(),
717
+ };
718
+ // Use error recovery for workspace info tool
719
+ return executeToolWithRecovery(getWorkspaceInfoTool, actualParams, {
720
+ maxRetries: 2,
721
+ workspacePath: workspacePath || process.cwd(),
722
+ });
723
+ },
724
+ };
725
+ // Helper to resolve paths relative to workspace
726
+ const resolvePath = (p) => {
727
+ if (!p)
728
+ return p;
729
+ if (path.isAbsolute(p))
730
+ return p;
731
+ return path.join(workspacePath || process.cwd(), p);
732
+ };
733
+ // Helper to wrap tools with workspace path resolution AND error recovery
734
+ const wrapTool = (tool,
735
+ // eslint-disable-next-line no-unused-vars
736
+ localExecute) => {
737
+ return {
738
+ ...tool,
739
+ execute: async (params) =>
740
+ // Wrap execution with error recovery
741
+ (await executeToolWithRecovery({
742
+ ...tool,
743
+ // eslint-disable-next-line no-unused-vars
744
+ execute: localExecute,
745
+ }, params, {
746
+ maxRetries: 2,
747
+ workspacePath: workspacePath || process.cwd(),
748
+ })),
749
+ };
750
+ };
751
+ // Wrap tools to ensure they use the correct workspace path
752
+ const workspaceBasePath = workspacePath || process.cwd();
753
+ const rejectOutsideWorkspaceMutation = (...filePaths) => rejectUnsafeWorkspaceMutation(workspaceBasePath, Boolean(isVirtualPath), filePaths);
754
+ const withPostEditValidation = async (result) => {
755
+ if (!result || result.success !== true) {
756
+ return result;
757
+ }
758
+ const validation = await runPostEditValidation(workspaceBasePath);
759
+ if (!validation) {
760
+ return result;
761
+ }
762
+ return {
763
+ ...result,
764
+ validation,
765
+ };
766
+ };
767
+ const wrappedReadFile = wrapTool(readFileTool, async (p) => readFileTool.execute({
768
+ ...p,
769
+ filePath: resolvePath(p.filePath),
770
+ workspaceRoot: p.workspaceRoot || workspacePath || process.cwd(),
771
+ }));
772
+ const wrappedWriteFile = wrapTool(writeFileTool, async (p) => {
773
+ const filePath = resolvePath(p.filePath);
774
+ const rejection = await rejectOutsideWorkspaceMutation(filePath);
775
+ if (rejection)
776
+ return rejection;
777
+ return withPostEditValidation(await writeFileTool.execute({ ...p, filePath }));
778
+ });
779
+ const wrappedEditFile = wrapTool(editFileTool, async (p) => {
780
+ const filePath = resolvePath(p.filePath);
781
+ const rejection = await rejectOutsideWorkspaceMutation(filePath);
782
+ if (rejection)
783
+ return rejection;
784
+ return withPostEditValidation(await editFileTool.execute({ ...p, filePath }));
785
+ });
786
+ const wrappedDeleteFile = wrapTool(deleteFileTool, async (p) => {
787
+ const filePath = resolvePath(p.filePath);
788
+ const rejection = await rejectOutsideWorkspaceMutation(filePath);
789
+ if (rejection)
790
+ return rejection;
791
+ return deleteFileTool.execute({ ...p, filePath });
792
+ });
793
+ const wrappedRenameFile = wrapTool(renameFileTool, async (p) => {
794
+ const oldPath = resolvePath(p.oldPath);
795
+ const newPath = resolvePath(p.newPath);
796
+ const rejection = await rejectOutsideWorkspaceMutation(oldPath, newPath);
797
+ if (rejection)
798
+ return rejection;
799
+ return renameFileTool.execute({ ...p, oldPath, newPath });
800
+ });
801
+ const wrappedListDirectory = wrapTool(listDirectoryTool, async (p) => listDirectoryTool.execute({ ...p, dirPath: resolvePath(p.dirPath) }));
802
+ const wrappedCreateDirectory = wrapTool(createDirectoryTool, async (p) => {
803
+ const dirPath = resolvePath(p.dirPath);
804
+ const rejection = await rejectOutsideWorkspaceMutation(dirPath);
805
+ if (rejection)
806
+ return rejection;
807
+ return createDirectoryTool.execute({ ...p, dirPath });
808
+ });
809
+ const wrappedGetSymbols = wrapTool(getSymbolsTool, async (p) => getSymbolsTool.execute({ ...p, filePath: resolvePath(p.filePath) }));
810
+ const wrappedApplyDiff = wrapTool(applyDiffTool, async (p) => {
811
+ const filePath = resolvePath(p.filePath);
812
+ const rejection = await rejectOutsideWorkspaceMutation(filePath);
813
+ if (rejection)
814
+ return rejection;
815
+ return withPostEditValidation(await applyDiffTool.execute({ ...p, filePath }));
816
+ });
817
+ const wrappedSearchFiles = wrapTool(searchFilesTool, async (p) => searchFilesTool.execute({ ...p, cwd: workspacePath || process.cwd() }));
818
+ const wrappedGrepSearch = wrapTool(grepSearchTool, async (p) => grepSearchTool.execute({ ...p, cwd: workspacePath || process.cwd() }));
819
+ const wrappedFindFileContent = wrapTool(findFileContentTool, async (p) => findFileContentTool.execute({
820
+ ...p,
821
+ rootFolder: workspacePath || process.cwd(),
822
+ }));
823
+ const wrappedQueryKnowledgeGraph = wrapTool(queryKnowledgeGraphTool, async (p) => queryKnowledgeGraphTool.execute({
824
+ ...p,
825
+ filePath: p.filePath ? resolvePath(p.filePath) : undefined,
826
+ }));
827
+ // LSP-based tools for code intelligence
828
+ const wrappedGetTypeInfo = wrapTool(getTypeInfoTool, async (p) => getTypeInfoTool.execute({ ...p, filePath: resolvePath(p.filePath) }));
829
+ const wrappedFindDefinition = wrapTool(findDefinitionTool, async (p) => findDefinitionTool.execute({ ...p, filePath: resolvePath(p.filePath) }));
830
+ const wrappedFindReferences = wrapTool(findReferencesTool, async (p) => findReferencesTool.execute({ ...p, filePath: resolvePath(p.filePath) }));
831
+ const wrappedGetSymbolsLSP = wrapTool(getSymbolsLSPTool, async (p) => getSymbolsLSPTool.execute({ ...p, filePath: resolvePath(p.filePath) }));
832
+ const wrappedGetCodeCompletion = wrapTool(getCodeCompletionTool, async (p) => getCodeCompletionTool.execute({ ...p, filePath: resolvePath(p.filePath) }));
833
+ const wrappedGetSignatureHelp = wrapTool(getSignatureHelpTool, async (p) => getSignatureHelpTool.execute({ ...p, filePath: resolvePath(p.filePath) }));
834
+ const wrappedGetCodeActions = wrapTool(getCodeActionsTool, async (p) => getCodeActionsTool.execute({ ...p, filePath: resolvePath(p.filePath) }));
835
+ const wrappedRenameSymbol = wrapTool(renameSymbolTool, async (p) => renameSymbolTool.execute({ ...p, filePath: resolvePath(p.filePath) }));
836
+ const wrappedFormatDocument = wrapTool(formatDocumentTool, async (p) => formatDocumentTool.execute({ ...p, filePath: resolvePath(p.filePath) }));
837
+ const wrappedGetWorkspaceSymbols = wrapTool(getWorkspaceSymbolsTool, async (p) => getWorkspaceSymbolsTool.execute(p));
838
+ // Wrap getCodeContext tool for VS Code Copilot-style responses
839
+ const wrappedGetCodeContext = {
840
+ ...getCodeContextTool,
841
+ execute: async (inputData, context) => {
842
+ const execute = getCodeContextTool.execute;
843
+ if (!execute) {
844
+ throw new Error("getCodeContext tool is unavailable");
845
+ }
846
+ return execute({ ...inputData, cwd: workspacePath || process.cwd() }, context);
847
+ },
848
+ };
849
+ // Keep browser search as a fallback for local, routed, and unsupported providers.
850
+ const wrappedBrowserWebSearch = wrapTool(browserWebSearchTool, async (p) => browserWebSearchTool.execute(p));
851
+ const resolvedWebSearchTool = supportsMastraProviderWebSearch(modelId)
852
+ ? mastraWebSearchTool
853
+ : wrappedBrowserWebSearch;
854
+ // Initialize LSP manager with workspace path
855
+ if (workspacePath) {
856
+ coreLsp.connect(workspacePath);
857
+ }
858
+ // Discover MCP tools once for this agent instance.
859
+ const mcpTools = await buildMcpTools(mcpServers, basePath);
860
+ console.log("🧩 MCP Tools registered:", Object.keys(mcpTools));
861
+ if (Object.keys(mcpTools).length > 0) {
862
+ console.log("✅ MCP tools available to agent:", Object.keys(mcpTools).join(", "));
863
+ // Persist MCP tool documentation to workspace for semantic search
864
+ // Only document connected servers; remove docs if no servers available
865
+ if (workspace && mcpServers.length > 0) {
866
+ const mcpDocsPath = path.join(basePath, ".mcp-tools.md");
867
+ try {
868
+ const mcpDocs = await generateMcpToolsDocs(mcpServers, basePath);
869
+ if (mcpDocs) {
870
+ // Write documentation for connected servers
871
+ await fs.writeFile(mcpDocsPath, mcpDocs, "utf8");
872
+ console.log("📚 MCP tool documentation persisted to:", mcpDocsPath);
873
+ console.log("🔍 MCP tools documentation ready for workspace queries");
874
+ }
875
+ else {
876
+ // No connected servers - remove stale documentation if it exists
877
+ try {
878
+ await fs.unlink(mcpDocsPath);
879
+ console.log("🗑️ Removed stale MCP documentation (no servers connected)");
880
+ }
881
+ catch (err) {
882
+ // File might not exist, that's fine
883
+ }
884
+ }
885
+ }
886
+ catch (err) {
887
+ const error = err;
888
+ console.warn("⚠️ Failed to persist MCP tool docs:", error.message);
889
+ }
890
+ }
891
+ }
892
+ else {
893
+ console.log("⚠️ No MCP tools registered (mcpServers:", mcpServers.length, ")");
894
+ }
895
+ // Wrap terminal command to use workspace path as cwd
896
+ const wrappedRunTerminalCommand = {
897
+ ...runTerminalCommandTool,
898
+ execute: async (params) => {
899
+ const actualParams = {
900
+ ...params,
901
+ cwd: params.cwd || workspacePath || process.cwd(),
902
+ workspaceRoot: params.workspaceRoot || workspacePath || process.cwd(),
903
+ autoApproveRules: params.autoApproveRules || terminalAutoApproveRules,
904
+ };
905
+ // Use error recovery for terminal commands (with extended timeout handling)
906
+ return executeToolWithRecovery(runTerminalCommandTool, actualParams, {
907
+ maxRetries: 1, // Fewer retries for terminal commands
908
+ workspacePath: workspacePath || process.cwd(),
909
+ timeout: params.timeout || 30000, // Default 30s timeout
910
+ });
911
+ },
912
+ };
913
+ const runtimeTools = withRuntimePreToolHook({
914
+ // Local workspaces provide these through Mastra Workspace under the
915
+ // established Iris names. Virtual workspaces retain the client-aware tools.
916
+ ...(workspace
917
+ ? {
918
+ writeFile: wrappedWriteFile,
919
+ editFile: wrappedEditFile,
920
+ }
921
+ : {
922
+ readFile: wrappedReadFile,
923
+ listDirectory: wrappedListDirectory,
924
+ grepSearch: wrappedGrepSearch,
925
+ writeFile: wrappedWriteFile,
926
+ editFile: wrappedEditFile,
927
+ deleteFile: wrappedDeleteFile,
928
+ createDirectory: wrappedCreateDirectory,
929
+ }),
930
+ searchFiles: wrappedSearchFiles,
931
+ renameFile: wrappedRenameFile,
932
+ getWorkspaceInfo: wrappedGetWorkspaceInfo,
933
+ findFileContent: wrappedFindFileContent,
934
+ getSymbols: wrappedGetSymbols,
935
+ runTerminalCommand: wrappedRunTerminalCommand,
936
+ applyDiff: wrappedApplyDiff,
937
+ queryKnowledgeGraph: wrappedQueryKnowledgeGraph,
938
+ getTypeInfo: wrappedGetTypeInfo,
939
+ findDefinition: wrappedFindDefinition,
940
+ findReferences: wrappedFindReferences,
941
+ getSymbolsLSP: wrappedGetSymbolsLSP,
942
+ getCodeCompletion: wrappedGetCodeCompletion,
943
+ getSignatureHelp: wrappedGetSignatureHelp,
944
+ getCodeActions: wrappedGetCodeActions,
945
+ renameSymbol: wrappedRenameSymbol,
946
+ formatDocument: wrappedFormatDocument,
947
+ getWorkspaceSymbols: wrappedGetWorkspaceSymbols,
948
+ getCodeContext: wrappedGetCodeContext,
949
+ taskList: taskListTool,
950
+ taskOutput: taskOutputTool,
951
+ taskStop: taskStopTool,
952
+ webSearch: resolvedWebSearchTool,
953
+ fetchWebpage: mastraWebFetchTool,
954
+ ...mcpTools,
955
+ });
956
+ const agent = createMastraCodingAgent({
957
+ id: options.id || "custom-coding-agent",
958
+ name: options.name || "Coding Agent",
959
+ skills: async ({ requestContext }) => {
960
+ const enabledSkills = requestContext
961
+ ?.get?.("enabledSkills");
962
+ return resolveAgentSkillPaths(enabledSkills);
963
+ },
964
+ instructions: CODING_AGENT_INSTRUCTIONS,
965
+ model: getModel(modelId),
966
+ workspace,
967
+ hooks: options.hooks,
968
+ tools: enforceToolCallBudgetForTools(runtimeTools),
969
+ // Add memory for conversation context with token-aware budgeting.
970
+ // TokenLimiterProcessor moved to inputProcessors (new Mastra API).
971
+ // Memory is on by default and can be disabled only via explicit options.
972
+ memory: isMemoryEnabled(options.enableMemory)
973
+ ? new Memory({
974
+ storage: new LibSQLStore({
975
+ id: options.storageId || "custom-coding-agent-storage",
976
+ url: options.dbUrl || getDefaultDbUrl(),
977
+ }),
978
+ options: {
979
+ generateTitle: true,
980
+ // Bounded conversation history: prioritize recent turns over full replay.
981
+ // TokenLimiterProcessor enforces input token budget; lastMessages controls
982
+ // max messages to keep in context before older turns are archived.
983
+ lastMessages: 20,
984
+ workingMemory: { enabled: true },
985
+ // Compress long-running conversations into observations instead
986
+ // of replaying full history. Opt-in until embedder/model costs
987
+ // are validated for local setups.
988
+ observationalMemory: observationalMemoryOption,
989
+ },
990
+ vector: new LibSQLVector({
991
+ id: options.vectorId || "custom-coding-agent-vector",
992
+ url: options.dbUrl || getDefaultDbUrl(),
993
+ }),
994
+ })
995
+ : undefined,
996
+ goal: isMemoryEnabled(options.enableMemory)
997
+ ? {
998
+ judge: getModel(goalJudgeModelId),
999
+ maxRuns: options.goalMaxRuns ?? 12,
1000
+ maxSteps: 1,
1001
+ prompt: "Mark the objective complete only when the requested implementation is complete and applicable validation has passed. Do not accept a plan, partial implementation, or an unsupported completion claim.",
1002
+ }
1003
+ : undefined,
1004
+ // Token-aware budgeting: enforce model-specific input token limits.
1005
+ // TokenLimiterProcessor limits input tokens; outputProcessors can compress output if needed.
1006
+ inputProcessors: [new MultimodalTokenLimiterProcessor(modelInputTokenLimit, modelSupportsVision)],
1007
+ outputProcessors: [workspaceMutationBridge, new ToolResultSafetyProcessor()],
1008
+ // Keep helper defaults unless explicit retry overrides are provided.
1009
+ ...errorProcessorOverrides,
1010
+ // Default configuration
1011
+ defaultOptions: {
1012
+ maxSteps: options.maxSteps ?? 50,
1013
+ toolChoice: options.toolChoice ?? "auto",
1014
+ ...options.defaultOptions,
1015
+ },
1016
+ });
1017
+ return Object.assign(agent, {
1018
+ takeProcessedWorkspaceResults: (generationId, toolCallIds) => workspaceMutationBridge.takeProcessedResults(generationId, toolCallIds),
1019
+ clearProcessedWorkspaceResults: (generationId) => workspaceMutationBridge.clearProcessedResults(generationId),
1020
+ });
1021
+ };
1022
+ // createCodingAgent is the primary export. Callers should await it explicitly
1023
+ // rather than relying on a pre-initialized instance, to ensure environment
1024
+ // variables and database connections are ready before the agent is created.