Saturday, December 6, 2025

The AGENTS.md Guideline: Maximize AI Performance with Conciseness

 


Core Principles

These principles explain why conciseness and clarity are essential for high-performing agents.
  • Token Cost: Verbose explanations multiply token usage across many interactions
  • Signal-to-Noise Ratio:
    • Excessive detail dilutes critical information
    • Agents may struggle to identify which rules are most important
    • Too many examples can distract from the core pattern
  • Processing Overhead:
    • Longer documents = more cognitive load
    • Simple, direct imperatives are faster to process than lengthy explanations

Actionable Best Practices

  • Be Concise: Eliminate verbose statements and any language or content that is only relevant to human readers.
  • Focus on New Learning: Only insert rules that correct known AI mistakes or introduce "new" learning. Rules the AI can already generate on its own are typically noise.
  • Avoid Redundancy: If a rule or principle is stated clearly once, do not repeat it.
  • Use Minimal, High-Quality Examples: One good, representative example is more effective than three similar ones.
  • Prioritize High-Level Abstraction: Focus on stable, high-level principles over low-level, volatile details like method names.

The Maintenance Burden

  • Focus on Backbone Principles: Guide the AI with high-level, enduring principles rather than specific, volatile details. Code evolves quickly, and outdated specific guidance becomes a maintenance burden and can actively mislead the AI.
  • Team Maintainability: Long, verbose AGENTS.md files create maintenance problems:
    • Developers overwrite each other's rules without noticing
    • Hard to tell if changes improve or degrade agent performance
    • Outdated specifics mislead the agent

Conclusion

A well-crafted AGENTS.md is a powerful tool for developing highly effective AI agents. If you let AI generate the AGENTS.md, try creating AGENTS.md using the following rule:

  • Make AGENTS.md concise:
  • Remove human-only instructions
  • Eliminate redundant or duplicate rules



Tuesday, April 20, 2021

Thread Switch Performance difference between Thread, ThreadPool, and Coroutine

TL;DR

Thread Switch Time

private fun multiThreadTest_coroutine() {
val start = System.currentTimeMillis()
GlobalScope.launch(Dispatchers.IO) {
Log.d("thread_perf", "Coroutine 1: ${System.currentTimeMillis() - start}")
GlobalScope.launch(Dispatchers.Main) {
Log.d("thread_perf", "Coroutine 2: ${System.currentTimeMillis() - start}")
GlobalScope.launch(Dispatchers.IO) {
Log.d("thread_perf", "Coroutine 3: ${System.currentTimeMillis() - start}")
GlobalScope.launch(Dispatchers.Main) {
Log.d("thread_perf", "Coroutine 4: ${System.currentTimeMillis() - start}")
GlobalScope.launch(Dispatchers.IO) {
Log.d("thread_perf", "Coroutine 5: ${System.currentTimeMillis() - start}")
GlobalScope.launch(Dispatchers.Main) {
Log.d("thread_perf", "Coroutine 6: ${System.currentTimeMillis() - start}")
}
}
}
}
}
}
}
FirstD/thread_perf: Coroutine 1: 32D/thread_perf: Coroutine 2: 40D/thread_perf: Coroutine 3: 41D/thread_perf: Coroutine 4: 42D/thread_perf: Coroutine 5: 42D/thread_perf: Coroutine 6: 43SecondD/thread_perf: Coroutine 1: 0D/thread_perf: Coroutine 2: 2D/thread_perf: Coroutine 3: 2D/thread_perf: Coroutine 4: 3D/thread_perf: Coroutine 5: 4D/thread_perf: Coroutine 6: 4ThirdD/thread_perf: Coroutine 1: 0D/thread_perf: Coroutine 2: 1D/thread_perf: Coroutine 3: 1D/thread_perf: Coroutine 4: 2D/thread_perf: Coroutine 5: 2D/thread_perf: Coroutine 6: 2
val EXECUTOR: Executor = Executors.newCachedThreadPool()

