My Workflow
graph TB
A["Agent"] --> U["Utility.app"]
U --> M["Local MCP Service"]
U --> P["Proxy Gateway"]
U --> G["Dependency Graph"]
U --> K["Knowledge Panel"]
M --> D1["Memory"]
M --> D6["Built-in Tools"]
P --> D8["Dynamic Prompt Injection"]
P --> D9["Token Analysis & Governance"]
K --> D4["Knowledge Pool"]
To keep you reading, I'll start with a few questions about your experience with Agents:
- Have you ever lost context across sessions, or found the Agent's understanding inconsistent?
- Do you want the Agent to remember memories and content produced during a conversation?
- Have you run into long-context contamination? The longer the conversation, the lower the model's instruction-following rate and overall intelligence.
- When developing a project, does the Agent often need to search the codebase because it lacks a global view?
- Is the link between the Agent and the upstream LLM a black box that's hard to observe? Token consumption is unknown and hard to govern.
- Now that you use Agents more, do you feel less desire to explore knowledge, read articles, or read source code?
If any of these sound familiar, take a look at how I built my own workflow.
By the way, the tools I use are Claude Code + Utility.app connected to an LLM API.
Utility.app is a core tool I built for my own use.
Memory: The Foundation of Cross-Context Understanding
When you use multiple Agents, develop across sessions, or produce knowledge inside a conversation, what do you realize you need?
My answer is: a shared memory. We need a memory that records work progress and conversational knowledge, and every consumer uses the same memory. Like the human brain, everything starts from one memory, creating behavioral consistency.
So what is the best form of memory for an Agent?
A local MCP service. The MCP protocol is designed to be pluggable, so it can be connected to any Agent that supports MCP and consumed by them.
With these two things, we can solve two problems:
- Context retention and consistent understanding
- Knowledge storage
That's only the obvious value. Behind the scenes, we can also think about and analyze these memories, distill them into better content, or generate reports from them.
In practice, this becomes a three-layer memory architecture:
graph TB
A["Agent"] -->|"tools/call"| M["MCP Service"]
M -->|"result"| A
M --> R["recall"]
M --> W["remember"]
subgraph MEM["Three-Layer Memory"]
H["Hot Layer<br/>Session Memory"]
C["Cold Layer<br/>Long-Term Knowledge"]
N["Markdown Notes"]
end
W --> H
R --> H
R --> C
R --> N
The hot layer keeps recent, warm memories; the cold layer keeps long-term useful knowledge; and finally everything settles into the notes layer for permanent storage and easy human reading.
remember / recall are two built-in tools of the local MCP service, used to write and retrieve memories. The Agent operates on memory by calling these two tools.
Proxy Gateway: The Key to Extensibility
The memory solves cross-session understanding and shared memory across multiple Agents. Now let's focus on long-context contamination and the black-box system problem.
It's easy to see why a Proxy Gateway breaks the black box: insert a proxy layer between the Agent and the upstream LLM provider, and the black box becomes transparent. But why does long-context contamination also need a proxy layer?
In a long conversation, the initial system prompt, rules, or tool descriptions are very easily contaminated by later context. The model's attention shifts to the latest content and ignores the system prompt, rules, and tool descriptions. The consequences can be:
- Behavior drift
- Lower instruction-following rate
- Reduced willingness to call tools
- Lower intelligence
The usual fix is to start a new conversation, which I find annoying. So I found another way: dynamic prompt injection.
graph TB
A["Agent"] -->|"request"| P["Proxy Gateway"]
S["Prompts"] -->|"read rules"| P
P -->|"inject prompt"| B["Every real user message"]
B -->|"forward"| U["Upstream LLM"]
U -->|"response"| P
P -->|"response"| A
The idea is simple. Since the LLM pays less attention to old messages, inject the core prompt into every new user message so it gets the highest priority.
Why inject into user messages instead of the system prompt? This decision comes from long observation. With a more capable LLM like DeepSeek, injecting an uncached system prompt usually isn't a big problem. But for a less capable LLM like Xiaomi MiMo, it prefers to follow the latest user message.
This works very well, but the cost is obvious: injecting the same content into every user message increases token consumption. So dynamically injected prompts should be lightweight, core, and globally applicable. Project-specific prompts are better placed in AGENT.md / CLAUDE.md.
Token Analysis and Governance: The Essential Step for Saving Money
Another core value of the proxy is that the black-box system becomes observable. We can record each request's input and output, the model's success rate, latency, and cache hit rate. These metrics are valuable: we can use them to analyze and quantify token usage, and then govern accordingly.
Over a long period of governance, I found that only a few things really matter:
- Improve cache hit rate
- Do not break the cache
- Reduce input
Do not break the cache is an important prerequisite. The proxy layer can manipulate messages before forwarding them upstream. The obvious idea is to compress, trim, or remove messages before forwarding to reduce input tokens. That is not entirely correct, because it breaks the cache, and every message that falls out of the cache is billed at the non-cached rate.
When governing tokens, analyze from a cost perspective, not from the raw number of input tokens.
In other words, when the cost of a large cached input exceeds the cost of breaking the cache through trimming, trimming historical messages can be worth considering.
Reduce input is easy to understand. An Agent usually makes many tool calls, and those tools produce a large amount of token input. Some output is used only once but carried in every subsequent turn. Even at the cached price, the cost grows quickly as the volume explodes.
So we need to slim down tool output. For example, only checking Bash output for warning / error.
The token governance I've implemented:
- Reduce duplicate content: when the same tool output appears repeatedly in one session, replace every occurrence after the first with a short note, keeping the first full copy.
- Trim shell output: keep the first 70% and last 30% of very long command output, insert a truncation marker in the middle, and minify JSON losslessly.
- Make injection idempotent: deterministic front injection keeps the cache intact.
- Limit tool results: reduce the default response size of built-in tools.
graph TB
A["Agent"]
A -->|"model request"| P["Proxy Gateway"]
P --> Q["Request-side governance<br/>rule injection · content dedup · shell trimming"]
Q --> U["Upstream Provider"]
U --> R["Response-side governance<br/>usage recording · response annotation · context warning"]
R --> D["Usage DB"]
R --> A
A -->|"tool call"| M["MCP Service"]
M --> L["Tool result limits"]
L --> A
Dependency Graph: A Global View
Anyone who has used an Agent like Claude Code to develop a project has probably felt this: the Agent lacks global awareness. It may implement a component or utility this time, forget it the next time a similar need arises, and implement it again. Or it constantly needs to search the whole project for a component or utility.
This is because it lacks global awareness, or what I call the project's dependency graph.
To address this, I added a dependency-graph scanning feature to Utility.app, exposed as a CLI command:
utility -a "agent"
# eg. utility -a claude
It does several things: start the scanner, scan project symbols, build the dependency graph, and store it in the .utility/deps.sqlite database. Then it starts the Agent and enters the Agent terminal. Control returns to the Agent, while Utility.app watches for file changes in the background and updates the dependency graph.
The Agent can then query it through the project_search / project_structure / project_exports / project_packages tools exposed by the local MCP service.
graph TB
A["Agent"] --> Q["Query layer<br/>project_search · project_structure<br/>project_exports · project_packages"]
Q --> DB[".utility/deps.sqlite<br/>directories · files · symbols · packages · FTS"]
S["ProjectScanner<br/>full scan"] --> E["Symbol extraction"]
W["FileWatcher<br/>incremental scan"] --> E
E --> AST["AST scripts<br/>Python · Babel"]
E --> RE["Regex extractors<br/>Swift · JS · TS · Vue · Java · Kotlin"]
AST --> DB
RE --> DB
Knowledge Panel: Proactively Pushing Fresh Knowledge
This is about personal growth. The key problem is that the more I use Agents, the less willing I am to actively read and explore new knowledge. I wanted a panel that pops up proactively and shows content I've never seen before, or content I'm interested in, so I'd spend some time reading it.
Why content I've never seen before? We already have a memory that stores knowledge, so why not extract something from it and show it?
The key issue is that everything in memory comes from conversations, which means it's already in my head. Seeing it again doesn't excite the brain. So we need something unknown and interesting.
It works like this: we configure some topics of interest, the LLM crawls content related to those topics, and when the interval elapses, it picks one piece of knowledge to display in the knowledge panel.
graph TB
U["User"] --> P["Knowledge Panel<br/>Theme Cards · Fragments"]
subgraph REFILL["Background Refill"]
T["Timer"] -->|"refill when pool is not full"| E["Knowledge Engine"]
W["Web Retrieval"] --> E
E --> L["LLM Generation"]
L --> C["Self-check"]
end
C -->|"write card"| DB["Knowledge Pool"]
DB -->|"read"| P
These core capabilities live in my Utility.app, which I've open-sourced at yuanyxh/utility.
Top comments (0)