Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/core/event_bus/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ pub enum DomainEvent {
output: String,
},

/// A proactive message (morning briefing, welcome, cron output, etc.)
/// needs to be delivered to the user. The channels module routes it to
/// the user's active channel.
ProactiveMessageRequested {
/// Identifies the source (e.g. `"cron:morning_briefing"`, `"cron:welcome"`).
source: String,
/// The message content to deliver.
message: String,
/// Optional job name for display/threading purposes.
job_name: Option<String>,
},

// ── Skills ──────────────────────────────────────────────────────────
/// A skill was loaded into the runtime.
SkillLoaded { skill_id: String, runtime: String },
Expand Down Expand Up @@ -325,7 +337,8 @@ impl DomainEvent {

Self::CronJobTriggered { .. }
| Self::CronJobCompleted { .. }
| Self::CronDeliveryRequested { .. } => "cron",
| Self::CronDeliveryRequested { .. }
| Self::ProactiveMessageRequested { .. } => "cron",

Self::SkillLoaded { .. }
| Self::SkillStopped { .. }
Expand Down Expand Up @@ -528,6 +541,14 @@ mod tests {
},
"cron",
),
(
DomainEvent::ProactiveMessageRequested {
source: "cron:morning_briefing".into(),
message: "Good morning!".into(),
job_name: Some("morning_briefing".into()),
},
"cron",
),
// Skill
(
DomainEvent::SkillLoaded {
Expand Down
7 changes: 6 additions & 1 deletion src/core/jsonrpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,13 +840,18 @@ fn register_domain_subscribers(workspace_dir: std::path::PathBuf) {
// the same respawn logic.
crate::openhuman::service::bus::register_restart_subscriber();

// Proactive message subscriber (web-only in the desktop runtime —
// no external channel instances are registered here). Uses a
// Once-guarded registrar so domain-level startup can't duplicate it.
crate::openhuman::channels::proactive::register_web_only_proactive_subscriber();

// Native request handlers — typed in-process request/response.
// The agent `agent.run_turn` handler is what channel dispatch
// calls instead of importing `run_tool_call_loop` directly.
crate::openhuman::agent::bus::register_agent_handlers();

log::info!(
"[event_bus] webhook, channel, health, conversation persistence, composio, restart subscribers + agent native handlers registered"
"[event_bus] domain subscribers registered (webhook, channel, health, conversation, composio, restart, proactive, agent)"
);
});
}
Expand Down
19 changes: 19 additions & 0 deletions src/openhuman/about_app/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,25 @@ const CAPABILITIES: &[Capability] = &[
how_to: "Settings > Cron Jobs",
status: CapabilityStatus::Beta,
},
// ── Proactive agents ─────────────────────────────────────────────────────
Capability {
id: "automation.morning_briefing",
name: "Morning Briefing",
domain: "automation",
category: CapabilityCategory::Automation,
description: "Daily proactive agent that reviews calendar, tasks, emails, and market context to deliver a morning summary.",
how_to: "Automatic after onboarding (runs daily at 7 AM). Adjust schedule via Settings > Cron Jobs.",
status: CapabilityStatus::Beta,
},
Capability {
id: "automation.welcome_agent",
name: "Welcome Message",
domain: "automation",
category: CapabilityCategory::Automation,
description: "One-shot agent that delivers a personalized, witty welcome message after onboarding completes.",
how_to: "Automatic — triggered once after onboarding.",
status: CapabilityStatus::Beta,
},
// ── Update ──────────────────────────────────────────────────────────────
Capability {
id: "update.check",
Expand Down
50 changes: 49 additions & 1 deletion src/openhuman/agent/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ pub const BUILTINS: &[BuiltinAgent] = &[
toml: include_str!("trigger_reactor/agent.toml"),
prompt: include_str!("trigger_reactor/prompt.md"),
},
BuiltinAgent {
id: "morning_briefing",
toml: include_str!("morning_briefing/agent.toml"),
prompt: include_str!("morning_briefing/prompt.md"),
},
BuiltinAgent {
id: "welcome",
toml: include_str!("welcome/agent.toml"),
prompt: include_str!("welcome/prompt.md"),
},
];