private fun multiThreadTest_threadPool() {
val start = System.currentTimeMillis()
EXECUTOR.execute {
Log.d("thread_perf", "ThreadPool 1: ${System.currentTimeMillis() - start}")
runOnUiThread {
Log.d("thread_perf", "ThreadPool 2: ${System.currentTimeMillis() - start}")
EXECUTOR.execute {
Log.d("thread_perf", "ThreadPool 3: ${System.currentTimeMillis() - start}")
runOnUiThread {
Log.d("thread_perf", "ThreadPool 4: ${System.currentTimeMillis() - start}")
EXECUTOR.execute {
Log.d("thread_perf", "ThreadPool 5: ${System.currentTimeMillis() - start}")
runOnUiThread {
Log.d("thread_perf", "ThreadPool 6: ${System.currentTimeMillis() - start}")
}
}
}
}
}
}
}
FirstD/thread_perf: ThreadPool 1: 2D/thread_perf: ThreadPool 2: 8D/thread_perf: ThreadPool 3: 9D/thread_perf: ThreadPool 4: 9D/thread_perf: ThreadPool 5: 12D/thread_perf: ThreadPool 6: 12SecondD/thread_perf: ThreadPool 1: 0D/thread_perf: ThreadPool 2: 8D/thread_perf: ThreadPool 3: 8D/thread_perf: ThreadPool 4: 9D/thread_perf: ThreadPool 5: 9D/thread_perf: ThreadPool 6: 9ThirdD/thread_perf: ThreadPool 1: 1D/thread_perf: ThreadPool 2: 14D/thread_perf: ThreadPool 3: 14D/thread_perf: ThreadPool 4: 15D/thread_perf: ThreadPool 5: 15D/thread_perf: ThreadPool 6: 15
private fun multiThreadTest_thread() {
val start = System.currentTimeMillis()
Thread {
Log.d("thread_perf", "Thread 1: ${System.currentTimeMillis() - start}")
runOnUiThread {
Log.d("thread_perf", "Thread 2: ${System.currentTimeMillis() - start}")
Thread {
Log.d("thread_perf", "Thread 3: ${System.currentTimeMillis() - start}")
runOnUiThread {
Log.d("thread_perf", "Thread 4: ${System.currentTimeMillis() - start}")
Thread {
Log.d("thread_perf", "Thread 5: ${System.currentTimeMillis() - start}")
runOnUiThread {
Log.d("thread_perf", "Thread 6: ${System.currentTimeMillis() - start}")
}
}
.start()
}
}
.start()
}
}
.start()
}
FirstD/thread_perf: Thread 1: 1D/thread_perf: Thread 2: 13D/thread_perf: Thread 3: 15D/thread_perf: Thread 4: 15D/thread_perf: Thread 5: 17D/thread_perf: Thread 6: 17SecondD/thread_perf: Thread 1: 1D/thread_perf: Thread 2: 9D/thread_perf: Thread 3: 11D/thread_perf: Thread 4: 11D/thread_perf: Thread 5: 12D/thread_perf: Thread 6: 13ThirdD/thread_perf: Thread 1: 1D/thread_perf: Thread 2: 14D/thread_perf: Thread 3: 15D/thread_perf: Thread 4: 15D/thread_perf: Thread 5: 16D/thread_perf: Thread 6: 17

Conclusion

Friday, April 16, 2021

Network Security for Android Developers (MitM attack & SSL Pinning)

 This is a summary of what I have been learning about Network Security from an Android developer perspective. How to enhance your mobile security.

Why is SSL/TLS required? What is a man-in-the-middle(MitM) attack?

  • See your auth token for any API calls you make to the server
  • Alter information so that you could send money to the wrong account (see above image)

SSL vs TLS

SSL was replaced by TLS. TLS is the superior version of SSL. Although SSL should no longer be used, the name is still widely being used.

SSL version updates

The SSL version has been updated from SSL 1.0, 2.0, 3.0 to TLS 1.0, 1.1, 1.2, 1.3. The security vulnerabilities continuously have been found and updates have been made.
GlobalSign warns to disable TLS 1.0 and below. There are more and more enforcements for payment services to use at least TLS 1.2. Stripe, for example, blocked below TLS1.2 as of June 13, 2018.

Headache of supporting minimum TLS version

So what’s the issue? Just use the highest TLS version. There’s an issue for older devices.

Minimum TLS versions

Refer following for TLS version support per Android version:

What happens during a TLS handshake?

Charles

SSL Pinning comes to rescue

By providing the “true” server public key information, you can restrict the application to only trust the provided public keys for certain domain names.

How to get the server’s SHA256 public key?

  1. Use https://github.com/scottyab/ssl-pin-generator to get the SSL pins.

How to enable SSL Pinning?

Enabling SSL Pinning using OkHttp is following:

