@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.
- package/LICENSE +21 -0
- package/README.md +690 -0
- package/dist/api/acp/acpServer.d.ts +23 -0
- package/dist/api/acp/acpServer.js +814 -0
- package/dist/api/acp/index.d.ts +2 -0
- package/dist/api/acp/index.js +2 -0
- package/dist/api/acp/irisClient.d.ts +58 -0
- package/dist/api/acp/irisClient.js +214 -0
- package/dist/api/agent.d.ts +14 -0
- package/dist/api/agent.js +4171 -0
- package/dist/api/backendService.d.ts +4 -0
- package/dist/api/backendService.js +14 -0
- package/dist/api/core/agent/host/AgentContract.d.ts +82 -0
- package/dist/api/core/agent/host/AgentContract.js +1 -0
- package/dist/api/core/agent/host/AgentRegistry.d.ts +24 -0
- package/dist/api/core/agent/host/AgentRegistry.js +125 -0
- package/dist/api/core/agent/host/defaultRegistry.d.ts +15 -0
- package/dist/api/core/agent/host/defaultRegistry.js +46 -0
- package/dist/api/core/agent/host/externalAgentLifecycle.d.ts +65 -0
- package/dist/api/core/agent/host/externalAgentLifecycle.js +117 -0
- package/dist/api/core/agent/host/hostSessionManager.d.ts +111 -0
- package/dist/api/core/agent/host/hostSessionManager.js +352 -0
- package/dist/api/core/agent/host/index.d.ts +5 -0
- package/dist/api/core/agent/host/index.js +5 -0
- package/dist/api/core/agent/index.d.ts +87 -0
- package/dist/api/core/agent/index.js +1024 -0
- package/dist/api/core/agent/publicContracts.d.ts +35 -0
- package/dist/api/core/agent/publicContracts.js +103 -0
- package/dist/api/core/agent/tools/applyDiff.d.ts +51 -0
- package/dist/api/core/agent/tools/applyDiff.js +131 -0
- package/dist/api/core/agent/tools/backgroundTasks.d.ts +240 -0
- package/dist/api/core/agent/tools/backgroundTasks.js +313 -0
- package/dist/api/core/agent/tools/createDirectory.d.ts +43 -0
- package/dist/api/core/agent/tools/createDirectory.js +77 -0
- package/dist/api/core/agent/tools/deleteFile.d.ts +37 -0
- package/dist/api/core/agent/tools/deleteFile.js +66 -0
- package/dist/api/core/agent/tools/editFile.d.ts +73 -0
- package/dist/api/core/agent/tools/editFile.js +173 -0
- package/dist/api/core/agent/tools/executeCommand.d.ts +59 -0
- package/dist/api/core/agent/tools/executeCommand.js +250 -0
- package/dist/api/core/agent/tools/fileContent.d.ts +54 -0
- package/dist/api/core/agent/tools/fileContent.js +227 -0
- package/dist/api/core/agent/tools/findDefinition.d.ts +72 -0
- package/dist/api/core/agent/tools/findDefinition.js +109 -0
- package/dist/api/core/agent/tools/findReferences.d.ts +86 -0
- package/dist/api/core/agent/tools/findReferences.js +135 -0
- package/dist/api/core/agent/tools/formatDocument.d.ts +67 -0
- package/dist/api/core/agent/tools/formatDocument.js +90 -0
- package/dist/api/core/agent/tools/getCodeActions.d.ts +77 -0
- package/dist/api/core/agent/tools/getCodeActions.js +97 -0
- package/dist/api/core/agent/tools/getCodeCompletion.d.ts +77 -0
- package/dist/api/core/agent/tools/getCodeCompletion.js +174 -0
- package/dist/api/core/agent/tools/getCodeContext.d.ts +14 -0
- package/dist/api/core/agent/tools/getCodeContext.js +282 -0
- package/dist/api/core/agent/tools/getSignatureHelp.d.ts +61 -0
- package/dist/api/core/agent/tools/getSignatureHelp.js +85 -0
- package/dist/api/core/agent/tools/getSymbols.d.ts +50 -0
- package/dist/api/core/agent/tools/getSymbols.js +164 -0
- package/dist/api/core/agent/tools/getSymbolsLSP.d.ts +58 -0
- package/dist/api/core/agent/tools/getSymbolsLSP.js +147 -0
- package/dist/api/core/agent/tools/getTypeInfo.d.ts +70 -0
- package/dist/api/core/agent/tools/getTypeInfo.js +84 -0
- package/dist/api/core/agent/tools/getWorkspaceInfo.d.ts +80 -0
- package/dist/api/core/agent/tools/getWorkspaceInfo.js +281 -0
- package/dist/api/core/agent/tools/getWorkspaceSymbols.d.ts +53 -0
- package/dist/api/core/agent/tools/getWorkspaceSymbols.js +97 -0
- package/dist/api/core/agent/tools/grepSearch.d.ts +100 -0
- package/dist/api/core/agent/tools/grepSearch.js +211 -0
- package/dist/api/core/agent/tools/languageModelToolsIntegration.d.ts +13 -0
- package/dist/api/core/agent/tools/languageModelToolsIntegration.js +22 -0
- package/dist/api/core/agent/tools/listDirectory.d.ts +66 -0
- package/dist/api/core/agent/tools/listDirectory.js +161 -0
- package/dist/api/core/agent/tools/mcpTools.d.ts +48 -0
- package/dist/api/core/agent/tools/mcpTools.js +442 -0
- package/dist/api/core/agent/tools/queryKnowledgeGraph.d.ts +120 -0
- package/dist/api/core/agent/tools/queryKnowledgeGraph.js +306 -0
- package/dist/api/core/agent/tools/readFile.d.ts +92 -0
- package/dist/api/core/agent/tools/readFile.js +337 -0
- package/dist/api/core/agent/tools/renameFile.d.ts +49 -0
- package/dist/api/core/agent/tools/renameFile.js +86 -0
- package/dist/api/core/agent/tools/renameSymbol.d.ts +70 -0
- package/dist/api/core/agent/tools/renameSymbol.js +106 -0
- package/dist/api/core/agent/tools/runTerminalCommand.d.ts +72 -0
- package/dist/api/core/agent/tools/runTerminalCommand.js +69 -0
- package/dist/api/core/agent/tools/searchFiles.d.ts +82 -0
- package/dist/api/core/agent/tools/searchFiles.js +280 -0
- package/dist/api/core/agent/tools/terminalAutoApprove.d.ts +3 -0
- package/dist/api/core/agent/tools/terminalAutoApprove.js +676 -0
- package/dist/api/core/agent/tools/treeTraversal.d.ts +60 -0
- package/dist/api/core/agent/tools/treeTraversal.js +217 -0
- package/dist/api/core/agent/tools/webSearch.d.ts +37 -0
- package/dist/api/core/agent/tools/webSearch.js +80 -0
- package/dist/api/core/agent/tools/writeFile.d.ts +59 -0
- package/dist/api/core/agent/tools/writeFile.js +97 -0
- package/dist/api/core/agent/utils/capturedWorkspaceMutationBridge.d.ts +12 -0
- package/dist/api/core/agent/utils/capturedWorkspaceMutationBridge.js +67 -0
- package/dist/api/core/agent/utils/diffUtils.d.ts +14 -0
- package/dist/api/core/agent/utils/diffUtils.js +39 -0
- package/dist/api/core/agent/utils/environmentSnapshot.d.ts +42 -0
- package/dist/api/core/agent/utils/environmentSnapshot.js +213 -0
- package/dist/api/core/agent/utils/errorRecovery.d.ts +49 -0
- package/dist/api/core/agent/utils/errorRecovery.js +229 -0
- package/dist/api/core/agent/utils/multimodalTokenLimiter.d.ts +8 -0
- package/dist/api/core/agent/utils/multimodalTokenLimiter.js +238 -0
- package/dist/api/core/agent/utils/openRouterModelSettings.d.ts +12 -0
- package/dist/api/core/agent/utils/openRouterModelSettings.js +34 -0
- package/dist/api/core/agent/utils/pathRecovery.d.ts +1 -0
- package/dist/api/core/agent/utils/pathRecovery.js +29 -0
- package/dist/api/core/agent/utils/repoMapIndex.d.ts +3 -0
- package/dist/api/core/agent/utils/repoMapIndex.js +197 -0
- package/dist/api/core/agent/utils/skillsDiscovery.d.ts +10 -0
- package/dist/api/core/agent/utils/skillsDiscovery.js +50 -0
- package/dist/api/core/agent/utils/toolCallBudget.d.ts +9 -0
- package/dist/api/core/agent/utils/toolCallBudget.js +55 -0
- package/dist/api/core/agent/utils/toolLifecycle.d.ts +30 -0
- package/dist/api/core/agent/utils/toolLifecycle.js +361 -0
- package/dist/api/core/agent/utils/toolResultSafetyProcessor.d.ts +12 -0
- package/dist/api/core/agent/utils/toolResultSafetyProcessor.js +57 -0
- package/dist/api/core/agent/utils/workspaceMutationBridge.d.ts +13 -0
- package/dist/api/core/agent/utils/workspaceMutationBridge.js +149 -0
- package/dist/api/core/agent/utils/workspacePathGuard.d.ts +7 -0
- package/dist/api/core/agent/utils/workspacePathGuard.js +79 -0
- package/dist/api/core/containers/chat/toolResultSerialization.d.ts +8 -0
- package/dist/api/core/containers/chat/toolResultSerialization.js +71 -0
- package/dist/api/core/library/BrowserManager.d.ts +11 -0
- package/dist/api/core/library/BrowserManager.js +66 -0
- package/dist/api/core/library/desktopWorkspace.d.ts +1 -0
- package/dist/api/core/library/desktopWorkspace.js +3 -0
- package/dist/api/core/library/extensionManager.d.ts +10 -0
- package/dist/api/core/library/extensionManager.js +27 -0
- package/dist/api/core/library/knowledgeGraph.d.ts +268 -0
- package/dist/api/core/library/knowledgeGraph.js +989 -0
- package/dist/api/core/library/languageModelTools.d.ts +141 -0
- package/dist/api/core/library/languageModelTools.js +207 -0
- package/dist/api/core/library/localRuntime.d.ts +23 -0
- package/dist/api/core/library/localRuntime.js +144 -0
- package/dist/api/core/library/lsp/coreLsp.d.ts +367 -0
- package/dist/api/core/library/lsp/coreLsp.js +2076 -0
- package/dist/api/core/library/lsp/protocol.d.ts +25 -0
- package/dist/api/core/library/lsp/protocol.js +1 -0
- package/dist/api/core/library/lsp/serverManager.d.ts +37 -0
- package/dist/api/core/library/lsp/serverManager.js +427 -0
- package/dist/api/core/library/mcpServerProvider.d.ts +69 -0
- package/dist/api/core/library/mcpServerProvider.js +152 -0
- package/dist/api/core/library/mcpSettings.d.ts +20 -0
- package/dist/api/core/library/mcpSettings.js +131 -0
- package/dist/api/core/library/patternMatcher.d.ts +66 -0
- package/dist/api/core/library/patternMatcher.js +284 -0
- package/dist/api/core/library/regexEscape.d.ts +4 -0
- package/dist/api/core/library/regexEscape.js +4 -0
- package/dist/api/core/library/runtimeEventBus.d.ts +27 -0
- package/dist/api/core/library/runtimeEventBus.js +78 -0
- package/dist/api/core/library/safetyMiddleware.d.ts +98 -0
- package/dist/api/core/library/safetyMiddleware.js +215 -0
- package/dist/api/core/library/tauri.d.ts +71 -0
- package/dist/api/core/library/tauri.js +222 -0
- package/dist/api/core/library/tauriImport.d.ts +5 -0
- package/dist/api/core/library/tauriImport.js +7 -0
- package/dist/api/core/library/terminalAutoApproveSettings.d.ts +32 -0
- package/dist/api/core/library/terminalAutoApproveSettings.js +529 -0
- package/dist/api/core/library/workspaceIdentity.d.ts +10 -0
- package/dist/api/core/library/workspaceIdentity.js +49 -0
- package/dist/api/core/library/workspaceSummary.d.ts +19 -0
- package/dist/api/core/library/workspaceSummary.js +35 -0
- package/dist/api/core/skills/agent-customization/SKILL.md +27 -0
- package/dist/api/core/skills/bug-fix/SKILL.md +21 -0
- package/dist/api/core/skills/create-pr/SKILL.md +32 -0
- package/dist/api/core/skills/dev-server/SKILL.md +20 -0
- package/dist/api/core/skills/fix-suggestions/SKILL.md +21 -0
- package/dist/api/core/skills/github-search/SKILL.md +21 -0
- package/dist/api/core/skills/github-summary/SKILL.md +25 -0
- package/dist/api/core/skills/integration-tests/SKILL.md +29 -0
- package/dist/api/core/skills/pr-comments/SKILL.md +23 -0
- package/dist/api/core/skills/project-setup/SKILL.md +20 -0
- package/dist/api/core/skills/search-integration/SKILL.md +19 -0
- package/dist/api/core/skills/search-results/SKILL.md +19 -0
- package/dist/api/core/skills/typescript-upgrade/SKILL.md +21 -0
- package/dist/api/data/runStore.d.ts +46 -0
- package/dist/api/data/runStore.js +244 -0
- package/dist/api/helpers/agentUtils.d.ts +11 -0
- package/dist/api/helpers/agentUtils.js +23 -0
- package/dist/api/helpers/modelTokenLimits.d.ts +2 -0
- package/dist/api/helpers/modelTokenLimits.js +158 -0
- package/dist/api/helpers/observationalMemory.d.ts +10 -0
- package/dist/api/helpers/observationalMemory.js +36 -0
- package/dist/api/helpers/promptBudget.d.ts +30 -0
- package/dist/api/helpers/promptBudget.js +216 -0
- package/dist/api/helpers/resolveImageMessageParts.d.ts +9 -0
- package/dist/api/helpers/resolveImageMessageParts.js +140 -0
- package/dist/api/helpers/slashCommands.d.ts +31 -0
- package/dist/api/helpers/slashCommands.js +129 -0
- package/dist/api/helpers/tokenUsage.d.ts +19 -0
- package/dist/api/helpers/tokenUsage.js +81 -0
- package/dist/api/routes/fileRoutes.d.ts +2 -0
- package/dist/api/routes/fileRoutes.js +108 -0
- package/dist/api/routes/lspDocumentRoutes.d.ts +4 -0
- package/dist/api/routes/lspDocumentRoutes.js +84 -0
- package/dist/api/routes/lspHierarchyRoutes.d.ts +4 -0
- package/dist/api/routes/lspHierarchyRoutes.js +122 -0
- package/dist/api/routes/lspPositionRoutes.d.ts +4 -0
- package/dist/api/routes/lspPositionRoutes.js +263 -0
- package/dist/api/routes/lspQueryRoutes.d.ts +4 -0
- package/dist/api/routes/lspQueryRoutes.js +679 -0
- package/dist/api/routes/lspResolveRoutes.d.ts +4 -0
- package/dist/api/routes/lspResolveRoutes.js +143 -0
- package/dist/api/routes/lspSemanticDocumentRoutes.d.ts +4 -0
- package/dist/api/routes/lspSemanticDocumentRoutes.js +98 -0
- package/dist/api/routes/semanticRoutes.d.ts +2 -0
- package/dist/api/routes/semanticRoutes.js +126 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +160 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +38 -0
- 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.
|