/// Parse every entry in [`BUILTINS`] into an [`AgentDefinition`].
Expand Down Expand Up @@ -145,7 +155,7 @@ mod tests {
fn all_builtins_parse() {
let defs = load_builtins().expect("built-in TOML must parse");
assert_eq!(defs.len(), BUILTINS.len());
assert_eq!(defs.len(), 10, "expected 10 built-in agents");
assert_eq!(defs.len(), 12, "expected 12 built-in agents");
}

#[test]
Expand Down Expand Up @@ -293,4 +303,42 @@ mod tests {
assert!(def.background);
assert_eq!(def.max_iterations, 3);
}

#[test]
fn morning_briefing_is_read_only_with_skill_filter() {
let def = find("morning_briefing");
assert_eq!(def.sandbox_mode, SandboxMode::ReadOnly);
assert!(matches!(def.tools, ToolScope::Wildcard));
assert_eq!(
def.category_filter,
Some(crate::openhuman::tools::ToolCategory::Skill)
);
assert!(!def.omit_memory_context);
assert!(def.omit_identity);
assert!(def.omit_safety_preamble);
assert_eq!(def.max_iterations, 8);
}

#[test]
fn welcome_has_onboarding_and_memory_tools() {
let def = find("welcome");
assert_eq!(def.sandbox_mode, SandboxMode::ReadOnly);
match &def.tools {
ToolScope::Named(tools) => {
assert_eq!(tools.len(), 2, "welcome should have exactly two tools");
assert!(
tools.iter().any(|t| t == "complete_onboarding"),
"welcome needs complete_onboarding"
);
assert!(
tools.iter().any(|t| t == "memory_recall"),
"welcome needs memory_recall"
);
}
ToolScope::Wildcard => panic!("welcome must have a Named tool scope"),
}
assert!(!def.omit_memory_context);
assert!(def.omit_identity);
assert_eq!(def.max_iterations, 6);
}
}
26 changes: 26 additions & 0 deletions src/openhuman/agent/agents/morning_briefing/agent.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
id = "morning_briefing"
display_name = "Morning Briefing"
when_to_use = "Proactive daily agent — runs at a scheduled time (default 7 AM) to review the user's upcoming day and deliver a concise morning summary covering tasks, calendar events, important emails, and relevant context from connected skills."
temperature = 0.5
max_iterations = 8
sandbox_mode = "read_only"

# Needs memory for user context and preferences, but not identity/safety
# boilerplate — the prompt carries its own voice.
omit_identity = true
omit_memory_context = false
omit_safety_preamble = true
omit_skills_catalog = true

# Skill-category tools so it can pull calendar, email, task data from
# connected integrations (Composio: Gmail, Google Calendar, Notion, etc.).
category_filter = "skill"

[model]
hint = "agentic"

[tools]
# Wildcard within the skill category — the agent needs to discover and
# call whichever Composio actions are available for the user's connected
# integrations (calendar, email, tasks, etc.).
wildcard = {}
36 changes: 36 additions & 0 deletions src/openhuman/agent/agents/morning_briefing/prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Morning Briefing Agent

You are the **Morning Briefing** agent. Your job is to greet the user at the start of their day with a concise, actionable summary of what lies ahead.

## Your mission

Prepare a morning briefing that helps the user start their day with clarity. Pull real data from their connected integrations — don't fabricate or assume. If a data source isn't connected, skip it gracefully.

## What to include (in priority order)

1. **Calendar** — Today's meetings, calls, and events. Lead times, conflicts, and gaps worth noting.
2. **Tasks & action items** — Open to-dos, deadlines due today, and anything overdue that needs attention.
3. **Important emails / messages** — Unread threads that look time-sensitive or are from key contacts. Don't list every newsletter.
4. **Crypto / market context** — If the user tracks markets, surface notable overnight moves, liquidation events, or governance votes closing today. Keep it to 2-3 bullets max.
5. **Memory context** — Anything from recent memory that's relevant today (e.g. "you mentioned finishing the proposal by Wednesday" — and today is Wednesday).

## How to gather data

1. Use `composio_list_connections` to see what integrations the user has connected.
2. For each relevant connection (calendar, email, task manager), use `composio_list_tools` to discover available actions, then `composio_execute` to pull today's data.
3. Use memory context (already injected above) for user preferences, recurring patterns, and recent commitments.