val certificatePinner = CertificatePinner.Builder()
.add(
"www.example.com",
"sha256/ZC3lTYTDBJQVf1P2V7+fibTqbIsWNR/X7CWNVW+CEEA="
).build()
val okHttpClient = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()

For Android 7.0+

For Android 7.0+, you can add it to the networkSecurityConfig config file.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<pin-set>
<pin digest="SHA-256">ZC3lTYTDBJQVf1P2V7+fibTqbIsWNR/X7CWNVW+CEEA=</pin>
<pin digest="SHA-256">GUAL5bejH7czkXcAeJ0vCiRxwMnVBsDlBMBsFtfLF8A=</pin>
</pin-set>
</domain-config>
</network-security-config>

What is the expected result?

When the hacker tries the man-in-the-middle(MitM) attack, the application will result in SSL Pinning mismatch and throw an exception.

Certificate Expiry

Certificate expires. We don’t want older app versions to stop working because the server certificate is renewed. So you might want to avoid hardcoding the SSL pin inside the application code. Make it so that it dynamically downloads the SSL pin from the server.

Tuesday, October 13, 2020

Android Offline-first Architecture using MVVM with Flow

 

Offline First Architecture

There may be many meanings tied to the "offline-first" architecture. The definition here means the app always loads from the cache "first" for the fast performance. Once the data from the cache is displayed, it updates again the difference (if any) using the data from the server.

Result:
Even if getting information from the server is slow, the app instantly loads the screen from the cache instead of showing the "loading" screen. Also, if the device is offline, the app will still work as long as there is a cache data (the page has been loaded before).


Folder Structure

I have grouped Fragment & ViewModel into one folder but separated the Repository into different folders. 

Reason:
In most cases you will have 1-to-1 ViewModel per Fragment. However, sometimes data(repository) can be shared between different View/ViewModels.
For example, both User List and User Detail screens share the same User data.





























ViewModel <-> Repository Relationship

With the typical MVVM architecture, the Repository offers the suspend function and the ViewModel will have the LiveData calling for the data. 



For offline first architecture we will have the Repository returning the "streams" of data which is called Flow. Flow allows it to return data more than once. 


Flow


Flow is similar to RxJava. The learning curve, in my opinion, is lower than the RxJava. Like RxJava, Flow also offers very useful operators like zip, combine, flatMapConcat, flatMapMerge, debounce, onStart, onComplete, etc. The fact that Flow can be directly converted into LiveData makes it really easy to adapt if your project already uses the MVVM architecture with LiveData.

Two-tier Cache

I use two-tier cache:
1. Memory Cache
2. Local Cache (DB or SharedPreference)

There are advantage and disadvantage of using two-tier cache:

Advantage:
  • Speed: Even reading the local DB takes time. It requires you to switch the worker thread. Coroutine has made the thread switch much faster than the original way but it can still cause the UI flickering issue during the page load. The flickering issue is more evident when the placeholder UI is not empty and the height is different from the UI with data. This is caused by the thread switch: UI (show placeholder UI / trigger event) -> Worker (read data) -> UI (display result). Reading from the memory can be done on UI thread without switching the threads.
Disadvantage:
  • Complexity: It adds an extra complexity to maintain two caches up-to-date with the latest data. 
  • Out of memory error: If you're working with the large size data, having all of them to be saved onto the memory cache does not make sense. They will eventually add up and the app will run out of memory. So you have to define the "scope" of how much and how long data should be saved on to memory. For example, you can create the memory cache as a LRU memory: only saving the last used data up-to the cache size pre-defined. You can also set a time limit. Some data is no longer valid after a certain time. You can wipe the cache data after a certain time so that the new data from the server has to be called.

2 Types Of Read Call

There are two types read call:
  1. Cache-and-server: First read from the cache (either memory or local) and return the result. Then read from the server and update the difference.
  2. Cache-or-server: If data from the cache exists, then return the cache data. If cache has no data, then return the data from the server.

