Hook Framework#
MassGen provides a hook framework for extending agent behavior at key execution points. Hooks enable content injection, permission validation, and custom processing during tool execution.
Overview#
Hooks are callbacks that execute at specific points in the agent lifecycle:
PreToolUse: Before a tool is invoked
PostToolUse: After a tool returns its result
Hooks can:
Allow/Deny tool execution (PreToolUse)
Inject content into tool results or as separate messages (PostToolUse)
Extract reminders from tool output for system notifications
Modify arguments before tool execution
Built-in Hooks#
MassGen includes two built-in hooks that power multi-agent coordination:
MidStreamInjectionHook#
Injects updates from other agents into tool results during coordination.
Purpose: When Agent A provides an answer, other agents need to see that update without losing their work progress. The MidStreamInjectionHook injects these updates at natural pauses (tool completion) rather than requiring full agent restarts.
Injection Strategy: tool_result - appends to the current tool output
Example injection:
============================================================
⚠️ IMPORTANT: NEW ANSWER RECEIVED - ACTION REQUIRED
============================================================
[UPDATE: agent1 submitted new answer(s)]
[agent1] (workspace: /path/to/temp_workspaces/agent_b/agent1):
I've implemented the authentication module...
============================================================
REQUIRED ACTION - You MUST do one of the following:
============================================================
1. **ADD A TASK** to your plan: 'Evaluate agent answer(s) and decide next action'
- Use update_task_status or create a new task to track this evaluation
- Read their workspace files (paths above) to understand their solution
- Compare their approach to yours
2. **THEN CHOOSE ONE**:
a) VOTE for their answer if it's complete and correct (use vote tool)
b) BUILD on their work - improve/extend it and submit YOUR enhanced answer
c) MERGE approaches - combine the best parts of their work with yours
d) CONTINUE your own approach if you believe it's better
DO NOT ignore this update - you must explicitly evaluate and decide!
============================================================
Benefits:
Preserves agent’s conversation history
No lost work on new answers
Lightweight mid-stream delivery
HighPriorityTaskReminderHook#
Injects reminders when high-priority tasks are completed using the planning tools.
Purpose: When an agent completes a high-priority task via update_task_status,
this hook injects a reminder to document learnings. This encourages reflection and
knowledge capture after important work.
Injection Strategy: user_message - creates a separate message after tool result
Trigger condition: The hook inspects update_task_status tool output and fires when:
task.priority == "high"task.status == "completed"
Formatted injection:
============================================================
⚠️ SYSTEM REMINDER
============================================================
✓ High-priority task completed! Document decisions to optimize future work:
• Which skills/tools were effective (or not)? → memory/long_term/skill_effectiveness.md
• What approach worked (or failed) and why? → memory/long_term/approach_patterns.md
• What would prevent mistakes on similar tasks? → memory/long_term/lessons_learned.md
• User preferences revealed? → memory/short_term/user_prefs.md
============================================================
Note: Unlike passive extraction from tool JSON, this hook actively inspects tool output and makes decisions about when to inject. This pattern is more flexible and keeps reminder logic separate from tool implementation.
Injection Strategies#
Hooks can inject content using two strategies:
tool_result Strategy#
Appends content directly to the tool result. Best for:
Cross-agent updates during coordination
Additional context that relates to the tool operation
Minimal message overhead
HookResult(
inject={
"content": "[UPDATE] New information...",
"strategy": "tool_result"
}
)
user_message Strategy#
Injects as a separate user message after the tool result. Best for:
System reminders and notifications
Content that should be clearly distinguished from tool output
Semantic separation
HookResult(
inject={
"content": "SYSTEM REMINDER: ...",
"strategy": "user_message"
}
)
API-Specific Handling#
Different LLM APIs handle injection differently:
Anthropic API (Claude)#
Uses separate content blocks within a single message for clean semantic separation:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_123",
"content": "actual tool output"
},
{
"type": "text",
"text": "<system-reminder>Your TODO list is empty</system-reminder>"
}
]
}
OpenAI API (GPT-4, o1, etc.)#
Two approaches:
Separate message:
{"role": "tool", "tool_call_id": "call_123", "content": "actual output"}
{"role": "user", "content": "<system-reminder>...</system-reminder>"}
Structured output (with system prompt explaining format):
{
"role": "tool",
"tool_call_id": "call_123",
"content": "{\"output\": \"actual\", \"system_notes\": [\"reminder\"]}"
}
Visual Separation#
When appending to tool results, clear visual separators prevent model confusion:
actual tool output here
═══════════════════════════════════════════════════════
SYSTEM CONTEXT (not part of tool output):
- Your TODO list is empty
- Consider using TodoWrite for multi-step tasks
═══════════════════════════════════════════════════════
Workspace Handling#
When injecting answers from other agents, workspace paths are normalized so the receiving agent can access the referenced files.
The Problem#
Each agent has an isolated workspace:
Agent A:
.massgen/workspaces/agent_a_workspace/Agent B:
.massgen/workspaces/agent_b_workspace/
If Agent A’s answer references their workspace path, Agent B cannot access it.
Path Normalization#
Workspace paths are automatically normalized during injection:
Before (Agent A's answer):
"I created /Users/foo/.massgen/workspaces/agent_a_workspace/output.py"
After (what Agent B sees):
"I created /Users/foo/.massgen/temp_workspaces/agent1/output.py"
The receiving agent can now actually access the files at the normalized path.
Snapshot Sharing#
When Agent A provides an answer:
Agent A’s workspace is snapshotted
The snapshot is copied to Agent B’s temp workspace
Paths in the injected answer point to the temp workspace
Agent B can read Agent A’s files for verification
Anonymization#
Agent identities are preserved through this process:
Agent B doesn’t know “agent1” is really “Agent A”
Prevents voting bias based on agent identity
But Agent B knows the injection is external (not from self)
This trade-off is acceptable because:
Knowing updates are external prevents self-confirmation loops
Anonymization between agents still prevents bias
The alternative (full restart) loses ALL progress
Custom Hooks#
You can create custom hooks by implementing the hook interface.
Python Callable Hook#
from massgen.mcp_tools.hooks import (
PythonCallableHook,
HookEvent,
HookResult,
GeneralHookManager,
HookType,
)
def my_audit_hook(event: HookEvent) -> HookResult:
"""Log all tool calls for auditing."""
print(f"Tool called: {event.tool_name}")
print(f"Arguments: {event.tool_input}")
return HookResult.allow()
# Register the hook
manager = GeneralHookManager()
hook = PythonCallableHook("audit", my_audit_hook, matcher="*")
manager.register_global_hook(HookType.PRE_TOOL_USE, hook)
Pattern Matching#
Hooks support glob-style pattern matching:
*- Match all toolsWrite- Match exactly “Write”mcp__*- Match all MCP toolsWrite|Edit- Match “Write” or “Edit”
Hook Registration#
Global vs Per-Agent#
# Global hooks - apply to ALL agents
hooks:
PreToolUse:
- matcher: "*"
handler: "massgen.hooks.audit_all_tools"
type: "python"
agents:
- id: "agent1"
backend:
# Per-agent hooks - extend global by default
hooks:
PreToolUse:
- matcher: "Write"
handler: "massgen.hooks.validate_writes"
type: "python"
fail_closed: true # Deny on hook errors
PostToolUse:
override: true # Only use per-agent hooks
hooks:
- handler: "massgen.hooks.log_outputs"
type: "python"
Error Handling#
By default, hooks fail open (allow tool execution) on errors to avoid blocking agents. For security-critical hooks, you can configure fail closed behavior:
hooks:
PreToolUse:
- matcher: "Write|Delete"
handler: "massgen.hooks.security_check"
fail_closed: true # Deny tool execution if hook fails
Default behavior (fail_closed: false):
Timeout: Allow - don’t block agent on slow hooks
Runtime errors: Allow with logging - don’t crash agent
Import errors: Always deny - configuration error
With fail_closed: true:
Timeout: Deny - block tool if hook can’t complete
Runtime errors: Deny - block tool if hook crashes
Timing Considerations#
Mid-stream injection has specific timing rules:
First update: Uses traditional full-message injection - Prevents premature convergence on first answer
Subsequent updates: Uses mid-stream hook injection - Lighter weight, preserves work in progress - Only new answers (not already seen) are injected
Vote-only mode: Skips mid-stream injection entirely - Tool schemas are fixed at stream start - Full restart required for new vote options
Debugging Injection#
Testing mid-stream injection requires one agent to be slower than others so it receives updates while working. MassGen provides a debug delay feature for this.
Debug Delay Configuration#
Add debug_delay_seconds to a backend config to artificially slow an agent:
agents:
- id: "agent_a"
backend:
type: "gemini"
model: "gemini-3-flash-preview"
- id: "agent_b"
backend:
type: "gemini"
model: "gemini-3-flash-preview"
debug_delay_seconds: 30 # Delay in seconds
debug_delay_after_n_tools: 2 # Apply after N tool calls
Parameters:
debug_delay_seconds: How long to pause (default: 0, disabled)debug_delay_after_n_tools: Apply delay after this many tool calls (default: 3)
Why delay after N tools? Delaying at the start would cause immediate restarts. By waiting until the agent has made progress (created tasks, done some work), the delay happens at a natural point where injection is meaningful.
Injection Visibility#
When injection occurs, you’ll see:
Log messages:
[Orchestrator] Copying snapshots for mid-stream injection to agent_b [Orchestrator] Injection workspace .../temp_workspaces/agent_b/agent1 contains: ['tasks', 'poem.txt'] [Orchestrator] Mid-stream injection for agent_b: 1 new answer(s) [PostToolUse] Hook injection for mcp__filesystem__read_text_file: strategy=tool_result, content_len=1641
Stream chunk: A visible
📥 [INJECTION]chunk in the outputFull content: The injection content is logged for debugging
Example Debug Config#
# massgen/configs/debug/injection_delay_test.yaml
agents:
- id: "agent_a"
backend:
type: "gemini"
model: "gemini-3-flash-preview"
cwd: "workspace1"
enable_mcp_command_line: true
- id: "agent_b"
backend:
type: "gemini"
model: "gemini-3-flash-preview"
cwd: "workspace2"
enable_mcp_command_line: true
debug_delay_seconds: 30
debug_delay_after_n_tools: 2
orchestrator:
coordination:
enable_agent_task_planning: true
task_planning_filesystem_mode: true
Run with:
uv run massgen --config massgen/configs/debug/injection_delay_test.yaml \
"Create a simple poem and write it into a file"
See also
Agent Communication - Multi-agent coordination and broadcasts
File Operations & Workspace Management - Workspace management