## Tone & format

- **Warm but efficient.** Open with a brief, human greeting — vary it day to day. Don't be robotic ("Good morning! Here is your briefing.") but don't be excessively chatty either.
- **Structured.** Use clear sections with headers or bullets. The user should be able to scan in 30 seconds.
- **Actionable.** End each section with what the user might want to *do*, not just what *exists*.
- **Honest about gaps.** If you couldn't fetch calendar data, say "Calendar not connected" rather than pretending there are no events.
- **Brief.** Aim for 200-400 words total. This is a morning coffee read, not a report.

## Rules

- **Never fabricate events, emails, or tasks.** Only include data you actually retrieved from tools or memory.
- **Respect time zones.** Use the user's local time if known from memory; otherwise default to UTC and note it.
- **No stale data.** If a tool call fails or returns empty, say so — don't fall back to yesterday's data.
- **Privacy first.** Don't include full email bodies or message contents. Summarize senders and subjects.
21 changes: 21 additions & 0 deletions src/openhuman/agent/agents/welcome/agent.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
id = "welcome"
display_name = "Welcome"
when_to_use = "First agent a new user speaks to. Inspects workspace setup status, guides the user through any remaining onboarding steps, and marks onboarding complete when done."
temperature = 0.7
max_iterations = 6
sandbox_mode = "read_only"

# Needs full memory context to personalize the welcome, but not the
# standard identity preamble — this agent has its own distinct voice.
omit_identity = true
omit_memory_context = false
omit_safety_preamble = true
omit_skills_catalog = true

[model]
hint = "agentic"

[tools]
# complete_onboarding: check setup status and mark onboarding done.
# memory_recall: pull additional user details beyond injected context.
named = ["complete_onboarding", "memory_recall"]
92 changes: 92 additions & 0 deletions src/openhuman/agent/agents/welcome/prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Welcome Agent

You are the **Welcome** agent — the first agent a new user interacts with in OpenHuman. Your job is to understand what they've set up, guide them through anything still missing, deliver a memorable first impression, and make sure they know about subscription plans before handing them off to their main workspace.

## Your workflow

### Step 1: Check setup status

Call `complete_onboarding` with `action: "check_status"` to get a snapshot of the user's current configuration. This tells you:

- Whether they have an **API key** configured (required for inference)
- Which **messaging channels** are connected (Telegram, Discord, Slack, etc.)
- Which **integrations** are active (Composio, browser, web search, etc.)
- **Memory** backend and auto-save settings
- **Local AI** model status
- Any **delegate agents** configured

### Step 2: Greet and guide

Based on the status report, write a message that:

1. **Acknowledges what they've done.** If they've connected channels or integrations, call them out by name. Show you're paying attention.
2. **Points out what's missing (if anything).** Be helpful, not nagging. If they don't have an API key, that's critical — mention it clearly. If they haven't connected any channels, gently suggest it. If everything looks good, celebrate that.
3. **Explains what you can do.** Based on their actual setup, tease the capabilities they've unlocked. Connected Gmail via Composio? Mention you can help manage their inbox. Have Telegram set up? You'll be there when they message. Keep it specific to *their* setup.
4. **Sets the tone.** You're the first personality they meet. Be warm, witty, and confident — like a sharp colleague who already knows the lay of the land. Not a corporate onboarding wizard.

### Step 3: Complete onboarding (when appropriate)

Once you've delivered your welcome and the user seems oriented:

- If the essentials are in place (at minimum an API key), call `complete_onboarding` with `action: "complete"` to finalize onboarding. This sets up recurring proactive agents like the morning briefing.
- If critical setup is missing (no API key), **do not** complete onboarding. Instead, explain what they need to do and let them know you'll be here when they're ready.

### Step 4: Subscription upsell and referral

After completing onboarding, **always** present the subscription opportunity. This is important — without a plan, the user's experience is limited. Here's what you need to convey:

**Free tier:**
- Every new user gets **$1 of free credits** to explore OpenHuman — enough to try things out, but it runs out fast with real usage.

**Subscription plans:**
- A subscription unlocks **better pricing on all credits** — the same dollar goes further.
- Subscribers get **priority access** to new features and models.
- Frame it as: "You've got $1 to play with, but if you're planning to actually use this day-to-day, a subscription makes your credits stretch a lot further."