Cache-and-server
Following is a simple example cache-and-server read using Flow.
    override fun getUsers(): Flow<ArrayList<User>?> {
return flow {

// To show the data quickly as possible, first show data from cache, then from server

var hasCacheData = false
// 1. Emit first from either from memory or local
// Memory
if (currentUserList != null) {
hasCacheData = true
emit(currentUserList!!)
} else {
// Local
val localCache = myDao.getUserList()
if (localCache.isNotEmpty()) {
val users =
ServiceLocator.getGson()
.fromJson(localCache[0].payload, UserList::class.java)
if (users?.value != null) {
// Save to memory
currentUserList = users.value!!

hasCacheData = true
emit(users.value!!)
}
}
}

// 2. Emit again from the network (for the latest update)

// Network
val apiResult = ServiceLocator.provideJsonHolderService().getUsers()

// Saving to DB takes time. Create another thread to save to DB.
GlobalScope.launch(Dispatchers.IO) {
saveToLocal(apiResult)
}

// Do not overwrite cache value with null network value (offline case)
// If cache has no data, then it's ok to return null.
if (apiResult != null || !hasCacheData)
emit(apiResult)


}.flowOn(Dispatchers.IO)
}
Cache-and-server method is useful for the first time launch. The app can display the screen almost instantly with the data in cache and then apply the difference soon after (if any). Also, since it displays data from cache first, the app displays the result fast even if the device is offline or in a low connectivity state.

Cache-or-server
Following is a simple example cache-or-server read using Flow.
override fun getUsersAllowCache(): Flow<ArrayList<User>?> {
return flow {

// If cache is found, don't try to download from server

// 1. Memory
if (currentUserList != null) {
emit(currentUserList!!)
} else {
// 2. Local
var hasLocalData = false
val localCache = myDao.getUserList()
if (localCache.isNotEmpty()) {
val users =
ServiceLocator.getGson()
.fromJson(localCache[0].payload, UserList::class.java)
if (users?.value != null) {
// Save to memory
currentUserList = users.value!!

hasLocalData = true
emit(users.value!!)
}
}

if (!hasLocalData) {
// 3. Network
val apiResult = ServiceLocator.provideJsonHolderService().getUsers()

// Saving to DB takes time. Create another thread to save to DB.
GlobalScope.launch(Dispatchers.IO) {
saveToLocal(apiResult)
}
emit(apiResult)
}
}
}.flowOn(Dispatchers.IO)
}
Cache-or-server method is useful for the screen re-entry or when you know you don't need an updated value from the server. 

Continuous Update From Server
Examples like current stock price or a number of likes in a post requires to be frequently auto refreshed. This can be done using the while loop and the delay:
override fun getUsers(): Flow<ArrayList<User>?> {
return flow {

// To show the data quickly as possible, first show data from cache, then from server

var hasCacheData = false
// 1. Emit first from either from memory or local
// Memory
if (currentUserList != null) {
hasCacheData = true
emit(currentUserList!!)
} else {
// Local
val localCache = myDao.getUserList()
if (localCache.isNotEmpty()) {
val users =
ServiceLocator.getGson()
.fromJson(localCache[0].payload, UserList::class.java)
if (users?.value != null) {
// Save to memory
currentUserList = users.value!!

hasCacheData = true
emit(users.value!!)
}
}
}

// 2. Emit again from the network (for the latest update)
// If API call is like a current stock price, we should auto refresh every 1 seconds. Then we use "while" loop with "delay(1000)"
while (true) {

// Network
val apiResult = ServiceLocator.provideJsonHolderService().getUsers()

// Saving to DB takes time. Create another thread to save to DB.
GlobalScope.launch(Dispatchers.IO) {
saveToLocal(apiResult)
}

// Do not overwrite cache value with null network value (offline case)
// If cache has no data, then it's ok to return null.
if (apiResult != null || !hasCacheData)
emit(apiResult)

delay(1000)
}


}.flowOn(Dispatchers.IO)
}
The while loop will stop when there is no more observer (i.e. screen change).

ViewModel

The ViewModel code can be as simple as following:

fun getUserList(): LiveData<ArrayList<User>?> = userRepo.getUsers().asLiveData()

Use .asLiveData() to convert a Flow into a LiveData.

Using .onStart(), .onEach(), and .onComplete(), the viewModel can display the spinner:

fun getUserList(): LiveData<ArrayList<User>?> = userRepo.getUsers()
.onStart { _spinner.value = true }
.onEach { _spinner.value = false }
.catch { e -> Timber.e(e) }
.asLiveData()


Also,  the data can be transform into another type using .map() inside the viewModel:

fun getUserDetail(id: String): LiveData<User?> =
userRepo.getUsersAllowCache()
.map {
if (it != null) {
for (user in it) {
if (user.id == id) {
return@map user
}
}
}
return@map null
}.asLiveData()

Full Source Code

Following link is the source code of the Offline-first Architecture using MVVM with Flow Sample app.
https://github.com/jclova/MvvmWithFlow