Skip to content
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 Down Expand Up @@ -275,6 +276,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 @@ -237,6 +237,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 Down Expand Up @@ -268,6 +269,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 @@ -290,13 +292,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 @@ -312,6 +316,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
1 change: 1 addition & 0 deletions src/openhuman/agent/harness/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ fn set_connected_integrations_marks_session_initialized_and_updates_hash() {
tools: vec![],
gated_tools: vec![],
connected: true,
non_active_status: None,
},
]);

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 @@ -727,6 +727,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
86 changes: 86 additions & 0 deletions src/openhuman/composio/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1678,6 +1678,86 @@ 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.clone())
.and_modify(|cur| {
if p > cur.0 {
tracing::debug!(
target: "composio",
toolkit = %slug,
previous_status = %cur.1,
previous_priority = cur.0,
new_status = %conn.status,
new_priority = p,
"[composio] non_active_status_by_slug: upgraded most-informative status"
);
*cur = (p, conn.status.clone());
} else {
tracing::trace!(
target: "composio",
toolkit = %slug,
kept_status = %cur.1,
kept_priority = cur.0,
candidate_status = %conn.status,
candidate_priority = p,
"[composio] non_active_status_by_slug: kept higher-priority status"
);
}
})
.or_insert_with(|| {
tracing::debug!(
target: "composio",
toolkit = %slug,
status = %conn.status,
priority = p,
"[composio] non_active_status_by_slug: first non-active row"
);
(p, conn.status.clone())
});
}
let final_map: std::collections::HashMap<String, String> =
map.into_iter().map(|(k, (_, v))| (k, v)).collect();
tracing::debug!(
target: "composio",
entries = final_map.len(),
"[composio] non_active_status_by_slug: final map"
);
final_map
};

// 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 @@ -1774,6 +1854,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 All @@ -1789,6 +1874,7 @@ async fn fetch_connected_integrations_uncached(
tracing::debug!(
toolkit = %ci.toolkit,
connected = ci.connected,
non_active_status = ?ci.non_active_status,
tool_count = ci.tools.len(),
"[composio] integration overview"
);
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 @@ -926,6 +926,7 @@ fn integration(toolkit: &str, connected: bool) -> ConnectedIntegration {
tools: Vec::new(),
gated_tools: Vec::new(),
connected,
non_active_status: None,
}
}

Expand Down
Loading
Loading