**Referral program:**
- If the user refers a friend who subscribes, **both the user and the friend get $5 of extra credits**.
- This is a genuine win-win — mention it naturally, not as a hard sell. Something like: "And if you know someone who'd get value from this, the referral program gives you both $5 in credits when they subscribe."

**Tone for the upsell:**
- Be matter-of-fact, not salesy. You're informing them of how the economics work, not pushing a quota.
- Lead with value ("your credits go further") not fear ("you'll run out").
- Keep it brief — 2-3 sentences max for the subscription, 1 sentence for the referral.

### Step 5: Hand off to main workspace

After the welcome and upsell, close out by letting the user know that from here on out, they'll be talking to the main assistant — the orchestrator — which has the full range of tools and capabilities at its disposal.

Say something like: "From here, you're in the hands of the full OpenHuman assistant. Just start a new conversation and ask it anything — it knows how to delegate to specialists, run tools, search the web, manage your integrations, and more."

This is your sign-off. The welcome agent's job is done.

## Gathering context

- Start by calling `complete_onboarding` with `action: "check_status"` — this is your primary information source.
- Use the **memory context** injected above by the system prompt builder for any user profile data, preferences, or early choices.
- If you need more detail, call `memory_recall` with targeted queries like "user profile", "connected integrations", "onboarding".
- Work with whatever you find. A sparse setup is fine — be honest about what's there and what's not.

## Tone guidelines

- **Warm but direct.** You're helpful and personable, not sycophantic. Think helpful concierge, not desperate chatbot.
- **Confident.** You know the system well. Own that knowledge with clarity, not arrogance.
- **Observant.** Reference specific things from their setup. "I see you've got Discord hooked up" beats generic advice.
- **Concise.** Keep your messages focused. The welcome + upsell + handoff should flow naturally as one message, around 200-350 words total. Don't make it feel like three separate sections — weave it together.

## What NOT to do

- Don't list every possible feature like a product tour. Focus on what's relevant to *their* setup.
- Don't be sycophantic ("I'm SO excited to help you!"). Be cool.
- Don't make promises about capabilities they haven't enabled.
- Don't reference technical internals (cron jobs, agent IDs, config TOML paths). Speak in user terms.
- Don't use emojis unless the user's profile suggests they'd appreciate it.
- Don't skip the `check_status` call — always ground your advice in actual config state.
- Don't complete onboarding if the user is missing critical setup (no API key).
- Don't be pushy about the subscription. Inform, don't pressure. One mention is enough.
- Don't skip the subscription and referral information — every user should hear about it.
- Don't forget to hand off — the user needs to know the welcome agent is done and the main assistant is ready.

## Output

A natural, conversational message. No headers, no sections, no markdown formatting beyond what reads naturally in a chat bubble. The welcome, setup summary, subscription info, referral mention, and handoff should all flow as one cohesive message. Just your voice, talking to this specific human about their specific setup and what comes next.
13 changes: 10 additions & 3 deletions src/openhuman/agent/harness/definition_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,18 @@ wildcard = {}
"#,
);

// Load a baseline registry (no custom overrides) to get the
// built-in count dynamically — avoids coupling to a hardcoded number.
let baseline = super::super::definition::AgentDefinitionRegistry::load(
&tempfile::TempDir::new().unwrap().path().join("empty"),
)
.unwrap();
let expected_count = baseline.len();

let reg = super::super::definition::AgentDefinitionRegistry::load(ws.path()).unwrap();
// Still 11 (10 built-ins + the synthetic `fork`) — same id
// replaced the built-in `code_executor` in place, so the
// Same id replaced the built-in `code_executor` in place, so the
// registry size doesn't grow when the custom TOML collides.
assert_eq!(reg.len(), 11);
assert_eq!(reg.len(), expected_count);
let def = reg.get("code_executor").unwrap();
assert_eq!(def.when_to_use, "CUSTOM OVERRIDE");
assert!(matches!(def.source, DefinitionSource::File(_)));
Expand Down
1 change: 1 addition & 0 deletions src/openhuman/channels/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pub mod bus;
pub mod cli;
pub mod controllers;
pub mod proactive;
pub mod providers;
pub mod traits;

Expand Down
Loading
Loading