AgentsMD
Overview
agentsmd is an Eino ADK middleware that automatically injects the content of Agents.md files into the message sequence before each model call. The injected messages are persisted to the agent’s internal state by the framework, but idempotency checks (the Extra["__agentsmd_content__"] marker) ensure no duplicate injection. Since the injected content is fixed upon first appearance, it does not change with subsequent summarization/compression. Core value: Define system-level behavioral instructions and context for the Agent through Agents.md files (similar to Claude Code’s CLAUDE.md), without manually managing system prompt concatenation. Package path: github.com/cloudwego/eino/adk/middlewares/agentsmd
Quick Start
ctx := context.Background()
// 1. Create the agentsmd middleware
mw, err := agentsmd.New(ctx, &agentsmd.Config{
Backend: myBackend, // Implements the agentsmd.Backend interface
AgentsMDFiles: []string{"/project/agents.md"},
})
if err != nil {
panic(err)
}
// 2. Configure into the Agent
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Model: chatModel,
Handlers: []adk.ChatModelAgentMiddleware{mw},
})
Configuration Details
Config Struct
type Config struct {
Backend Backend
AgentsMDFiles []string
AllAgentsMDMaxBytes int
OnLoadWarning func(filePath string, err error)
}
Parameter Description
| Parameter | Type | Required | Default | Description |
Backend | Backend | Yes | — | File reading backend, responsible for actual file I/O |
AgentsMDFiles | []string | Yes | — | List of Agents.md file paths to load (at least one), loaded and injected in order |
AllAgentsMDMaxBytes | int | No | 0(unlimited) | Total byte limit for all files; subsequent files are skipped when exceeded, but each file is always loaded completely |
OnLoadWarning | func(string, error) | No | log.Printf | Callback for non-fatal errors (missing file, circular @import, depth exceeded, etc.) |
Validation Rules
New / NewTyped validates the Config during creation:
Configcannot be nilBackendcannot be nilAgentsMDFilesmust contain at least one pathAllAgentsMDMaxBytescannot be negative
Constructors
New — Standard Constructor
func New(ctx context.Context, cfg *Config) (adk.ChatModelAgentMiddleware, error)
Returns ChatModelAgentMiddleware (i.e., TypedChatModelAgentMiddleware[*schema.Message]), suitable for standard ChatModelAgent.
NewTyped — Generic Constructor
func NewTyped[M adk.MessageType](_ context.Context, cfg *Config) (adk.TypedChatModelAgentMiddleware[M], error)
Generic version supporting both *schema.Message and *schema.AgenticMessage message types. New internally calls NewTyped[*schema.Message].
Backend Interface
Interface Definition
type Backend interface {
Read(ctx context.Context, req *ReadRequest) (*FileContent, error)
}
Type Definitions
ReadRequest and FileContent are aliases of the same-named types in the github.com/cloudwego/eino/adk/filesystem package:
type ReadRequest = filesystem.ReadRequest
type FileContent = filesystem.FileContent
💡 Backend Implementation Requirements
- When a file does not exist, the error must wrap
os.ErrNotExist(makingerrors.Is(err, os.ErrNotExist)returntrue); the loader uses this to distinguish “file missing” from “real I/O error”- Other errors (permission denied, I/O errors) will abort the entire loading process and are not treated as warnings
- The
Readmethod should be concurrency-safe
@import Syntax
Agents.md files support the @path syntax for recursively including other files.
Syntax Format
# Project Instructions
You are a code assistant.
Please refer to the following specifications:
@rules/code-style.md
@rules/api-conventions.md
Matching Rules
The loader scans file content using the regex @([a-zA-Z0-9_.~/][a-zA-Z0-9_.~/\-]*), combined with the following filtering logic:
- Paths containing /: Treated directly as @import (e.g.,
@rules/style.md) - Paths without /: Only treated as @import when the extension is in the allowed list, otherwise ignored. Allowed extensions:
.md,.txt,.mdx,.yaml,.yml,.json,.toml. This design avoids misidentifying@someone,@example.com, etc. as import targets.
Resolution Behavior
| Rule | Description |
| Path resolution | Relative paths are resolved based on the current file's directory; absolute paths are used directly |
| Maximum recursion depth | 5 levels (exceeding this is skipped and triggers OnLoadWarning) |
| Circular reference detection | Paths already present in the current ancestor chain are skipped (triggers OnLoadWarning) |
| Global deduplication | The same file path is read and injected only once during the entire loading process |
| Original text preserved | Files referenced by @import are appended as separate paragraphs; the @pathtext in the original is not removed |
| Byte budget | Once cumulative bytes exceed AllAgentsMDMaxBytes, subsequent imports are skipped |
Directory Structure Example
project/
├── Agents.md # Main entry file
├── rules/
│ ├── code-style.md # @rules/code-style.md
│ ├── api-conventions.md # @rules/api-conventions.md
│ └── testing.md
└── context/
└── architecture.md
How It Works
Implementation Hook
The middleware implements the BeforeModelRewriteState method of the TypedChatModelAgentMiddleware interface (not WrapModel). This hook is triggered before each model call when the state is being rewritten.
Injection Flow
Message Sequence After Injection
[System] System prompt
[User] ← Agents.md content (with Extra marker)
[User] User history message 1
[Assistant] Assistant reply 1
[User] Current user message
Key Mechanisms
1. Persistent Injection + Idempotency Guarantee The framework persists the state returned by BeforeModelRewriteState to the agent’s internal state (st.Messages = state.Messages). Injected messages are marked with Extra["__agentsmd_content__"]; each time the hook is entered, it scans first — if the marker already exists, the original state is returned directly, avoiding duplicate injection. The effect is: content is injected and persisted on the first model call, and subsequent iterations do not re-insert. 2. Run-Level Caching Within the same Run(), content loaded for the first time is cached to RunLocal storage via adk.SetRunLocalValue. Subsequent model calls (e.g., multi-round tool calls) reuse the cache directly via adk.GetRunLocalValue. Each new Run() reloads, so file modifications take effect on the next Run. 4. Insertion Position Content is inserted as a User role message before the first User message in the message sequence. If there are no User messages in the sequence, it is appended to the end. 5. Content Formatting Loaded file content undergoes formatting:
- Wrapped in
<system-reminder>tags - Includes i18n header (prompting the model to follow instructions) and footer (noting context may not be relevant)
- Each file is displayed independently with a prefix of
File content: {path} (instructions): - Language (Chinese/English) is controlled globally via
adk.SetLanguage
Notes
Middleware Order
💡 It is recommended to place the agentsmd middleware after the summarization/compression middleware. This way, Agents.md content will not be summarized or compressed, and each model call receives the complete instructions.
Handlers: []adk.ChatModelAgentMiddleware{
summarizationMiddleware, // Summarize first
agentsMDMiddleware, // Then inject Agents.md
}
Error Handling
| Scenario | Behavior |
File does not exist (os.ErrNotExist) | Skip the file, trigger OnLoadWarning |
| Circular @import | Skip the circular file, trigger OnLoadWarning |
| @import depth exceeds 5 levels | Skip, trigger OnLoadWarning |
Cumulative size exceeds AllAgentsMDMaxBytes | Skip subsequent files, trigger OnLoadWarning(the first file is always loaded completely) |
| Permission denied / I/O error | Abort loading, return error |
| All file contents are empty | No injection, pass messages through unchanged |
Performance Considerations
- Set
AllAgentsMDMaxBytesappropriately to avoid injecting too much content that occupies the context window - Agents.md content is loaded only once per
Run()(run-level caching), but each new Run() reloads - Avoid @importing too many files; the recursion depth limit is 5 levels
Agents.md Writing Tips
- Keep content concise, only including instructions that genuinely affect model behavior
- Use @import to split by concern area (code conventions, API conventions, architecture descriptions, etc.)
- Avoid including large code examples or data to conserve context window space
- File content is delivered to the model wrapped in
<system-reminder>tags
FAQ
Q: Is Agents.md content saved to the conversation history?
A: Yes. The state returned by BeforeModelRewriteState is persisted by the framework. However, due to the idempotency check (Extra["__agentsmd_content__"] marker), content is only injected once on the first model call, and subsequent iterations skip it. It is recommended to place agentsmd after summarization to avoid the injected content being summarized or compressed.
Q: What happens if an Agents.md file does not exist?
A: The file is skipped, triggering the OnLoadWarning callback (default log.Printf), without affecting loading of other files.
Q: What directory are @import paths relative to?
A: Relative to the current file’s directory. For example, @rules/style.md in /project/Agents.md resolves to /project/rules/style.md.
Q: Will the same file be loaded multiple times if @imported from multiple files?
A: No. The loader maintains a global deduplication map (seen); the same path is read and injected only once.
Q: Are the @path references in the original text replaced?
A: No. Files referenced by @import are appended as separate paragraphs after the original text; the original content remains unchanged.
Q: What is the difference between New and NewTyped?
A: New returns ChatModelAgentMiddleware (i.e., TypedChatModelAgentMiddleware[*schema.Message]), suitable for standard Agents. NewTyped is the generic version that additionally supports the *schema.AgenticMessage type for Agentic Model scenarios.