Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/openhuman/agent/agents/integrations_agent/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ mod tests {
tools: Vec::new(),
gated_tools: Vec::new(),
connected: true,
non_active_status: None,
}];
let body = build(&ctx_with(&integrations, &[])).unwrap();
assert!(body.contains("## Connected Integrations"));
Expand All @@ -265,6 +266,7 @@ mod tests {
tools: Vec::new(),
gated_tools: Vec::new(),
connected: false,
non_active_status: None,
}];
let body = build(&ctx_with(&integrations, &[])).unwrap();
assert!(!body.contains("## Connected Integrations"));
Expand Down
5 changes: 5 additions & 0 deletions src/openhuman/agent/agents/orchestrator/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ mod tests {
tools: Vec::new(),
gated_tools: Vec::new(),
connected: true,
non_active_status: None,
}];
let body = build(&ctx_with(&integrations)).unwrap();
assert!(body.contains("## Connected Integrations"));
Expand All @@ -245,6 +246,7 @@ mod tests {
tools: Vec::new(),
gated_tools: Vec::new(),
connected: true,
non_active_status: None,
}];
let body = build(&ctx_with(&integrations)).unwrap();
assert!(body.contains("## Connected Integrations"));
Expand All @@ -267,13 +269,15 @@ mod tests {
tools: Vec::new(),
gated_tools: Vec::new(),
connected: true,
non_active_status: None,
},
ConnectedIntegration {
toolkit: "linear".into(),
description: "Tracker.".into(),
tools: Vec::new(),
gated_tools: Vec::new(),
connected: false,
non_active_status: None,
},
];
let body = build(&ctx_with(&integrations)).unwrap();
Expand All @@ -289,6 +293,7 @@ mod tests {
tools: Vec::new(),
gated_tools: Vec::new(),
connected: false,
non_active_status: None,
}];
let body = build(&ctx_with(&integrations)).unwrap();
assert!(!body.contains("## Connected Integrations"));
Expand Down
2 changes: 2 additions & 0 deletions src/openhuman/agent/agents/welcome/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,15 @@ mod tests {
tools: Vec::new(),
gated_tools: Vec::new(),
connected: true,
non_active_status: None,
},
ConnectedIntegration {
toolkit: "notion".into(),
description: "Pitch during onboarding.".into(),
tools: Vec::new(),
gated_tools: Vec::new(),
connected: false,
non_active_status: None,
},
];
let body = build(&ctx_with(&integrations)).unwrap();
Expand Down
4 changes: 4 additions & 0 deletions src/openhuman/agent/harness/subagent_runner/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,10 @@ async fn run_typed_mode(
// the user pref and doesn't change per-spawn.
gated_tools: cached_integration.gated_tools.clone(),
connected: cached_integration.connected,
// Inherit the cached non-active status — this spawn
// path only fires on connected toolkits, but keep the
// field consistent with the source row for #2365.
non_active_status: cached_integration.non_active_status.clone(),
};
let integration = &integration;
// Fuzzy-filter the toolkit's actions against the task prompt
Expand Down
1 change: 1 addition & 0 deletions src/openhuman/agent/harness/test_support_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,7 @@ async fn orchestrator_prompt_drives_composio_call_via_delegation_chain() {
tools: Vec::new(),
gated_tools: Vec::new(),
connected: true,
non_active_status: None,
}];
let ctx = {
use crate::openhuman::context::prompt::{LearnedContextData, ToolCallFormat};
Expand Down
13 changes: 13 additions & 0 deletions src/openhuman/agent/prompts/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,19 @@ pub struct ConnectedIntegration {
/// and the orchestrator must point the user at Settings instead of
/// attempting to delegate.
pub connected: bool,
/// Raw upstream connection status when a connection row exists but
/// is not `ACTIVE` — e.g. `"INITIATED"`, `"INITIALIZING"`,
/// `"FAILED"`, `"EXPIRED"`. `None` means either the user is
/// `ACTIVE` (use `connected = true`) OR there is no connection
/// row at all (truly disconnected).
///
/// Used by the `integrations_agent` spawn-gate to surface the
/// real reason a delegation can't proceed — see issue #2365
/// ("Agent says Gmail is disconnected when sending email"). The
/// gate previously emitted the same "not authorized yet" message
/// regardless of whether OAuth was mid-flight, the token had
/// expired, or the user had simply never started the flow.
pub non_active_status: Option<String>,
}

/// A toolkit action that exists in the catalog but is currently hidden from
Expand Down
50 changes: 50 additions & 0 deletions src/openhuman/composio/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1668,6 +1668,51 @@ async fn fetch_connected_integrations_uncached(
.filter(|toolkit| !toolkit.is_empty())
.collect();

// Most-informative *non-active* status per toolkit slug. Lets the
// integrations_agent spawn-gate (#2365) emit a precise message
// when a connection row exists but isn't usable yet (`INITIATED`
// — OAuth still in progress) or any longer (`EXPIRED` / `FAILED`)
// — instead of the legacy generic "available but not authorized".
//
// Status priority (UI-actionability):
// 1. EXPIRED — reconnect path
// 2. FAILED / ERROR — reconnect path
// 3. INITIATED / INITIALIZING / PENDING — finish OAuth in browser
// 4. anything else — passes through verbatim
let non_active_status_by_slug: std::collections::HashMap<String, String> = {
fn priority(status: &str) -> u8 {
let s = status.trim().to_ascii_uppercase();
match s.as_str() {
"EXPIRED" => 4,
"FAILED" | "ERROR" => 3,
"INITIATED" | "INITIALIZING" | "PENDING" => 2,
_ => 1,
}
}
let mut map: std::collections::HashMap<String, (u8, String)> =
std::collections::HashMap::new();
for conn in connections.iter().filter(|c| !c.is_active()) {
let slug = conn.normalized_toolkit();
if slug.is_empty() {
continue;
}
// Don't override an ACTIVE-slug — those carry no non-active
// status from this map's perspective.
if connected_slugs.contains(&slug) {
continue;
}
let p = priority(&conn.status);
map.entry(slug)
.and_modify(|cur| {
if p > cur.0 {
*cur = (p, conn.status.clone());
}
})
.or_insert_with(|| (p, conn.status.clone()));
}
map.into_iter().map(|(k, (_, v))| (k, v)).collect()
};

// Deduplicate the allowlist so a backend that returns duplicates
// doesn't produce dual entries downstream.
let mut unique_toolkits: Vec<String> = allowlisted_toolkits.clone();
Expand Down Expand Up @@ -1764,6 +1809,11 @@ async fn fetch_connected_integrations_uncached(
tools,
gated_tools,
connected,
non_active_status: if connected {
None
} else {
non_active_status_by_slug.get(slug).cloned()
},
});
}

Expand Down
1 change: 1 addition & 0 deletions src/openhuman/composio/ops_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,7 @@ fn integration(toolkit: &str, connected: bool) -> ConnectedIntegration {
tools: Vec::new(),
gated_tools: Vec::new(),
connected,
non_active_status: None,
}
}

Expand Down
Loading
Loading