9.4 NoteTool: Structured Notes
NoteTool is a structured external memory component provided for "long-horizon tasks". It uses Markdown files as carriers, with YAML front matter in the header to record key information, and the body to record status, conclusions, blockers, and action items. This design combines human readability, version control friendliness, and ease of re-injecting into context, making it an important tool for building long-horizon agents.
9.4.1 Design Philosophy and Application Scenarios
Before diving into implementation details, let's first understand the design philosophy and typical application scenarios of NoteTool.
(1) Why do we need NoteTool?
In Chapter 8, we introduced MemoryTool, which provides powerful memory management capabilities. However, MemoryTool mainly focuses on conversational memory—short-term working memory, episodic memory, and semantic memory. For project-based tasks that require long-term tracking and structured management, we need a lighter, more human-friendly recording method.
NoteTool fills this gap by providing:
- Structured recording: Uses Markdown + YAML format, suitable for both machine parsing and human reading and editing
- Version friendly: Plain text format, naturally supports version control systems like Git
- Low overhead: No complex database operations required, suitable for lightweight state tracking
- Flexible categorization: Flexibly organize notes through
typeandtags, supporting multi-dimensional retrieval
(2) Typical Application Scenarios
NoteTool is particularly suitable for the following scenarios:
Scenario 1: Long-term Project Tracking
Imagine an agent is assisting with a large codebase refactoring task, which may take days or even weeks. NoteTool can record:
task_state: Current stage task status and progressconclusion: Key conclusions after each stage endsblocker: Problems and blocking points encounteredaction: Next action plan
# Record task status
notes.run({
"action": "create",
"title": "Refactoring Project - Phase 1",
"content": "Completed refactoring of data model layer, test coverage reached 85%. Next will refactor business logic layer.",
"note_type": "task_state",
"tags": ["refactoring", "phase1"]
})
# Record blocker
notes.run({
"action": "create",
"title": "Dependency Conflict Issue",
"content": "Found some third-party library versions incompatible, need to resolve. Impact scope: 3 modules in business logic layer.",
"note_type": "blocker",
"tags": ["dependency", "urgent"]
})Scenario 2: Research Task Management
An intelligent research assistant conducting literature review can use NoteTool to record:
- Core viewpoints of each paper (
conclusion) - Topics to be investigated in depth (
action) - Important references (
reference)
Scenario 3: Cooperation with ContextBuilder
Before each round of dialogue, the Agent can retrieve relevant notes through search or list operations and inject them into the context:
# In Agent's run method
def run(self, user_input: str) -> str:
# 1. Retrieve relevant notes
relevant_notes = self.note_tool.run({
"action": "search",
"query": user_input,
"limit": 3
})
# 2. Convert note content to ContextPacket
note_packets = []
for note in relevant_notes:
note_packets.append(ContextPacket(
content=note['content'],
timestamp=note['updated_at'],
token_count=self._count_tokens(note['content']),
relevance_score=0.7,
metadata={"type": "note", "note_type": note['type']}
))
# 3. Pass notes when building context
context = self.context_builder.build(
user_query=user_input,
custom_packets=note_packets,
...
)9.4.2 Storage Format Detailed Explanation
NoteTool adopts a hybrid format of Markdown + YAML, which balances structure and readability.
(1) Note File Format
Each note is an independent .md file with the following format:
---
id: note_20250119_153000_0
title: Project Progress - Phase 1
type: task_state
tags: [refactoring, phase1, backend]
created_at: 2025-01-19T15:30:00
updated_at: 2025-01-19T15:30:00
---
# Project Progress - Phase 1
## Completion Status
Completed refactoring of data model layer, main changes include:
1. Unified entity class naming conventions
2. Introduced type hints to improve code maintainability
3. Optimized database query performance
## Test Coverage
- Unit test coverage: 85%
- Integration test coverage: 70%
## Next Steps
1. Refactor business logic layer
2. Resolve dependency conflict issues
3. Increase integration test coverage to 85%Advantages of this format:
- YAML metadata: Machine-parsable, supports precise field extraction and retrieval
- Markdown body: Human-readable, supports rich formatting (headings, lists, code blocks, etc.)
- Filename as ID: Simplifies management, each note's filename is its unique identifier
(2) Index File
NoteTool maintains a notes_index.json file for quick retrieval and management of notes:
{
"note_20250119_153000_0": {
"id": "note_20250119_153000_0",
"title": "Project Progress - Phase 1",
"type": "task_state",
"tags": ["refactoring", "phase1", "backend"],
"created_at": "2025-01-19T15:30:00",
"updated_at": "2025-01-19T15:30:00",
"file_path": "./notes/note_20250119_153000_0.md"
}
}The role of this index file:
- Quick retrieval: No need to open each file, search directly from the index
- Metadata management: Centrally manage metadata for all notes
- Integrity check: Can detect missing or corrupted files
9.4.3 Core Operations Detailed Explanation
NoteTool provides seven core operations covering the complete lifecycle management of notes.
(1) create: Create Note
def _create_note(
self,
title: str,
content: str,
note_type: str = "general",
tags: Optional[List[str]] = None
) -> str:
"""Create note
Args:
title: Note title
content: Note content (Markdown format)
note_type: Note type (task_state/conclusion/blocker/action/reference/general)
tags: Tag list
Returns:
str: Note ID
"""
from datetime import datetime
# 1. Generate unique ID
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
note_id = f"note_{timestamp}_{len(self.index)}"
# 2. Build metadata
metadata = {
"id": note_id,
"title": title,
"type": note_type,
"tags": tags or [],
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
# 3. Build complete Markdown file content
md_content = self._build_markdown(metadata, content)
# 4. Save to file
file_path = os.path.join(self.workspace, f"{note_id}.md")
with open(file_path, 'w', encoding='utf-8') as f:
f.write(md_content)
# 5. Update index
metadata["file_path"] = file_path
self.index[note_id] = metadata
self._save_index()
return note_id
def _build_markdown(self, metadata: Dict, content: str) -> str:
"""Build Markdown file content (YAML + body)"""
import yaml
# YAML front matter
yaml_header = yaml.dump(metadata, allow_unicode=True, sort_keys=False)
# Combined format
return f"---\n{yaml_header}---\n\n{content}"Usage example:
from hello_agents.tools import NoteTool
notes = NoteTool(workspace="./project_notes")
note_id = notes.run({
"action": "create",
"title": "Refactoring Project - Phase 1",
"content": """## Completion Status
Completed refactoring of data model layer, test coverage reached 85%.
## Next Steps
Refactor business logic layer""",
"note_type": "task_state",
"tags": ["refactoring", "phase1"]
})
print(f"✅ Note created successfully, ID: {note_id}")(2) read: Read Note
def _read_note(self, note_id: str) -> Dict:
"""Read note content
Args:
note_id: Note ID
Returns:
Dict: Dictionary containing metadata and content
"""
if note_id not in self.index:
raise ValueError(f"Note does not exist: {note_id}")
file_path = self.index[note_id]["file_path"]
# Read file
with open(file_path, 'r', encoding='utf-8') as f:
raw_content = f.read()
# Parse YAML metadata and Markdown body
metadata, content = self._parse_markdown(raw_content)
return {
"metadata": metadata,
"content": content
}
def _parse_markdown(self, raw_content: str) -> Tuple[Dict, str]:
"""Parse Markdown file (separate YAML and body)"""
import yaml
# Find YAML delimiters
parts = raw_content.split('---\n', 2)
if len(parts) >= 3:
# Has YAML front matter
yaml_str = parts[1]
content = parts[2].strip()
metadata = yaml.safe_load(yaml_str)
else:
# No metadata, all as body
metadata = {}
content = raw_content.strip()
return metadata, content(3) update: Update Note
def _update_note(
self,
note_id: str,
title: Optional[str] = None,
content: Optional[str] = None,
note_type: Optional[str] = None,
tags: Optional[List[str]] = None
) -> str:
"""Update note
Args:
note_id: Note ID
title: New title (optional)
content: New content (optional)
note_type: New type (optional)
tags: New tags (optional)
Returns:
str: Operation result message
"""
if note_id not in self.index:
raise ValueError(f"Note does not exist: {note_id}")
# 1. Read existing note
note = self._read_note(note_id)
metadata = note["metadata"]
old_content = note["content"]
# 2. Update fields
if title:
metadata["title"] = title
if note_type:
metadata["type"] = note_type
if tags is not None:
metadata["tags"] = tags
if content is not None:
old_content = content
# Update timestamp
from datetime import datetime
metadata["updated_at"] = datetime.now().isoformat()
# 3. Rebuild and save
md_content = self._build_markdown(metadata, old_content)
file_path = metadata["file_path"]
with open(file_path, 'w', encoding='utf-8') as f:
f.write(md_content)
# 4. Update index
self.index[note_id] = metadata
self._save_index()
return f"✅ Note updated: {metadata['title']}"(4) search: Search Notes
def _search_notes(
self,
query: str,
limit: int = 10,
note_type: Optional[str] = None,
tags: Optional[List[str]] = None
) -> List[Dict]:
"""Search notes
Args:
query: Search keyword
limit: Return quantity limit
note_type: Filter by type (optional)
tags: Filter by tags (optional)
Returns:
List[Dict]: List of matching notes
"""
results = []
query_lower = query.lower()
for note_id, metadata in self.index.items():
# Type filter
if note_type and metadata.get("type") != note_type:
continue
# Tag filter
if tags:
note_tags = set(metadata.get("tags", []))
if not note_tags.intersection(tags):
continue
# Read note content
try:
note = self._read_note(note_id)
content = note["content"]
title = metadata.get("title", "")
# Search in title and content
if query_lower in title.lower() or query_lower in content.lower():
results.append({
"note_id": note_id,
"title": title,
"type": metadata.get("type"),
"tags": metadata.get("tags", []),
"content": content,
"updated_at": metadata.get("updated_at")
})
except Exception as e:
print(f"[WARNING] Failed to read note {note_id}: {e}")
continue
# Sort by update time
results.sort(key=lambda x: x["updated_at"], reverse=True)
return results[:limit](5) list: List Notes
def _list_notes(
self,
note_type: Optional[str] = None,
tags: Optional[List[str]] = None,
limit: int = 20
) -> List[Dict]:
"""List notes (in reverse chronological order by update time)
Args:
note_type: Filter by type (optional)
tags: Filter by tags (optional)
limit: Return quantity limit
Returns:
List[Dict]: List of note metadata
"""
results = []
for note_id, metadata in self.index.items():
# Type filter
if note_type and metadata.get("type") != note_type:
continue
# Tag filter
if tags:
note_tags = set(metadata.get("tags", []))
if not note_tags.intersection(tags):
continue
results.append(metadata)
# Sort by update time
results.sort(key=lambda x: x.get("updated_at", ""), reverse=True)
return results[:limit](6) summary: Note Summary
def _summary(self) -> Dict[str, Any]:
"""Generate note summary statistics
Returns:
Dict: Statistical information
"""
total_count = len(self.index)
# Count by type
type_counts = {}
for metadata in self.index.values():
note_type = metadata.get("type", "general")
type_counts[note_type] = type_counts.get(note_type, 0) + 1
# Recently updated notes
recent_notes = sorted(
self.index.values(),
key=lambda x: x.get("updated_at", ""),
reverse=True
)[:5]
return {
"total_notes": total_count,
"type_distribution": type_counts,
"recent_notes": [
{
"id": note["id"],
"title": note.get("title", ""),
"type": note.get("type"),
"updated_at": note.get("updated_at")
}
for note in recent_notes
]
}(7) delete: Delete Note
def _delete_note(self, note_id: str) -> str:
"""Delete note
Args:
note_id: Note ID
Returns:
str: Operation result message
"""
if note_id not in self.index:
raise ValueError(f"Note does not exist: {note_id}")
# 1. Delete file
file_path = self.index[note_id]["file_path"]
if os.path.exists(file_path):
os.remove(file_path)
# 2. Remove from index
title = self.index[note_id].get("title", note_id)
del self.index[note_id]
self._save_index()
return f"✅ Note deleted: {title}"9.4.4 Deep Integration with ContextBuilder
The true power of NoteTool lies in its combined use with ContextBuilder. Let's demonstrate this integration through a complete case study.
(1) Scenario Setup
Suppose we are building a long-term project assistant that needs to:
- Record phased progress of the project
- Track pending issues
- Automatically review relevant notes during each conversation
- Provide coherent recommendations based on historical notes
(2) Implementation Example
from hello_agents import SimpleAgent, HelloAgentsLLM
from hello_agents.context import ContextBuilder, ContextConfig, ContextPacket
from hello_agents.tools import MemoryTool, RAGTool, NoteTool
from datetime import datetime
class ProjectAssistant(SimpleAgent):
"""Long-term project assistant, integrating NoteTool and ContextBuilder"""
def __init__(self, name: str, project_name: str, **kwargs):
super().__init__(name=name, llm=HelloAgentsLLM(), **kwargs)
self.project_name = project_name
# Initialize tools
self.memory_tool = MemoryTool(user_id=project_name)
self.rag_tool = RAGTool(knowledge_base_path=f"./{project_name}_kb")
self.note_tool = NoteTool(workspace=f"./{project_name}_notes")
# Initialize context builder
self.context_builder = ContextBuilder(
memory_tool=self.memory_tool,
rag_tool=self.rag_tool,
config=ContextConfig(max_tokens=4000)
)
self.conversation_history = []
def run(self, user_input: str, note_as_action: bool = False) -> str:
"""Run assistant, automatically integrate notes"""
# 1. Retrieve relevant notes from NoteTool
relevant_notes = self._retrieve_relevant_notes(user_input)
# 2. Convert notes to ContextPacket
note_packets = self._notes_to_packets(relevant_notes)
# 3. Build optimized context
context = self.context_builder.build(
user_query=user_input,
conversation_history=self.conversation_history,
system_instructions=self._build_system_instructions(),
custom_packets=note_packets
)
# 4. Call LLM
response = self.llm.invoke(context)
# 5. If needed, record interaction as note
if note_as_action:
self._save_as_note(user_input, response)
# 6. Update conversation history
self._update_history(user_input, response)
return response
def _retrieve_relevant_notes(self, query: str, limit: int = 3) -> List[Dict]:
"""Retrieve relevant notes"""
try:
# Prioritize retrieving blocker and action type notes
blockers = self.note_tool.run({
"action": "list",
"note_type": "blocker",
"limit": 2
})
# General search
search_results = self.note_tool.run({
"action": "search",
"query": query,
"limit": limit
})
# Merge and deduplicate
all_notes = {note['note_id']: note for note in blockers + search_results}
return list(all_notes.values())[:limit]
except Exception as e:
print(f"[WARNING] Note retrieval failed: {e}")
return []
def _notes_to_packets(self, notes: List[Dict]) -> List[ContextPacket]:
"""Convert notes to context packets"""
packets = []
for note in notes:
content = f"[Note: {note['title']}]\n{note['content']}"
packets.append(ContextPacket(
content=content,
timestamp=datetime.fromisoformat(note['updated_at']),
token_count=len(content) // 4, # Simple estimation
relevance_score=0.75, # Notes have high relevance
metadata={
"type": "note",
"note_type": note['type'],
"note_id": note['note_id']
}
))
return packets
def _save_as_note(self, user_input: str, response: str):
"""Save interaction as note"""
try:
# Determine what type of note to save
if "problem" in user_input.lower() or "blocker" in user_input.lower():
note_type = "blocker"
elif "plan" in user_input.lower() or "next" in user_input.lower():
note_type = "action"
else:
note_type = "conclusion"
self.note_tool.run({
"action": "create",
"title": f"{user_input[:30]}...",
"content": f"## Question\n{user_input}\n\n## Analysis\n{response}",
"note_type": note_type,
"tags": [self.project_name, "auto_generated"]
})
except Exception as e:
print(f"[WARNING] Failed to save note: {e}")
def _build_system_instructions(self) -> str:
"""Build system instructions"""
return f"""You are a long-term assistant for the {self.project_name} project.
Your responsibilities:
1. Provide coherent recommendations based on historical notes
2. Track project progress and pending issues
3. Reference relevant historical notes when answering
4. Provide specific, actionable next-step recommendations
Notes:
- Prioritize issues marked as blockers
- Indicate source of basis in recommendations (notes, memory, or knowledge base)
- Maintain awareness of overall project progress"""
def _update_history(self, user_input: str, response: str):
"""Update conversation history"""
from hello_agents.core.message import Message
self.conversation_history.append(
Message(content=user_input, role="user", timestamp=datetime.now())
)
self.conversation_history.append(
Message(content=response, role="assistant", timestamp=datetime.now())
)
# Limit history length
if len(self.conversation_history) > 10:
self.conversation_history = self.conversation_history[-10:]
# Usage example
assistant = ProjectAssistant(
name="Project Assistant",
project_name="data_pipeline_refactoring"
)
# First interaction: Record project status
response = assistant.run(
"We have completed refactoring of the data model layer, test coverage reached 85%. Next plan is to refactor the business logic layer.",
note_as_action=True
)
# Second interaction: Raise issue
response = assistant.run(
"When refactoring the business logic layer, I encountered dependency version conflict issues. How should I resolve this?"
)
# View note summary
summary = assistant.note_tool.run({"action": "summary"})
print(summary)(3) Running Effect Demonstration
[ContextBuilder] Collected 8 candidate information packages
[ContextBuilder] Selected 7 information packages, total 3500 tokens
✅ Assistant answer:
I noticed this issue was mentioned in your previously recorded notes. According to the note [Refactoring Project - Phase 1], your current test coverage has reached 85%, which is a good foundation.
Regarding the dependency version conflict issue, I recommend:
1. **Use virtual environment isolation**: Create an independent virtual environment for the business logic layer to avoid dependency conflicts with other modules
2. **Lock versions**: Explicitly specify exact versions of all dependencies in requirements.txt
3. **Use pipdeptree**: Analyze the dependency tree to find the root cause of conflicts
I will mark this issue as a blocker and recommend prioritizing its resolution.
[Source: Note note_20250119_153000_0, Project knowledge base]
---
📋 Note summary:
{
"total_notes": 2,
"type_distribution": {
"action": 1,
"blocker": 1
},
"recent_notes": [
{
"id": "note_20250119_154500_1",
"title": "When refactoring the business logic layer, I encountered dependency version conflict issues...",
"type": "blocker",
"updated_at": "2025-01-19T15:45:00"
},
{
"id": "note_20250119_153000_0",
"title": "We have completed refactoring of the data model layer...",
"type": "action",
"updated_at": "2025-01-19T15:30:00"
}
]
}9.4.5 Best Practices
When actually using NoteTool, the following best practices can help you build more powerful long-horizon agents:
Reasonable note classification:
task_state: Record phased progress and statusconclusion: Record important conclusions and findingsblocker: Record blocking issues, highest priorityaction: Record next action plansreference: Record important reference materials
Regular cleanup and archiving:
- For resolved blockers, update to conclusion
- For outdated actions, delete or update promptly
- Use tags for version management, such as
["v1.0", "completed"]
Cooperation with ContextBuilder:
- Retrieve relevant notes before each round of dialogue
- Set different relevance scores based on note type (blocker > action > conclusion)
- Limit number of notes to avoid context overload
Human-machine collaboration:
- Notes are in human-readable Markdown format, supporting manual editing
- Use Git for version control to track note evolution
- At key stages, manually review notes generated by Agent
Automated workflow:
- Regularly generate note summary reports
- Automatically generate project progress documents based on notes
- Synchronize note content to other systems (such as Notion, Confluence)