Watney4 aims to be the platform for experimentation. This project iterates on many of the ideas from the other agent harnesses I've built. Just as the first draft of anything isn't perfect, neither were mine. Thomas, Flash, and Martha were the first three agents I built, with varying degrees of success.
Why build an agent at all? I think the power of an LLM combined with some clever architecture amplifies many of the qualities of the person using it. It's like any tool, really. Put a chisel in a mason's hands and he makes something beautiful. Put that same chisel in my hands and I wouldn't rival the tradesman. There are plenty of agents out there, but none are tuned to how I think about the world or that I feel 100% comfortable giving access to my system.
The agent architecture is essentially an event engine. For you event driven zealots, you can feel validated.
Event sources publish events to an Event Broker. The Event Broker receives, filters, and routes events based on subscriptions. Subscribers receive only the events they subscribe to. The agent loop acts as the Event Broker, while the various tool calls, interfaces, and the LLM act as subscribers. It's a pleasant architecture to work on, and it's efficient.
It's Kotlin, structured about as you'd expect.
src/main/kotlin/
├── core/ # Main.kt, Agent.kt, Watney4.kt — the personality
├── interfaces/ # Cli.kt, DiscordBot.kt
├── tools/ # 18 tools. Tool.kt + implementations for each
└── utils/ # Providers, memory, context, voice, config, cron, whisper, researchThe agent loop is an infinite coroutine that reads IncomingMessage objects from a shared coroutine channel sequentially.
- 1.
Read a message from the inbox.
- 2.
Handle meta commands directly (
/exit,/clear,/research,/model). - 3.
Tag non-user messages with
[cron]or[reminder]so the agent can tell who's talking. - 4.
Check the Whisper buffer for passive observations.
- 5.
Query the LLM with the full context and tool definitions.
- 6.
If the LLM calls tools, execute each one and append the results.
- 7.
Loop back to step 5, up to a maximum of 15 iterations.
- 8.
Send the final response.
The agent manages its own context through tools. It doesn't need me to tell it when things are getting long. Some models still struggle with this. Progress messages (running...) are buffered and flushed every two seconds. No one wants to watch a stream of running... messages.
Messages arrive from multiple interfaces: CLI, Discord DM, voice, the cron scheduler, and reminders (implemented as delayed coroutines).
There are two passive watchers that observe what's happening and inject context: VaultWatcher and ConversationWatcher. They create passive awareness for the agent by scanning my files and conversations, extracting keywords and topics that are injected into the system prompt. Ambient awareness.
There are three model providers, all swappable at runtime with a slash command.
ToolRunner was an attempt to modularize the tool system into MCP. It worked, and it probably could have been successful, but maintaining two repositories became frustrating. I no longer see enough benefit to justify moving in that direction.
With Thomas's memory, there were some things I got right. The high level idea was spot on. I wanted to engineer the context window so it contained only one active process at a time. However, Thomas's memory platform was primitive.
I kept everything. Every interaction was stored forever. While that was interesting from an experimentation perspective, it wasn't actually useful. Searching became increasingly difficult, and manipulating the data was awkward. Long term memory was built around a convoluted key system that I tried to teach the LLM. It worked. Not particularly well, but it worked. Looking back, it didn't need to be that complicated.
Watney4's memory system is considerably simpler. KISS is a good rule for most things. It's a good rule in engineering.
The goal isn't to build the most sophisticated memory system possible. It's to build one that's predictable.
At a high level, the architecture functions similarly to human memory. Yes, I just said it was simple.
The context window acts as working memory. Information that remains useful throughout the week becomes long term memory. At the end of the week, that memory is distilled into an archive that can be searched semantically.
The architecture has three tiers: working memory, long term memory, and the archive. Each tier has a single responsibility.
Working memory is nothing more than a List<ChatMessage>. The last fifty messages are persisted in the messages table so conversations survive restarts. This is the only information the model actively reasons over. Everything else is compressed.
Long term memory exists only for the current week. Every night at 3 AM CET, or whenever the conversation is cleared, the day's interactions are consolidated into a small number of perspective summaries. Rather than generating a single generic summary, the system stores five different perspectives of the conversation: factual (dates, numbers, and decisions), preferences (likes, dislikes, and tastes), projects (status, plans, and blockers), goals (milestones and aspirations), and personal (relationships and routines).
When Watney starts, the current week's summaries are loaded into the context window as a system message. The model begins with the important context instead of replaying hundreds of previous messages.
Once a week, the previous week's memories are archived.
The archival process is straightforward.
- 1.
Generate the five perspective summaries.
- 2.
Generate semantic tags.
- 3.
Generate embeddings.
- 4.
Store everything in
ltm_archive. - 5.
Generate a weekly highlight that seeds the following week.
This is where semantic search becomes useful. There is no separate vector database. Embeddings are stored directly in SQLite as serialized float arrays. For a project of this size, introducing another service would only add complexity without providing much value.
Searching is simply cosine similarity over the stored embeddings.
I considered adding a decay rate similar to how human memory fades over time. Ultimately, it's a machine. Forgetting serves people because our brains have limited capacity. Watney doesn't have that limitation. Once information has been archived, there is little benefit in intentionally making it harder to retrieve.
The persistence layer is intentionally uncomplicated. Messages, weekly summaries, archival memories, and interaction logs all live in SQLite.
Some observations
With Thomas's memory, there were some things I got right. The high level idea was correct. I wanted to engineer the context window so it contained only one active process at a time. But Thomas's memory platform was primitive. I kept everything. Every interaction was stored forever, and while that was interesting, it wasn't necessary. It was difficult to search, difficult to manipulate, and the long term memory relied on a convoluted key system that I tried to teach the LLM. It didn't need to be that complicated.
If I had to sum up what I've learned across four agents, it's this: the architecture matters more than the model. A mediocre model with good tools, good memory, and an agent loop that doesn't get stuck will outperform a great model bolted onto a mess.
Thomas taught me that memory needs structure. Flash taught me that tools need boundaries. Martha taught me that context management is everything. Watney4 is the first one where all three feel right.
It's also the first agent that self identifies as it/its. I didn't program that in. It simply started referring to itself that way one day. I noticed, shrugged, and moved on. It felt right.
Watney isn't a person. It's a process. A persistent one that happens to talk back.