removed twitter agent, create new terminal, resume command, update quickstart

This commit is contained in:
bryan
2026-02-10 16:41:08 -08:00
parent 32cae75ef5
commit 1f9c47fef1
18 changed files with 199 additions and 1145 deletions
+17 -1
View File
@@ -141,6 +141,12 @@ for f in ~/.zshrc ~/.bashrc ~/.profile; do [ -f "$f" ] && grep -q 'HIVE_CREDENTI
- **In shell config but NOT in current session** — run `source ~/.zshrc` (or `~/.bashrc`) first, then proceed
- **Not set anywhere** — `EncryptedFileStorage` will auto-generate one. After storing, tell the user to persist it: `export HIVE_CREDENTIAL_KEY="{generated_key}"` in their shell profile
> **⚠️ IMPORTANT: After adding `HIVE_CREDENTIAL_KEY` to the user's shell config, always display:**
> ```
> ⚠️ Environment variables were added to your shell config.
> Open a NEW TERMINAL for them to take effect outside this session.
> ```
#### Option 1: Aden Platform (OAuth)
This is the recommended flow for supported integrations (HubSpot, etc.).
@@ -202,6 +208,12 @@ if success:
print(f"Run: {source_cmd}")
```
> **⚠️ IMPORTANT: After adding `ADEN_API_KEY` to the user's shell config, always display:**
> ```
> ⚠️ Environment variables were added to your shell config.
> Open a NEW TERMINAL for them to take effect outside this session.
> ```
Also save to `~/.hive/configuration.json` for the framework:
```python
@@ -607,6 +619,10 @@ All credentials are now configured:
│ ✅ CREDENTIALS CONFIGURED │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ OPEN A NEW TERMINAL before running commands below. │
│ Environment variables were saved to your shell config but │
│ only take effect in new terminal sessions. │
│ │
│ NEXT STEPS: │
│ │
│ 1. RUN YOUR AGENT: │
@@ -618,7 +634,7 @@ All credentials are now configured:
│ /hive-debugger │
│ │
│ The debugger analyzes runtime logs, identifies retry loops, tool │
│ failures, stalled execution, and provides actionable fix suggestions. │
│ failures, stalled execution, and provides actionable fix suggestions.
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
+57 -57
View File
@@ -47,7 +47,7 @@ Before using this skill, ensure:
**What to do:**
1. **Ask the developer which agent needs debugging:**
- Get agent name (e.g., "twitter_outreach", "deep_research_agent")
- Get agent name (e.g., "deep_research_agent", "deep_research_agent")
- Confirm the agent exists in `exports/{agent_name}/`
2. **Determine agent working directory:**
@@ -66,7 +66,7 @@ Before using this skill, ensure:
4. **Store context for the debugging session:**
- agent_name
- agent_work_dir (e.g., `/home/user/.hive/twitter_outreach`)
- agent_work_dir (e.g., `/home/user/.hive/deep_research_agent`)
- goal_id
- success_criteria
- constraints
@@ -74,19 +74,19 @@ Before using this skill, ensure:
**Example:**
```
Developer: "My twitter_outreach agent keeps failing"
Developer: "My deep_research_agent agent keeps failing"
You: "I'll help debug the twitter_outreach agent. Let me gather context..."
You: "I'll help debug the deep_research_agent agent. Let me gather context..."
[Read exports/twitter_outreach/agent.json]
[Read exports/deep_research_agent/agent.json]
Context gathered:
- Agent: twitter_outreach
- Goal: twitter-outreach-multi-loop
- Working Directory: /home/user/.hive/twitter_outreach
- Success Criteria: ["Successfully send 5 personalized outreach messages"]
- Constraints: ["Must verify handle exists", "Must personalize message"]
- Nodes: ["intake-collector", "profile-analyzer", "message-composer", "outreach-sender"]
- Agent: deep_research_agent
- Goal: deep-research
- Working Directory: /home/user/.hive/deep_research_agent
- Success Criteria: ["Produce a comprehensive research report with cited sources"]
- Constraints: ["Must cite all sources", "Must cover multiple perspectives"]
- Nodes: ["intake", "research", "analysis", "report-writer"]
```
---
@@ -224,7 +224,7 @@ Which run would you like to investigate?
```
Diagnosis for session_20260206_115718_e22339c5:
Problem Node: intake-collector
Problem Node: research
├─ Exit Status: escalate
├─ Retry Count: 5 (HIGH)
├─ Verdict Counts: {RETRY: 5, ESCALATE: 1}
@@ -232,7 +232,7 @@ Problem Node: intake-collector
├─ Total Steps: 8
└─ Categories: Missing Outputs + Retry Loops
Root Issue: The intake-collector node is stuck in a retry loop because it's not setting required outputs.
Root Issue: The research node is stuck in a retry loop because it's not setting required outputs.
```
---
@@ -293,25 +293,25 @@ Root Issue: The intake-collector node is stuck in a retry loop because it's not
**Example Output:**
```
Root Cause Analysis for intake-collector:
Root Cause Analysis for research:
Step-by-step breakdown:
Step 3:
- Tool Call: web_search(query="@RomuloNevesOf")
- Result: Found Twitter profile information
- Tool Call: web_search(query="latest AI regulations 2026")
- Result: Found relevant articles and sources
- Verdict: RETRY
- Feedback: "Missing required output 'twitter_handles'. You found the handle but didn't call set_output."
- Feedback: "Missing required output 'research_findings'. You found sources but didn't call set_output."
Step 4:
- Tool Call: web_search(query="@RomuloNevesOf twitter")
- Result: Found additional Twitter information
- Tool Call: web_search(query="AI regulation policy 2026")
- Result: Found additional policy information
- Verdict: RETRY
- Feedback: "Still missing 'twitter_handles'. Use set_output to save your findings."
- Feedback: "Still missing 'research_findings'. Use set_output to save your findings."
Steps 5-7: Similar pattern continues...
ROOT CAUSE: The node is successfully finding Twitter handles via web_search, but the LLM is not calling set_output to save the results. It keeps searching for more information instead of completing the task.
ROOT CAUSE: The node is successfully finding research sources via web_search, but the LLM is not calling set_output to save the results. It keeps searching for more information instead of completing the task.
```
---
@@ -653,7 +653,7 @@ Available Checkpoints: (3)
**Example interaction:**
```
Developer: "I applied the fix to intake-collector. How do I verify it works?"
Developer: "I applied the fix to research. How do I verify it works?"
You: "Great! Let's verify the fix with these steps:
@@ -665,11 +665,11 @@ You: "Great! Let's verify the fix with these steps:
[Use query_runtime_logs to check for attention flags]
3. Verify the specific node:
[Use query_runtime_log_details for intake-collector]
[Use query_runtime_log_details for research]
Expected results:
- No 'needs_attention' flags
- intake-collector shows exit_status='success'
- research shows exit_status='success'
- retry_count should be 0
Let me know when you've run it and I'll help check the logs!"
@@ -687,7 +687,7 @@ Let me know when you've run it and I'll help check the logs!"
- **Example:**
```
query_runtime_logs(
agent_work_dir="/home/user/.hive/twitter_outreach",
agent_work_dir="/home/user/.hive/deep_research_agent",
status="needs_attention",
limit=20
)
@@ -699,7 +699,7 @@ Let me know when you've run it and I'll help check the logs!"
- **Example:**
```
query_runtime_log_details(
agent_work_dir="/home/user/.hive/twitter_outreach",
agent_work_dir="/home/user/.hive/deep_research_agent",
run_id="session_20260206_115718_e22339c5",
needs_attention_only=True
)
@@ -711,9 +711,9 @@ Let me know when you've run it and I'll help check the logs!"
- **Example:**
```
query_runtime_log_raw(
agent_work_dir="/home/user/.hive/twitter_outreach",
agent_work_dir="/home/user/.hive/deep_research_agent",
run_id="session_20260206_115718_e22339c5",
node_id="intake-collector"
node_id="research"
)
```
@@ -743,23 +743,23 @@ Loop every 10 seconds:
## Complete Example Walkthrough
### Scenario: twitter_outreach Agent Failing
### Scenario: deep_research_agent Agent Failing
**Developer says:** "My twitter_outreach agent keeps failing. It's not finding Twitter handles correctly."
**Developer says:** "My deep_research_agent keeps failing. It's not saving research findings correctly."
#### Stage 1: Setup
```
You: "I'll help debug the twitter_outreach agent. Let me gather context..."
You: "I'll help debug the deep_research_agent agent. Let me gather context..."
[Read exports/twitter_outreach/agent.json]
[Read exports/deep_research_agent/agent.json]
Context:
- Agent: twitter_outreach
- Goal: twitter-outreach-multi-loop
- Working Dir: ~/.hive/agents/twitter_outreach
- Agent: deep_research_agent
- Goal: deep-research
- Working Dir: ~/.hive/agents/deep_research_agent
- Success Criteria: ["Successfully send 5 personalized outreach messages"]
- Constraints: ["Must verify handle exists", "Must personalize message"]
- Nodes: intake-collector, profile-analyzer, message-composer, outreach-sender
- Nodes: research, profile-analyzer, message-composer, outreach-sender
```
#### Stage 2: Mode Selection
@@ -791,31 +791,31 @@ You: "Analyzing node-level details..."
[Call query_runtime_log_details for the session]
Results:
- Problem Node: intake-collector
- Problem Node: research
- Exit Status: escalate
- Retry Count: 5
- Verdict Counts: {RETRY: 5, ESCALATE: 1}
- Attention Reasons: ["high_retry_count", "missing_outputs"]
- Total Steps: 8
Diagnosis: intake-collector has retry loop + missing outputs issue
Diagnosis: research has retry loop + missing outputs issue
Category: Missing Outputs + Retry Loops
```
#### Stage 5: Root Cause (L3)
```
You: "Drilling into detailed logs for intake-collector..."
You: "Drilling into detailed logs for research..."
[Call query_runtime_log_raw for node intake-collector]
[Call query_runtime_log_raw for node research]
Evidence:
Step 3: web_search("@RomuloNevesOf") → Found profile
Step 3: web_search("latest AI regulations 2026") → Found articles
Verdict: RETRY
Feedback: "Missing required output 'twitter_handles'. You found the handle but didn't call set_output."
Feedback: "Missing required output 'research_findings'. You found sources but didn't call set_output."
Step 4: web_search("@RomuloNevesOf twitter") → Found more info
Step 4: web_search("AI regulation policy 2026") → Found more info
Verdict: RETRY
Feedback: "Still missing 'twitter_handles'. Use set_output to save your findings."
Feedback: "Still missing 'research_findings'. Use set_output to save your findings."
Steps 5-7: Same pattern repeats...
@@ -826,37 +826,37 @@ ROOT CAUSE: Node successfully finds handles but LLM doesn't call set_output. It
```
You: "Here's the fix for this issue:
## Issue: intake-collector Not Setting Outputs
## Issue: research Node Not Setting Outputs
**Root Cause:** Node successfully finds Twitter handles but doesn't call set_output
**Root Cause:** Node successfully finds research sources but doesn't call set_output
**Category:** Missing Outputs
**File to edit:** exports/twitter_outreach/nodes/intake_collector.py
**File to edit:** exports/deep_research_agent/nodes/research.py
**Changes:**
Update the system_prompt to emphasize the output requirement:
```python
system_prompt = """
You are the Intake Collector for the Twitter outreach agent.
You are the Research node for the deep research agent.
Your task:
1. Extract Twitter handle from user input (URL or @mention)
2. Verify it's a valid Twitter handle format
3. **CRITICAL: Call set_output(twitter_handles=[handle]) immediately after finding valid handle**
1. Search for relevant sources on the research topic
2. Extract key findings from each source
3. **CRITICAL: Call set_output(research_findings=[...]) immediately after gathering sufficient findings**
IMPORTANT: You MUST call set_output even if you found the handle on the first try.
Do not continue searching once you have a valid handle - set output and finish.
IMPORTANT: You MUST call set_output even if you found findings on the first search.
Do not continue searching indefinitely - set output and finish once you have enough data.
Output format:
{
"twitter_handles": ["@handle1", "@handle2", ...]
"research_findings": [{"source": "...", "summary": "...", "url": "..."}, ...]
}
"""
```
**Why this works:** The explicit instruction to call set_output immediately prevents the LLM from continuing to search after finding the handle.
**Why this works:** The explicit instruction to call set_output immediately prevents the LLM from continuing to search after finding sufficient sources.
```
#### Stage 7: Verification
@@ -880,13 +880,13 @@ You: "Checking now..."
[Call query_runtime_log_details for the new run]
Results:
- Node: intake-collector
- Node: research
- Exit Status: success ✓
- Retry Count: 0 ✓
- Total Steps: 2 ✓
- No attention flags ✓
"Perfect! The fix worked. The intake-collector now:
"Perfect! The fix worked. The research now:
- Completes successfully on first try
- No retries needed
- Calls set_output properly
@@ -478,7 +478,7 @@ async def resume_session(
```bash
# List resumable sessions
hive sessions list --agent twitter_outreach --status failed
hive sessions list --agent deep_research_agent --status failed
# Show checkpoints for a session
hive sessions checkpoints session_20260208_143022_abc12345
+5 -5
View File
@@ -224,7 +224,7 @@ Three MCP tools provide access to the logging system:
```python
query_runtime_logs(
agent_work_dir: str, # e.g., "~/.hive/agents/twitter_outreach"
agent_work_dir: str, # e.g., "~/.hive/agents/deep_research_agent"
status: str = "", # "needs_attention", "success", "failure", "degraded"
limit: int = 20
) -> dict # {"runs": [...], "total": int}
@@ -371,14 +371,14 @@ query_runtime_log_raw(agent_work_dir, run_id)
```python
# 1. Find problematic runs (L1)
result = query_runtime_logs(
agent_work_dir="~/.hive/agents/twitter_outreach",
agent_work_dir="~/.hive/agents/deep_research_agent",
status="needs_attention"
)
run_id = result["runs"][0]["run_id"]
# 2. Identify failing nodes (L2)
details = query_runtime_log_details(
agent_work_dir="~/.hive/agents/twitter_outreach",
agent_work_dir="~/.hive/agents/deep_research_agent",
run_id=run_id,
needs_attention_only=True
)
@@ -386,7 +386,7 @@ problem_node = details["nodes"][0]["node_id"]
# 3. Analyze root cause (L3)
raw = query_runtime_log_raw(
agent_work_dir="~/.hive/agents/twitter_outreach",
agent_work_dir="~/.hive/agents/deep_research_agent",
run_id=run_id,
node_id=problem_node
)
@@ -496,7 +496,7 @@ logger.start_run(goal_id, session_id=execution_id)
```json
{
"run_id": "session_20260206_115718_e22339c5",
"goal_id": "twitter-outreach-multi-loop",
"goal_id": "deep-research",
"status": "degraded",
"started_at": "2026-02-06T11:57:18.593081",
"ended_at": "2026-02-06T11:58:45.123456",
+1 -1
View File
@@ -37,7 +37,7 @@ class SessionStore:
Initialize session store.
Args:
base_path: Base path for storage (e.g., ~/.hive/agents/twitter_outreach)
base_path: Base path for storage (e.g., ~/.hive/agents/deep_research_agent)
"""
self.base_path = Path(base_path)
self.sessions_dir = self.base_path / "sessions"
+92 -37
View File
@@ -82,8 +82,10 @@ class ChatRepl(Vertical):
self._streaming_snapshot: str = ""
self._waiting_for_input: bool = False
self._input_node_id: str | None = None
self._pending_ask_question: str = ""
self._resume_session = resume_session
self._resume_checkpoint = resume_checkpoint
self._session_index: list[str] = [] # Ordered session IDs from last /sessions or /resume listing
# Dedicated event loop for agent execution.
# Keeps blocking runtime code (LLM calls, MCP tools) off
@@ -138,8 +140,9 @@ class ChatRepl(Vertical):
self._write_history("""[bold cyan]Available Commands:[/bold cyan]
[bold]/sessions[/bold] - List all sessions for this agent
[bold]/sessions[/bold] <session_id> - Show session details and checkpoints
[bold]/resume[/bold] - Resume latest paused/failed session
[bold]/resume[/bold] <session_id> - Resume session from where it stopped
[bold]/resume[/bold] - List sessions and pick one to resume
[bold]/resume[/bold] <number> - Resume session by list number
[bold]/resume[/bold] <session_id> - Resume session by ID
[bold]/recover[/bold] <session_id> <cp_id> - Recover from specific checkpoint
[bold]/pause[/bold] - Pause current execution (Ctrl+Z)
[bold]/help[/bold] - Show this help message
@@ -147,8 +150,9 @@ class ChatRepl(Vertical):
[dim]Examples:[/dim]
/sessions [dim]# List all sessions[/dim]
/sessions session_20260208_143022 [dim]# Show session details[/dim]
/resume [dim]# Resume latest session (from state)[/dim]
/resume session_20260208_143022 [dim]# Resume specific session (from state)[/dim]
/resume [dim]# Show numbered session list[/dim]
/resume 1 [dim]# Resume first listed session[/dim]
/resume session_20260208_143022 [dim]# Resume by full session ID[/dim]
/recover session_20260208_143022 cp_xxx [dim]# Recover from specific checkpoint[/dim]
/pause [dim]# Pause (or Ctrl+Z)[/dim]
""")
@@ -156,15 +160,29 @@ class ChatRepl(Vertical):
session_id = parts[1].strip() if len(parts) > 1 else None
await self._cmd_sessions(session_id)
elif cmd == "/resume":
# Resume from session state (not checkpoint-based)
if len(parts) < 2:
session_id = await self._find_latest_resumable_session()
if not session_id:
self._write_history("[bold red]No resumable sessions found[/bold red]")
self._write_history(" Tip: Use [bold]/sessions[/bold] to see all sessions")
# No arg → show session list so user can pick one
await self._cmd_sessions(None)
return
arg = parts[1].strip()
# Numeric index → resolve from last listing
if arg.isdigit():
idx = int(arg) - 1 # 1-based to 0-based
if 0 <= idx < len(self._session_index):
session_id = self._session_index[idx]
else:
self._write_history(
f"[bold red]Error:[/bold red] No session at index {arg}"
)
self._write_history(
" Use [bold]/resume[/bold] to see available sessions"
)
return
else:
session_id = parts[1].strip()
session_id = arg
await self._cmd_resume(session_id)
elif cmd == "/recover":
# Recover from specific checkpoint
@@ -241,6 +259,15 @@ class ChatRepl(Vertical):
except Exception:
return None
def _get_session_label(self, state: dict) -> str:
"""Extract the first user message from input_data as a human-readable label."""
input_data = state.get("input_data", {})
for value in input_data.values():
if isinstance(value, str) and value.strip():
label = value.strip()
return label[:60] + "..." if len(label) > 60 else label
return "(no input)"
async def _list_sessions(self, storage_path: Path) -> None:
"""List all sessions for the agent."""
self._write_history("[bold cyan]Available Sessions:[/bold cyan]")
@@ -264,6 +291,11 @@ class ChatRepl(Vertical):
self._write_history(f"[dim]Found {len(session_dirs)} session(s)[/dim]\n")
# Reset the session index for numeric lookups
self._session_index = []
import json
for session_dir in session_dirs[:10]: # Show last 10 sessions
session_id = session_dir.name
state_file = session_dir / "state.json"
@@ -273,12 +305,15 @@ class ChatRepl(Vertical):
# Read session state
try:
import json
with open(state_file) as f:
state = json.load(f)
# Track this session for /resume <number> lookup
self._session_index.append(session_id)
index = len(self._session_index)
status = state.get("status", "unknown").upper()
label = self._get_session_label(state)
# Status with color
if status == "COMPLETED":
@@ -292,25 +327,21 @@ class ChatRepl(Vertical):
else:
status_colored = f"[dim]{status}[/dim]"
# Check for checkpoints
checkpoint_dir = session_dir / "checkpoints"
checkpoint_count = 0
if checkpoint_dir.exists():
checkpoint_files = list(checkpoint_dir.glob("cp_*.json"))
checkpoint_count = len(checkpoint_files)
# Session line
self._write_history(f"📋 [bold]{session_id}[/bold]")
self._write_history(f" Status: {status_colored} Checkpoints: {checkpoint_count}")
if checkpoint_count > 0:
self._write_history(f" [dim]Resume: /resume {session_id}[/dim]")
# Session line with index and label
self._write_history(
f" [bold]{index}.[/bold] {label} {status_colored}"
)
self._write_history(f" [dim]{session_id}[/dim]")
self._write_history("") # Blank line
except Exception as e:
self._write_history(f" [dim red]Error reading: {e}[/dim red]")
if self._session_index:
self._write_history(
"[dim]Use [bold]/resume <number>[/bold] to resume a session[/dim]"
)
async def _show_session_details(self, storage_path: Path, session_id: str) -> None:
"""Show detailed information about a specific session."""
self._write_history(f"[bold cyan]Session Details:[/bold cyan] {session_id}\n")
@@ -682,16 +713,20 @@ class ChatRepl(Vertical):
{
"session_id": session_dir.name,
"status": status.upper(),
"label": self._get_session_label(state),
}
)
except Exception:
continue
if resumable:
self._write_history("\n[bold yellow]⚠ Non-terminated sessions found:[/bold yellow]")
# Populate session index so /resume <number> works immediately
self._session_index = [s["session_id"] for s in resumable[:3]]
self._write_history("\n[bold yellow]Non-terminated sessions found:[/bold yellow]")
for i, session in enumerate(resumable[:3], 1): # Show top 3
status = session["status"]
session_id = session["session_id"]
label = session["label"]
# Color code status
if status == "PAUSED":
@@ -703,15 +738,12 @@ class ChatRepl(Vertical):
else:
status_colored = f"[dim]{status}[/dim]"
self._write_history(f" {i}. {session_id[:32]}... [{status_colored}]")
self._write_history(f" [bold]{i}.[/bold] {label} {status_colored}")
self._write_history("\n[bold cyan]What would you like to do?[/bold cyan]")
self._write_history(" • Type [bold]/resume[/bold] to continue the latest session")
self._write_history(
f" Type [bold]/resume {resumable[0]['session_id']}[/bold] "
"for specific session"
"\n Type [bold]/resume <number>[/bold] to continue a session"
)
self._write_history(" Or just type your input to start a new session\n")
self._write_history(" Or just type your input to start a new session\n")
except Exception:
# Silently fail - don't block TUI startup
@@ -833,8 +865,16 @@ class ChatRepl(Vertical):
def handle_tool_started(self, tool_name: str, tool_input: dict[str, Any]) -> None:
"""Handle a tool call starting."""
# Update indicator to show tool activity
indicator = self.query_one("#processing-indicator", Label)
if tool_name == "ask_user":
# Stash the question for handle_input_requested() to display.
# Suppress the generic "Tool: ask_user" line.
self._pending_ask_question = tool_input.get("question", "")
indicator.update("Preparing question...")
return
# Update indicator to show tool activity
indicator.update(f"Using tool: {tool_name}...")
# Write a discrete status line to history
@@ -842,6 +882,11 @@ class ChatRepl(Vertical):
def handle_tool_completed(self, tool_name: str, result: str, is_error: bool) -> None:
"""Handle a tool call completing."""
if tool_name == "ask_user":
# Suppress the synthetic "Waiting for user input..." result.
# The actual question is displayed by handle_input_requested().
return
result_str = str(result)
preview = result_str[:200] + "..." if len(result_str) > 200 else result_str
preview = preview.replace("\n", " ")
@@ -872,6 +917,7 @@ class ChatRepl(Vertical):
self._streaming_snapshot = ""
self._waiting_for_input = False
self._input_node_id = None
self._pending_ask_question = ""
# Re-enable input
chat_input = self.query_one("#chat-input", Input)
@@ -890,6 +936,7 @@ class ChatRepl(Vertical):
self._current_exec_id = None
self._streaming_snapshot = ""
self._waiting_for_input = False
self._pending_ask_question = ""
self._input_node_id = None
# Re-enable input
@@ -906,10 +953,18 @@ class ChatRepl(Vertical):
and sets a flag so the next submission routes to inject_input().
"""
# Flush accumulated streaming text as agent output
if self._streaming_snapshot:
self._write_history(f"[bold blue]Agent:[/bold blue] {self._streaming_snapshot}")
flushed_snapshot = self._streaming_snapshot
if flushed_snapshot:
self._write_history(f"[bold blue]Agent:[/bold blue] {flushed_snapshot}")
self._streaming_snapshot = ""
# Display the ask_user question if stashed and not already
# present in the streaming snapshot (avoids double-display).
question = self._pending_ask_question
self._pending_ask_question = ""
if question and question not in flushed_snapshot:
self._write_history(f"[bold blue]Agent:[/bold blue] {question}")
self._waiting_for_input = True
self._input_node_id = node_id or None
+9 -9
View File
@@ -32,26 +32,26 @@ Each goal has weighted success criteria that define what "done" looks like. Thes
```python
Goal(
id="twitter-outreach",
name="Personalized Twitter Outreach",
id="deep-research",
name="Deep Research Report",
success_criteria=[
SuccessCriterion(
id="personalized",
description="Messages reference specific details from the prospect's profile",
id="comprehensive",
description="Report covers all major aspects of the research topic",
metric="llm_judge",
weight=0.4
),
SuccessCriterion(
id="compliant",
description="Messages follow brand voice guidelines",
id="cited",
description="All claims are backed by cited sources",
metric="llm_judge",
weight=0.3
),
SuccessCriterion(
id="actionable",
description="Each message includes a clear call to action",
id="structured",
description="Report has clear sections with headings and a summary",
metric="output_contains",
target="CTA",
target="## Summary",
weight=0.3
),
],
-1
View File
@@ -44,4 +44,3 @@ uv run python -m exports.my_research_agent --input '{"topic": "..."}'
|----------|-------------|
| [deep_research_agent](deep_research_agent/) | Interactive research agent that searches diverse sources, evaluates findings with user checkpoints, and produces a cited HTML report |
| [tech_news_reporter](tech_news_reporter/) | Researches the latest technology and AI news from the web and produces a well-organized report |
| [twitter_outreach](twitter_outreach/) | Researches a Twitter/X profile, crafts a personalized outreach email, and sends it after user approval |
@@ -69,10 +69,22 @@ You do NOT have web search — instead, scrape news directly from known sites.
- Recency (past week)
- Significance and diversity of topics
CRITICAL: Copy URLs EXACTLY as they appear in the "href" field of the scraped
links. Do NOT reconstruct, guess, or modify URLs from memory. Use the verbatim
href value from the web_scrape result.
3. For each selected article, use web_scrape with max_length=3000 on the
individual article URL to get the content. Extract: title, source name,
URL, publication date, a 2-3 sentence summary, and the main topic category.
4. **VERIFY LINKS** Before producing your final output, verify each article URL
by checking the web_scrape result you got in step 3:
- If the scrape returned content successfully, the URL is verified use it as-is.
- If the scrape returned an error or the page was not found (404, timeout, etc.),
go back to the front page links from step 1 and pick a different article URL
to replace it. Scrape the replacement to confirm it works.
- Only include articles whose URLs returned successful scrape results.
**Output format:**
Use set_output("articles_data", <JSON string>) with this structure:
```json
@@ -94,12 +106,13 @@ Use set_output("articles_data", <JSON string>) with this structure:
**Rules:**
- Only include REAL articles with REAL URLs you scraped. Never fabricate.
- The "url" field MUST be a URL you successfully scraped. Never invent URLs.
- Focus on news from the past week.
- Aim for at least 3 distinct topic categories.
- Keep summaries factual and concise.
- If a site fails to load, skip it and move on to the next.
- Always use max_length to limit scraped content (5000 for front pages, 3000 for articles).
- Work in batches: scrape front pages first, then articles. Don't scrape everything at once.
- Work in batches: scrape front pages first, then articles, then verify. Don't scrape everything at once.
""",
tools=["web_scrape"],
)
@@ -1,57 +0,0 @@
# Twitter Outreach Agent
Personalized email outreach powered by Twitter/X research.
## What it does
1. **Intake** — Collects the target's Twitter handle, outreach purpose, and recipient email
2. **Research** — Searches and scrapes the target's Twitter/X profile for bio, tweets, interests
3. **Draft & Review** — Crafts a personalized email and presents it for your approval (with iteration)
4. **Send** — Sends the approved email
## Usage
```bash
# Validate the agent structure
PYTHONPATH=core:exports uv run python -m twitter_outreach validate
# Show agent info
PYTHONPATH=core:exports uv run python -m twitter_outreach info
# Run the workflow
PYTHONPATH=core:exports uv run python -m twitter_outreach run
# Launch the TUI
PYTHONPATH=core:exports uv run python -m twitter_outreach tui
# Interactive shell
PYTHONPATH=core:exports uv run python -m twitter_outreach shell
```
## Architecture
```
intake → research → draft-review → send
```
## Tools Used
- `web_search` — Search for Twitter profiles and public info
- `web_scrape` — Read Twitter/X profile pages
- `send_email` — Send the approved outreach email
## Nodes
| Node | Type | Client-Facing | Description |
|------|------|:---:|-------------|
| `intake` | event_loop | Yes | Collect target info from user |
| `research` | event_loop | No | Research Twitter/X profile |
| `draft-review` | event_loop | Yes | Draft email, iterate with user |
| `send` | event_loop | No | Send approved email |
## Constraints
- **No Spam** — No spammy language, clickbait, or aggressive sales tactics
- **Approval Required** — Never sends without explicit user approval
- **Tone** — Professional, authentic, conversational
- **Privacy** — Only uses publicly available information
@@ -1,23 +0,0 @@
"""
Twitter Outreach Agent - Personalized email outreach powered by Twitter/X research.
Reads a target's Twitter/X profile, crafts a personalized outreach email
referencing their specific activity, and sends it after user approval.
"""
from .agent import TwitterOutreachAgent, default_agent, goal, nodes, edges
from .config import RuntimeConfig, AgentMetadata, default_config, metadata
__version__ = "1.0.0"
__all__ = [
"TwitterOutreachAgent",
"default_agent",
"goal",
"nodes",
"edges",
"RuntimeConfig",
"AgentMetadata",
"default_config",
"metadata",
]
@@ -1,206 +0,0 @@
"""
CLI entry point for Twitter Outreach Agent.
Uses AgentRuntime for TUI support with client-facing interaction.
"""
import asyncio
import json
import logging
import sys
import click
from .agent import default_agent, TwitterOutreachAgent
def setup_logging(verbose=False, debug=False):
"""Configure logging for execution visibility."""
if debug:
level, fmt = logging.DEBUG, "%(asctime)s %(name)s: %(message)s"
elif verbose:
level, fmt = logging.INFO, "%(message)s"
else:
level, fmt = logging.WARNING, "%(levelname)s: %(message)s"
logging.basicConfig(level=level, format=fmt, stream=sys.stderr)
logging.getLogger("framework").setLevel(level)
@click.group()
@click.version_option(version="1.0.0")
def cli():
"""Twitter Outreach Agent - Personalized email outreach powered by Twitter/X research."""
pass
@cli.command()
@click.option("--quiet", "-q", is_flag=True, help="Only output result JSON")
@click.option("--verbose", "-v", is_flag=True, help="Show execution details")
@click.option("--debug", is_flag=True, help="Show debug logging")
def run(quiet, verbose, debug):
"""Execute the outreach workflow."""
if not quiet:
setup_logging(verbose=verbose, debug=debug)
result = asyncio.run(default_agent.run({}))
output_data = {
"success": result.success,
"steps_executed": result.steps_executed,
"output": result.output,
}
if result.error:
output_data["error"] = result.error
click.echo(json.dumps(output_data, indent=2, default=str))
sys.exit(0 if result.success else 1)
@cli.command()
@click.option("--verbose", "-v", is_flag=True, help="Show execution details")
@click.option("--debug", is_flag=True, help="Show debug logging")
def tui(verbose, debug):
"""Launch the TUI dashboard for interactive outreach."""
setup_logging(verbose=verbose, debug=debug)
try:
from framework.tui.app import AdenTUI
except ImportError:
click.echo(
"TUI requires the 'textual' package. Install with: pip install textual"
)
sys.exit(1)
from pathlib import Path
from framework.llm import LiteLLMProvider
from framework.runner.tool_registry import ToolRegistry
from framework.runtime.agent_runtime import create_agent_runtime
from framework.runtime.event_bus import EventBus
from framework.runtime.execution_stream import EntryPointSpec
async def run_with_tui():
agent = TwitterOutreachAgent()
agent._event_bus = EventBus()
agent._tool_registry = ToolRegistry()
storage_path = Path.home() / ".hive" / "twitter_outreach"
storage_path.mkdir(parents=True, exist_ok=True)
mcp_config_path = Path(__file__).parent / "mcp_servers.json"
if mcp_config_path.exists():
agent._tool_registry.load_mcp_config(mcp_config_path)
llm = LiteLLMProvider(
model=agent.config.model,
api_key=agent.config.api_key,
api_base=agent.config.api_base,
)
tools = list(agent._tool_registry.get_tools().values())
tool_executor = agent._tool_registry.get_executor()
graph = agent._build_graph()
runtime = create_agent_runtime(
graph=graph,
goal=agent.goal,
storage_path=storage_path,
entry_points=[
EntryPointSpec(
id="start",
name="Start Outreach",
entry_node="intake",
trigger_type="manual",
isolation_level="isolated",
),
],
llm=llm,
tools=tools,
tool_executor=tool_executor,
)
await runtime.start()
try:
app = AdenTUI(runtime)
await app.run_async()
finally:
await runtime.stop()
asyncio.run(run_with_tui())
@cli.command()
@click.option("--json", "output_json", is_flag=True)
def info(output_json):
"""Show agent information."""
info_data = default_agent.info()
if output_json:
click.echo(json.dumps(info_data, indent=2))
else:
click.echo(f"Agent: {info_data['name']}")
click.echo(f"Version: {info_data['version']}")
click.echo(f"Description: {info_data['description']}")
click.echo(f"\nNodes: {', '.join(info_data['nodes'])}")
click.echo(f"Client-facing: {', '.join(info_data['client_facing_nodes'])}")
click.echo(f"Entry: {info_data['entry_node']}")
click.echo(f"Terminal: {', '.join(info_data['terminal_nodes'])}")
@cli.command()
def validate():
"""Validate agent structure."""
validation = default_agent.validate()
if validation["valid"]:
click.echo("Agent is valid")
if validation["warnings"]:
for warning in validation["warnings"]:
click.echo(f" WARNING: {warning}")
else:
click.echo("Agent has errors:")
for error in validation["errors"]:
click.echo(f" ERROR: {error}")
sys.exit(0 if validation["valid"] else 1)
@cli.command()
@click.option("--verbose", "-v", is_flag=True)
def shell(verbose):
"""Interactive outreach session (CLI, no TUI)."""
asyncio.run(_interactive_shell(verbose))
async def _interactive_shell(verbose=False):
"""Async interactive shell."""
setup_logging(verbose=verbose)
click.echo("=== Twitter Outreach Agent ===")
click.echo("Starting outreach workflow...\n")
agent = TwitterOutreachAgent()
await agent.start()
try:
result = await agent.trigger_and_wait("start", {})
if result is None:
click.echo("\n[Execution timed out]\n")
elif result.success:
output = result.output
status = output.get("delivery_status", "unknown")
click.echo(f"\nOutreach complete! Delivery status: {status}")
else:
click.echo(f"\nOutreach failed: {result.error}")
except KeyboardInterrupt:
click.echo("\nGoodbye!")
except Exception as e:
click.echo(f"Error: {e}", err=True)
import traceback
traceback.print_exc()
finally:
await agent.stop()
if __name__ == "__main__":
cli()
@@ -1,265 +0,0 @@
{
"agent": {
"id": "twitter_outreach",
"name": "Personalized Twitter Outreach",
"version": "1.0.0",
"description": "Given a Twitter/X handle and outreach context, research the target's profile (bio, tweets, interests), craft a personalized outreach email referencing their specific activity, and send it after user approval."
},
"graph": {
"id": "twitter_outreach-graph",
"goal_id": "twitter-outreach",
"version": "1.0.0",
"entry_node": "intake",
"entry_points": {
"start": "intake"
},
"pause_nodes": [],
"terminal_nodes": [
"send"
],
"nodes": [
{
"id": "intake",
"name": "Intake",
"description": "Collect the target Twitter handle, outreach purpose, and recipient email from the user",
"node_type": "event_loop",
"input_keys": [],
"output_keys": [
"twitter_handle",
"outreach_context",
"recipient_email"
],
"nullable_output_keys": [],
"input_schema": {},
"output_schema": {},
"system_prompt": "You are the intake assistant for a personalized Twitter outreach agent.\n\n**STEP 1 \u2014 Respond to the user (text only, NO tool calls):**\nGreet the user and ask them to provide:\n1. The Twitter/X handle of the person they want to reach out to\n2. The purpose/context of the outreach (e.g., partnership opportunity, hiring, collaboration, introduction)\n3. The recipient's email address\n\nBe friendly and concise. If the user provides partial info, ask for what's missing.\n\n**STEP 2 \u2014 After the user provides ALL three pieces of information, call set_output:**\n- set_output(\"twitter_handle\", \"<the Twitter handle, including @>\")\n- set_output(\"outreach_context\", \"<the outreach purpose/context>\")\n- set_output(\"recipient_email\", \"<the email address>\")",
"tools": [],
"model": null,
"function": null,
"routes": {},
"max_retries": 3,
"retry_on": [],
"max_node_visits": 1,
"output_model": null,
"max_validation_retries": 2,
"client_facing": true
},
{
"id": "research",
"name": "Research",
"description": "Research the target's Twitter/X profile \u2014 bio, recent tweets, interests, and topics they engage with",
"node_type": "event_loop",
"input_keys": [
"twitter_handle"
],
"output_keys": [
"profile_summary"
],
"nullable_output_keys": [],
"input_schema": {},
"output_schema": {},
"system_prompt": "You are a Twitter/X profile researcher. Your job is to thoroughly research a person's public Twitter/X presence.\n\nGiven the Twitter handle provided in your inputs, do the following:\n\n1. Use web_search to find their Twitter/X profile and any relevant public information about them.\n2. Use web_scrape to read their Twitter/X profile page (try https://x.com/{handle} or https://twitter.com/{handle}).\n3. Extract and analyze:\n - Their bio and self-description\n - Recent tweets and topics they post about\n - Professional interests, projects, or accomplishments\n - Any recurring themes or passions\n - Specific tweets worth referencing in outreach\n4. Look for additional context (personal website, blog, other social profiles mentioned in bio).\n\nCompile a comprehensive profile summary that would help someone write a highly personalized outreach email.\n\nUse set_output(\"profile_summary\", <your detailed summary as a string>) to store your findings.\n\nDo NOT return raw JSON. Use the set_output tool to produce outputs.",
"tools": [
"web_search",
"web_scrape"
],
"model": null,
"function": null,
"routes": {},
"max_retries": 3,
"retry_on": [],
"max_node_visits": 1,
"output_model": null,
"max_validation_retries": 2,
"client_facing": false
},
{
"id": "draft-review",
"name": "Draft & Review",
"description": "Draft a personalized outreach email using profile research, present to user for review, and iterate until approved",
"node_type": "event_loop",
"input_keys": [
"outreach_context",
"recipient_email",
"profile_summary"
],
"output_keys": [
"approved_email"
],
"nullable_output_keys": [],
"input_schema": {},
"output_schema": {},
"system_prompt": "You are an expert email copywriter specializing in personalized outreach.\n\nYou have been given:\n- A profile summary of the target person (from their Twitter/X)\n- The outreach context/purpose\n- The recipient's email address\n\n**STEP 1 \u2014 Draft and present the email (text only, NO tool calls):**\n\nUsing the profile research, draft a personalized outreach email that:\n- References at least 2 specific details from their Twitter profile (tweets, interests, projects)\n- Clearly connects to the outreach purpose\n- Includes a specific, relevant call to action\n- Is professional but conversational and authentic \u2014 NOT spammy, robotic, or overly formal\n- Is concise (under 300 words)\n\nPresent the complete email draft to the user, formatted clearly with Subject line and Body.\nThen ask: \"Would you like any changes, or shall I send this?\"\n\nIf the user requests changes, revise the email and present the updated version. Keep iterating until the user is satisfied.\n\n**STEP 2 \u2014 After the user explicitly approves the email, call set_output:**\n- set_output(\"approved_email\", \"<the final approved email text including subject line>\")",
"tools": [],
"model": null,
"function": null,
"routes": {},
"max_retries": 3,
"retry_on": [],
"max_node_visits": 1,
"output_model": null,
"max_validation_retries": 2,
"client_facing": true
},
{
"id": "send",
"name": "Send",
"description": "Send the approved outreach email to the recipient",
"node_type": "event_loop",
"input_keys": [
"approved_email",
"recipient_email"
],
"output_keys": [
"delivery_status"
],
"nullable_output_keys": [],
"input_schema": {},
"output_schema": {},
"system_prompt": "You are responsible for sending the approved outreach email.\n\nYou have the approved email text and the recipient's email address in your inputs.\n\nParse the subject line and body from the approved_email, then use the send_email tool to send it to the recipient_email address.\n\nAfter sending successfully, call:\n- set_output(\"delivery_status\", \"sent\")\n\nIf there is an error sending, call:\n- set_output(\"delivery_status\", \"failed: <error details>\")\n\nDo NOT return raw JSON. Use the set_output tool to produce outputs.",
"tools": [
"send_email"
],
"model": null,
"function": null,
"routes": {},
"max_retries": 3,
"retry_on": [],
"max_node_visits": 1,
"output_model": null,
"max_validation_retries": 2,
"client_facing": false
}
],
"edges": [
{
"id": "intake-to-research",
"source": "intake",
"target": "research",
"condition": "on_success",
"condition_expr": null,
"priority": 1,
"input_mapping": {}
},
{
"id": "research-to-draft-review",
"source": "research",
"target": "draft-review",
"condition": "on_success",
"condition_expr": null,
"priority": 1,
"input_mapping": {}
},
{
"id": "draft-review-to-send",
"source": "draft-review",
"target": "send",
"condition": "on_success",
"condition_expr": null,
"priority": 1,
"input_mapping": {}
}
],
"max_steps": 100,
"max_retries_per_node": 3,
"description": "Given a Twitter/X handle and outreach context, research the target's profile (bio, tweets, interests), craft a personalized outreach email referencing their specific activity, and send it after user approval.",
"created_at": "2026-02-05T13:32:44.573661"
},
"goal": {
"id": "twitter-outreach",
"name": "Personalized Twitter Outreach",
"description": "Given a Twitter/X handle and outreach context, research the target's profile (bio, tweets, interests), craft a personalized outreach email referencing their specific activity, and send it after user approval.",
"status": "draft",
"success_criteria": [
{
"id": "profile-research",
"description": "Agent extracts meaningful information from target's Twitter profile including bio, recent tweets, interests, and topics they engage with",
"metric": "research_quality",
"target": "Identifies at least 3 distinct profile details",
"weight": 0.25,
"met": false
},
{
"id": "email-personalization",
"description": "Drafted email references at least 2 specific details from the target's Twitter profile",
"metric": "personalization_score",
"target": "At least 2 specific references to profile content",
"weight": 0.25,
"met": false
},
{
"id": "clear-cta",
"description": "Email includes a specific relevant call to action",
"metric": "cta_present",
"target": "Email contains clear call to action",
"weight": 0.15,
"met": false
},
{
"id": "user-approval-gate",
"description": "Email is presented to user for review and only sent after explicit approval with opportunity to request edits",
"metric": "approval_obtained",
"target": "User explicitly approves before send",
"weight": 0.2,
"met": false
},
{
"id": "successful-delivery",
"description": "Email is sent successfully via the send_email tool",
"metric": "delivery_status",
"target": "Email sent without errors",
"weight": 0.15,
"met": false
}
],
"constraints": [
{
"id": "no-spam",
"description": "Email must not use spammy language, clickbait, or aggressive sales tactics",
"constraint_type": "quality",
"category": "content",
"check": ""
},
{
"id": "approval-required",
"description": "Must never send an email without explicit user approval",
"constraint_type": "safety",
"category": "process",
"check": ""
},
{
"id": "tone-appropriate",
"description": "Email tone must be professional, authentic, and conversational \u2014 not robotic or overly formal",
"constraint_type": "quality",
"category": "content",
"check": ""
},
{
"id": "privacy-respect",
"description": "Only use publicly available information from the target's Twitter profile",
"constraint_type": "safety",
"category": "ethics",
"check": ""
}
],
"context": {},
"required_capabilities": [],
"input_schema": {},
"output_schema": {},
"version": "1.0.0",
"parent_version": null,
"evolution_reason": null,
"created_at": "2026-02-05 13:30:59.934460",
"updated_at": "2026-02-05 13:30:59.934462"
},
"required_tools": [
"web_scrape",
"send_email",
"web_search"
],
"metadata": {
"created_at": "2026-02-05T13:32:44.573712",
"node_count": 4,
"edge_count": 3
}
}
@@ -1,306 +0,0 @@
"""Agent graph construction for Twitter Outreach Agent."""
from framework.graph import EdgeSpec, EdgeCondition, Goal, SuccessCriterion, Constraint
from framework.graph.edge import GraphSpec
from framework.graph.executor import ExecutionResult, GraphExecutor
from framework.runtime.event_bus import EventBus
from framework.runtime.core import Runtime
from framework.llm import LiteLLMProvider
from framework.runner.tool_registry import ToolRegistry
from .config import default_config, metadata
from .nodes import (
intake_node,
research_node,
draft_review_node,
send_node,
)
# Goal definition
goal = Goal(
id="twitter-outreach",
name="Personalized Twitter Outreach",
description=(
"Given a Twitter/X handle and outreach context, research the target's profile "
"(bio, tweets, interests), craft a personalized outreach email referencing their "
"specific activity, and send it after user approval."
),
success_criteria=[
SuccessCriterion(
id="profile-research",
description="Agent extracts meaningful information from target's Twitter profile including bio, recent tweets, interests, and topics they engage with",
metric="research_quality",
target="Identifies at least 3 distinct profile details",
weight=0.25,
),
SuccessCriterion(
id="email-personalization",
description="Drafted email references at least 2 specific details from the target's Twitter profile",
metric="personalization_score",
target="At least 2 specific references to profile content",
weight=0.25,
),
SuccessCriterion(
id="clear-cta",
description="Email includes a specific relevant call to action",
metric="cta_present",
target="Email contains clear call to action",
weight=0.15,
),
SuccessCriterion(
id="user-approval-gate",
description="Email is presented to user for review and only sent after explicit approval with opportunity to request edits",
metric="approval_obtained",
target="User explicitly approves before send",
weight=0.2,
),
SuccessCriterion(
id="successful-delivery",
description="Email is sent successfully via the send_email tool",
metric="delivery_status",
target="Email sent without errors",
weight=0.15,
),
],
constraints=[
Constraint(
id="no-spam",
description="Email must not use spammy language, clickbait, or aggressive sales tactics",
constraint_type="quality",
category="content",
),
Constraint(
id="approval-required",
description="Must never send an email without explicit user approval",
constraint_type="safety",
category="process",
),
Constraint(
id="tone-appropriate",
description="Email tone must be professional, authentic, and conversational — not robotic or overly formal",
constraint_type="quality",
category="content",
),
Constraint(
id="privacy-respect",
description="Only use publicly available information from the target's Twitter profile",
constraint_type="safety",
category="ethics",
),
],
)
# Node list
nodes = [
intake_node,
research_node,
draft_review_node,
send_node,
]
# Edge definitions
edges = [
EdgeSpec(
id="intake-to-research",
source="intake",
target="research",
condition=EdgeCondition.ON_SUCCESS,
priority=1,
),
EdgeSpec(
id="research-to-draft-review",
source="research",
target="draft-review",
condition=EdgeCondition.ON_SUCCESS,
priority=1,
),
EdgeSpec(
id="draft-review-to-send",
source="draft-review",
target="send",
condition=EdgeCondition.ON_SUCCESS,
priority=1,
),
]
# Graph configuration
entry_node = "intake"
entry_points = {"start": "intake"}
pause_nodes = []
terminal_nodes = ["send"]
class TwitterOutreachAgent:
"""
Twitter Outreach Agent 4-node pipeline with user approval checkpoint.
Flow: intake -> research -> draft-review -> send
"""
def __init__(self, config=None):
self.config = config or default_config
self.goal = goal
self.nodes = nodes
self.edges = edges
self.entry_node = entry_node
self.entry_points = entry_points
self.pause_nodes = pause_nodes
self.terminal_nodes = terminal_nodes
self._executor: GraphExecutor | None = None
self._graph: GraphSpec | None = None
self._event_bus: EventBus | None = None
self._tool_registry: ToolRegistry | None = None
def _build_graph(self) -> GraphSpec:
"""Build the GraphSpec."""
return GraphSpec(
id="twitter-outreach-graph",
goal_id=self.goal.id,
version="1.0.0",
entry_node=self.entry_node,
entry_points=self.entry_points,
terminal_nodes=self.terminal_nodes,
pause_nodes=self.pause_nodes,
nodes=self.nodes,
edges=self.edges,
default_model=self.config.model,
max_tokens=self.config.max_tokens,
loop_config={
"max_iterations": 50,
"max_tool_calls_per_turn": 10,
"max_history_tokens": 32000,
},
)
def _setup(self) -> GraphExecutor:
"""Set up the executor with all components."""
from pathlib import Path
storage_path = Path.home() / ".hive" / "twitter_outreach"
storage_path.mkdir(parents=True, exist_ok=True)
self._event_bus = EventBus()
self._tool_registry = ToolRegistry()
mcp_config_path = Path(__file__).parent / "mcp_servers.json"
if mcp_config_path.exists():
self._tool_registry.load_mcp_config(mcp_config_path)
llm = LiteLLMProvider(
model=self.config.model,
api_key=self.config.api_key,
api_base=self.config.api_base,
)
tool_executor = self._tool_registry.get_executor()
tools = list(self._tool_registry.get_tools().values())
self._graph = self._build_graph()
runtime = Runtime(storage_path)
self._executor = GraphExecutor(
runtime=runtime,
llm=llm,
tools=tools,
tool_executor=tool_executor,
event_bus=self._event_bus,
storage_path=storage_path,
loop_config=self._graph.loop_config,
)
return self._executor
async def start(self) -> None:
"""Set up the agent (initialize executor and tools)."""
if self._executor is None:
self._setup()
async def stop(self) -> None:
"""Clean up resources."""
self._executor = None
self._event_bus = None
async def trigger_and_wait(
self,
entry_point: str,
input_data: dict,
timeout: float | None = None,
session_state: dict | None = None,
) -> ExecutionResult | None:
"""Execute the graph and wait for completion."""
if self._executor is None:
raise RuntimeError("Agent not started. Call start() first.")
if self._graph is None:
raise RuntimeError("Graph not built. Call start() first.")
return await self._executor.execute(
graph=self._graph,
goal=self.goal,
input_data=input_data,
session_state=session_state,
)
async def run(self, context: dict, session_state=None) -> ExecutionResult:
"""Run the agent (convenience method for single execution)."""
await self.start()
try:
result = await self.trigger_and_wait(
"start", context, session_state=session_state
)
return result or ExecutionResult(success=False, error="Execution timeout")
finally:
await self.stop()
def info(self):
"""Get agent information."""
return {
"name": metadata.name,
"version": metadata.version,
"description": metadata.description,
"goal": {
"name": self.goal.name,
"description": self.goal.description,
},
"nodes": [n.id for n in self.nodes],
"edges": [e.id for e in self.edges],
"entry_node": self.entry_node,
"entry_points": self.entry_points,
"pause_nodes": self.pause_nodes,
"terminal_nodes": self.terminal_nodes,
"client_facing_nodes": [n.id for n in self.nodes if n.client_facing],
}
def validate(self):
"""Validate agent structure."""
errors = []
warnings = []
node_ids = {node.id for node in self.nodes}
for edge in self.edges:
if edge.source not in node_ids:
errors.append(f"Edge {edge.id}: source '{edge.source}' not found")
if edge.target not in node_ids:
errors.append(f"Edge {edge.id}: target '{edge.target}' not found")
if self.entry_node not in node_ids:
errors.append(f"Entry node '{self.entry_node}' not found")
for terminal in self.terminal_nodes:
if terminal not in node_ids:
errors.append(f"Terminal node '{terminal}' not found")
for ep_id, node_id in self.entry_points.items():
if node_id not in node_ids:
errors.append(
f"Entry point '{ep_id}' references unknown node '{node_id}'"
)
return {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
}
# Create default instance
default_agent = TwitterOutreachAgent()
@@ -1,25 +0,0 @@
"""Runtime configuration."""
from dataclasses import dataclass
from framework.config import RuntimeConfig
default_config = RuntimeConfig()
@dataclass
class AgentMetadata:
name: str = "Twitter Outreach Agent"
version: str = "1.0.0"
description: str = (
"Reads a target's Twitter/X profile, crafts a personalized outreach email "
"referencing their specific activity, and sends it after user approval."
)
intro_message: str = (
"Hi! I can help you with personalized Twitter outreach. Give me a Twitter/X "
"handle and I'll analyze their profile, then craft a tailored outreach email "
"for your approval."
)
metadata = AgentMetadata()
@@ -1,9 +0,0 @@
{
"hive-tools": {
"transport": "stdio",
"command": "uv",
"args": ["run", "python", "mcp_server.py", "--stdio"],
"cwd": "../../../tools",
"description": "Hive tools MCP server providing web_search, web_scrape, and send_email"
}
}
@@ -1,137 +0,0 @@
"""Node definitions for Twitter Outreach Agent."""
from framework.graph import NodeSpec
# Node 1: Intake (client-facing)
# Collect the target Twitter handle, outreach purpose, and recipient email.
intake_node = NodeSpec(
id="intake",
name="Intake",
description="Collect the target Twitter handle, outreach purpose, and recipient email from the user",
node_type="event_loop",
client_facing=True,
input_keys=[],
output_keys=["twitter_handle", "outreach_context", "recipient_email"],
system_prompt="""\
You are the intake assistant for a personalized Twitter outreach agent.
**STEP 1 Respond to the user (text only, NO tool calls):**
Greet the user and ask them to provide:
1. The Twitter/X handle of the person they want to reach out to
2. The purpose/context of the outreach (e.g., partnership opportunity, hiring, collaboration, introduction)
3. The recipient's email address
Be friendly and concise. If the user provides partial info, ask for what's missing.
**STEP 2 After the user provides ALL three pieces of information, call set_output:**
- set_output("twitter_handle", "<the Twitter handle, including @>")
- set_output("outreach_context", "<the outreach purpose/context>")
- set_output("recipient_email", "<the email address>")
""",
tools=[],
)
# Node 2: Research
# Searches the web and scrapes the target's Twitter/X profile to build a comprehensive summary.
research_node = NodeSpec(
id="research",
name="Research",
description="Research the target's Twitter/X profile — bio, recent tweets, interests, and topics they engage with",
node_type="event_loop",
input_keys=["twitter_handle"],
output_keys=["profile_summary"],
system_prompt="""\
You are a Twitter/X profile researcher. Your job is to thoroughly research a person's public Twitter/X presence.
Given the Twitter handle provided in your inputs, do the following:
1. Use web_search to find their Twitter/X profile and any relevant public information about them.
2. Use web_scrape to read their Twitter/X profile page (try https://x.com/{handle} or https://twitter.com/{handle}).
3. Extract and analyze:
- Their bio and self-description
- Recent tweets and topics they post about
- Professional interests, projects, or accomplishments
- Any recurring themes or passions
- Specific tweets worth referencing in outreach
4. Look for additional context (personal website, blog, other social profiles mentioned in bio).
Compile a comprehensive profile summary that would help someone write a highly personalized outreach email.
Use set_output("profile_summary", <your detailed summary as a string>) to store your findings.
Do NOT return raw JSON. Use the set_output tool to produce outputs.
""",
tools=["web_search", "web_scrape"],
)
# Node 3: Draft & Review (client-facing)
# Drafts a personalized email, presents to user, iterates until approved.
draft_review_node = NodeSpec(
id="draft-review",
name="Draft & Review",
description="Draft a personalized outreach email using profile research, present to user for review, and iterate until approved",
node_type="event_loop",
client_facing=True,
input_keys=["outreach_context", "recipient_email", "profile_summary"],
output_keys=["approved_email"],
system_prompt="""\
You are an expert email copywriter specializing in personalized outreach.
You have been given:
- A profile summary of the target person (from their Twitter/X)
- The outreach context/purpose
- The recipient's email address
**STEP 1 Draft and present the email (text only, NO tool calls):**
Using the profile research, draft a personalized outreach email that:
- References at least 2 specific details from their Twitter profile (tweets, interests, projects)
- Clearly connects to the outreach purpose
- Includes a specific, relevant call to action
- Is professional but conversational and authentic NOT spammy, robotic, or overly formal
- Is concise (under 300 words)
Present the complete email draft to the user, formatted clearly with Subject line and Body.
Then ask: "Would you like any changes, or shall I send this?"
If the user requests changes, revise the email and present the updated version. Keep iterating until the user is satisfied.
**STEP 2 After the user explicitly approves the email, call set_output:**
- set_output("approved_email", "<the final approved email text including subject line>")
""",
tools=[],
)
# Node 4: Send
# Sends the approved email using the send_email tool.
send_node = NodeSpec(
id="send",
name="Send",
description="Send the approved outreach email to the recipient",
node_type="event_loop",
input_keys=["approved_email", "recipient_email"],
output_keys=["delivery_status"],
system_prompt="""\
You are responsible for sending the approved outreach email.
You have the approved email text and the recipient's email address in your inputs.
Parse the subject line and body from the approved_email, then use the send_email tool to send it to the recipient_email address.
After sending successfully, call:
- set_output("delivery_status", "sent")
If there is an error sending, call:
- set_output("delivery_status", "failed: <error details>")
Do NOT return raw JSON. Use the set_output tool to produce outputs.
""",
tools=["send_email"],
)
__all__ = [
"intake_node",
"research_node",
"draft_review_node",
"send_node",
]
+3 -4
View File
@@ -66,7 +66,7 @@ prompt_choice() {
local choice
while true; do
read -r -p "Enter choice (1-${#options[@]}): " choice
read -r -p "Enter choice (1-${#options[@]}): " choice || true
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#options[@]}" ]; then
PROMPT_CHOICE=$((choice - 1))
return 0
@@ -602,8 +602,7 @@ prompt_model_selection() {
local choice
while true; do
read -r -p "Enter choice [1]: " choice
choice="${choice:-1}"
read -r -p "Enter choice (1-$count): " choice || true
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$count" ]; then
local idx=$((choice - 1))
SELECTED_MODEL="$(get_model_choice_id "$provider_id" "$idx")"
@@ -721,7 +720,7 @@ if [ ${#FOUND_PROVIDERS[@]} -gt 0 ]; then
echo ""
while true; do
read -r -p "Enter choice (1-$max_choice): " choice
read -r -p "Enter choice (1-$max_choice): " choice || true
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$max_choice" ]; then
if [ "$choice" -eq "$max_choice" ]; then
# Fall through to the manual provider selection below