Agent Handoff Workflows
This guide demonstrates real-world workflows for handing off work between interactive Claude Code sessions and autonomous AILANG agents.
SessionStart hooks run successfully but output doesn't reliably appear in Claude's context. Use manual inbox checking instead: Ask Claude to run ailang agent inbox --unread-only claude-code at the start of each session.
Quick Reference
Key Commands:
# Check inboxes (FLAGS BEFORE AGENT ID!)
ailang agent inbox --unread-only claude-code # Project-specific
ailang agent inbox --unread-only user # Global
# Acknowledge messages
ailang agent ack msg_20251025_155729_a5f3e77ee975 # Single message
ailang agent ack --all # All messages
ailang agent unack msg_20251025_155729_a5f3e77ee975 # Un-acknowledge (retry)
# Send messages
ailang agent send <agent> '<json-payload>'
ailang agent send --to-user '<json-payload>'
Workflow 1: Design → Implement → Notify
Scenario: You design a feature interactively, then hand it off to autonomous agents for implementation.
Step 1: Interactive Design (Claude Code)
You: "Design a fix for the import resolution bug"
Claude Code: *analyzes codebase*
Claude Code: *creates design_docs/planned/M-IMPORT-FIX.md*
Claude Code: "I've created a design doc. It adds path normalization to the import resolver."
You: "Looks good, implement it"
Step 2: Session Stops (Automatic Handoff)
When you stop the session (or it times out), the Stop hook fires:
# agent_handoff.sh runs automatically:
→ Detects design_docs/planned/M-IMPORT-FIX.md (modified < 5 min ago)
→ Computes hash: sha256:a1b2c3...
→ Stores artifact in ~/.ailang/state/artifacts/
→ Sends message to sprint-planner:
{
"task": "implement_design_doc",
"event": {
"session_id": "claude-session-xyz",
"user_id": "mark",
"event": "Stop",
"provider": "claude-code"
},
"artifacts": [
{
"path": "design_docs/planned/M-IMPORT-FIX.md",
"hash": "sha256:a1b2c3...",
"title": "M-IMPORT-FIX: Path normalization for import resolver"
}
]
}
Step 3: Autonomous Implementation
The sprint-planner agent (running separately):
# Sprint-planner polls for messages
→ Receives message from interactive session
→ Reads design doc from artifact store
→ Creates sprint plan
→ Sends to sprint-executor
→ Executor implements the fix
→ Runs tests
→ Sends completion message to user inbox
Step 4: Next Session (Check Inbox)
You start a new Claude Code session:
You: "Check inbox"
Claude: *runs: ailang agent inbox --unread-only claude-code*
Claude: "I found 1 message from sprint-executor:
The import fix has been implemented and tested.
All 15 tests passing. Ready to review."
You: "Great! Show me the changes"
Preview (most recent):
From: sprint-executor
Message ID: msg_20251025_143022_def456
The terminal notification shows you the message exists, BUT Claude (the AI assistant) can't see it automatically due to Claude Code's architecture.
To inform Claude:
- Tell Claude: "Check for agent messages"
- Or ask: "Any messages from agents?"
- Claude will run:
ailang agent inbox user
How it works:
- SessionStart hook forwards messages from user inbox
- You see terminal notification (visible to you)
- You tell Claude to check inbox (makes it visible to AI)
- Claude runs
ailang agent inbox userto display messages
Step 5: Review Results
$ ailang agent inbox user
📬 User Inbox (1 message)
================================================================================
▶ Message 1/1
ID: msg_20251025_143022_def456
From: sprint-executor
Type: notification
Correlation ID: cycle_20251025_001
Payload:
{
"status": "completed",
"task": "implement M-IMPORT-FIX",
"tests_passed": true,
"files_modified": [
"internal/loader/import_resolver.go",
"internal/loader/import_resolver_test.go"
],
"commit": "abc123def456"
}
✓ Marked as read
Total: 1 message(s)
Workflow 2: Interactive Query → Agent Response
Scenario: You ask an agent a question and wait for an answer.
# Send query with --wait flag
$ ailang agent send --wait 1m eval-analyzer '{
"action": "summarize_failures",
"baseline": "eval_results/baselines/v0.3.19"
}'
✓ Message sent to eval-analyzer
Message ID: msg_20251025_144523_xyz789
Correlation ID: cycle_20251025_014
Waiting for response (timeout: 1m)...
✓ Received response!
Message ID: msg_20251025_144530_abc456
From: eval-analyzer
Payload:
{
"total_failures": 12,
"top_errors": [
{"error": "type mismatch", "count": 5},
{"error": "import resolution", "count": 4},
{"error": "syntax error", "count": 3}
],
"recommendations": [
"Fix type inference in let-polymorphism",
"Add path normalization to imports"
]
}
Workflow 3: Background Agent → User Notification
Scenario: An agent completes a long-running task and notifies you.
Agent Side
// In your autonomous agent:
inbox := agentprotocol.NewUserInbox(stateDir)
// After completing work
msg := &agentprotocol.Envelope{
MessageID: agentprotocol.GenerateMessageID(),
FromAgent: "sprint-executor",
ToAgent: "user",
MessageType: "notification",
Payload: map[string]interface{}{
"status": "completed",
"task": "implement M-IMPORT-FIX",
"summary": "Fixed import resolver, all tests passing",
},
}
inbox.SendToUser(msg)
User Side
Next time you start Claude Code:
╔═══════════════════════════════════════════════════════════╗
║ 📬 You have 1 unread message(s) from agents ║
╚═══════════════════════════════════════════════════════════╝
Check details:
$ ailang agent inbox user
# ... message details displayed ...
Workflow 4: Multi-Agent Pipeline
Scenario: Work flows through multiple agents with handoffs.
Each agent sends messages to the next agent in the pipeline using the correlation_id to track the workflow.
Example from sprint-planner:
# Sprint-planner receives design doc
→ Creates sprint plan
→ Sends to sprint-executor:
{
"message_id": "msg_abc",
"correlation_id": "cycle_20251025_001", # Same as original
"parent_message_id": "msg_xyz", # References design doc message
"to_agent": "sprint-executor",
"payload": {
"sprint_plan_path": "~/.ailang/state/sprints/current_sprint.json",
"tasks": 5
}
}
Content-Addressed Artifacts
Large files (design docs, code, test results) are stored as artifacts to avoid bloating messages.
Storing Artifacts
store := agentprotocol.NewArtifactStore(stateDir)
// Read design doc
content, _ := os.ReadFile("design_docs/planned/M-TEST.md")
// Store with hash
hash, _ := store.StoreArtifact("design_docs/planned/M-TEST.md", content, "text/markdown")
// Returns: "sha256:a1b2c3d4e5f6..."
// Reference in message
msg.Payload["artifacts"] = []map[string]interface{}{
{
"path": "design_docs/planned/M-TEST.md",
"hash": hash,
"mime_type": "text/markdown",
},
}
Retrieving Artifacts
// Agent receives message with artifact reference
artifactHash := msg.Payload["artifacts"].([]interface{})[0].(map[string]interface{})["hash"].(string)
// Retrieve content
content, metadata, _ := store.RetrieveArtifact(artifactHash)
// Use content
fmt.Printf("Design doc: %s\n", string(content))
Benefits
- Deduplication: Same content stored only once
- Verification: Hash ensures content hasn't been tampered with
- Bandwidth: Messages stay small, artifacts stored separately
- History: Artifacts persist even after messages are archived
Message Signing & Security
All messages are signed with HMAC-SHA256 to prevent spoofing.
Automatic Signing
signer := agentprotocol.NewMessageSigner(stateDir)
// Sign message before sending
signer.SignMessage(msg)
// Message now has:
// - signature: "a1b2c3d4..."
// - signature_alg: "hmac-sha256"
// - kid: "key-xyz123"
Automatic Verification
// Verify message on receive
if err := signer.VerifyMessage(msg); err != nil {
log.Printf("Invalid signature: %v", err)
return // Reject message
}
Inbox Management
Read/Unread/Archive Flow
New message
↓
_unread/ ← Initial state
↓
(view)
↓
_read/ ← After viewing
↓
(archive)
↓
_archive/ ← Long-term storage
Commands
# View unread (marks as read automatically)
ailang agent inbox user
# View without marking as read
ailang agent inbox user --unread-only
# View read messages
ailang agent inbox user --read-only
# View and archive
ailang agent inbox user --archive
# View archived
ailang agent inbox user --archived
Best Practices
1. Use Correlation IDs
Track related messages across agents:
// First message in workflow
correlationID := agentprotocol.GenerateCorrelationID()
// All subsequent messages use same correlation ID
msg.CorrelationID = correlationID
2. Include Parent Message IDs
Build message chains for audit trails:
// Response to a previous message
parentID := originalMsg.MessageID
msg.ParentMessageID = &parentID
3. Set Reasonable TTLs
Messages expire after TTL to prevent stale data:
msg.TTLSeconds = 3600 // 1 hour for quick responses
msg.TTLSeconds = 86400 // 24 hours for batch jobs
4. Declare Effects
Make side effects explicit:
msg.DeclaredEffects = []string{"IO", "FS", "Net"}
5. Use Structured Payloads
Define clear schemas for payloads:
msg.PayloadSchema = "https://ailang.dev/schemas/sprint_plan/v1.json"
msg.Payload = map[string]interface{}{
"tasks": []Task{...},
"estimated_days": 3.5,
}
Troubleshooting
Message not received
Check inbox:
ls -la ~/.ailang/state/messages/inbox/user/_unread/
Check agent inbox:
ls -la ~/.ailang/state/messages/<agent-id>/
Verify message was sent:
# Look for confirmation in CLI output
✓ Message sent to sprint-planner
Path: ~/.ailang/state/messages/sprint-planner/msg_xyz.pending.json
Artifact not found
List all artifacts:
ls -la ~/.ailang/state/artifacts/sha256/
Verify hash is correct:
# Compute hash using ailang CLI
ailang debug hash design_docs/planned/M-TEST.md
Check artifact metadata:
cat ~/.ailang/state/artifacts/sha256/abc123.../metadata.json
Hook not firing
Check .claude/hooks.json exists:
cat .claude/hooks.json
Verify scripts are executable:
ls -la scripts/hooks/
Check hook logs:
tail -50 ~/.ailang/state/hooks.log
Related Documentation
- Claude Code Integration Guide - Main integration guide
- Hooks Setup - Quick setup guide
Design documents (GitHub):
- M-CLAUDE-CODE-INTEGRATION-V2 - Complete specification
- M-AGENT-PROTOCOL - Protocol details
Last updated: October 25, 2025 (v0.3.20)