mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(flows): an errored Composio tool_call fails the run instead of reporting Completed (B6) (#4659)
This commit is contained in:
@@ -1789,6 +1789,44 @@ pub(crate) async fn preflight_composio_args(
|
||||
)))
|
||||
}
|
||||
|
||||
/// Turns a Composio execute response that reports a provider-side failure
|
||||
/// into a real capability error.
|
||||
///
|
||||
/// The Composio execute endpoint is a "successful HTTP request describing an
|
||||
/// unsuccessful tool call" API: a transport-level failure (network error, 5xx,
|
||||
/// bad JSON) already surfaces as `Err` via `?` in [`OpenHumanTools::invoke`],
|
||||
/// but a 200 response whose body is `{successful: false, error: "..."}` (e.g.
|
||||
/// Slack rejecting `SLACK_SEND_MESSAGE` with a 400 "Invalid request data")
|
||||
/// comes back as `Ok(ComposioExecuteResponse)` — nothing downstream ever
|
||||
/// inspected `successful`, so the tinyflows engine recorded the step (and
|
||||
/// therefore the run) as `Success`/`"completed"` even though the requested
|
||||
/// action never actually happened upstream.
|
||||
///
|
||||
/// Called on every Composio response (never on native `oh:` tool results,
|
||||
/// which don't carry this envelope and return earlier in `invoke`). A
|
||||
/// genuinely successful response (`successful: true`) passes through
|
||||
/// unchanged; an unsuccessful one becomes `Err(EngineError::Capability(_))`,
|
||||
/// which the engine turns into `StepStatus::Error` and — via
|
||||
/// `degrade_completed_status` — a degraded/failed run instead of a false
|
||||
/// "Completed".
|
||||
fn reject_unsuccessful_composio_response(
|
||||
slug: &str,
|
||||
resp: crate::openhuman::composio::ComposioExecuteResponse,
|
||||
) -> Result<crate::openhuman::composio::ComposioExecuteResponse> {
|
||||
if resp.successful {
|
||||
return Ok(resp);
|
||||
}
|
||||
let detail = resp
|
||||
.error
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|e| !e.is_empty())
|
||||
.unwrap_or("no error detail returned by the provider");
|
||||
Err(EngineError::Capability(format!(
|
||||
"tool_call `{slug}` failed at the connected provider: {detail}"
|
||||
)))
|
||||
}
|
||||
|
||||
/// A [`ToolInvoker`] decorator that runs the host's Composio required-arg
|
||||
/// preflight before delegating to `inner`.
|
||||
///
|
||||
@@ -1997,6 +2035,12 @@ impl ToolInvoker for OpenHumanTools {
|
||||
}
|
||||
};
|
||||
|
||||
// A successful HTTP round-trip can still carry a provider-side failure
|
||||
// (`{successful: false, error: "..."}`, e.g. a Slack 400 on
|
||||
// `SLACK_SEND_MESSAGE`) — reject it into a real capability error, see
|
||||
// `reject_unsuccessful_composio_response`'s doc.
|
||||
let response = response.and_then(|resp| reject_unsuccessful_composio_response(slug, resp));
|
||||
|
||||
if let Some(id) = audit_id {
|
||||
if let Some(gate) = crate::openhuman::approval::ApprovalGate::try_global() {
|
||||
let exec = if response.is_ok() {
|
||||
@@ -2588,7 +2632,61 @@ pub fn open_flow_checkpointer(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::openhuman::agent::prompts::types::IntegrationConnection;
|
||||
use crate::openhuman::composio::ConnectedIntegration;
|
||||
use crate::openhuman::composio::{ComposioExecuteResponse, ConnectedIntegration};
|
||||
|
||||
// ── reject_unsuccessful_composio_response (B6) ──────────────────────────
|
||||
|
||||
#[test]
|
||||
fn reject_unsuccessful_composio_response_errors_on_provider_failure() {
|
||||
// Live-observed shape: SLACK_SEND_MESSAGE 400s upstream but the
|
||||
// Composio execute call itself still returns HTTP 200.
|
||||
let resp = ComposioExecuteResponse {
|
||||
data: json!({}),
|
||||
successful: false,
|
||||
error: Some("Invalid request data".to_string()),
|
||||
cost_usd: 0.0,
|
||||
markdown_formatted: None,
|
||||
};
|
||||
let err = reject_unsuccessful_composio_response("SLACK_SEND_MESSAGE", resp)
|
||||
.expect_err("unsuccessful response must become an Err");
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("SLACK_SEND_MESSAGE"), "message was: {msg}");
|
||||
assert!(msg.contains("Invalid request data"), "message was: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_unsuccessful_composio_response_falls_back_when_error_field_is_empty() {
|
||||
let resp = ComposioExecuteResponse {
|
||||
data: json!({}),
|
||||
successful: false,
|
||||
error: None,
|
||||
cost_usd: 0.0,
|
||||
markdown_formatted: None,
|
||||
};
|
||||
let err = reject_unsuccessful_composio_response("GMAIL_SEND_EMAIL", resp)
|
||||
.expect_err("unsuccessful response must become an Err");
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("GMAIL_SEND_EMAIL"), "message was: {msg}");
|
||||
assert!(
|
||||
msg.contains("no error detail returned by the provider"),
|
||||
"message was: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_unsuccessful_composio_response_passes_through_on_success() {
|
||||
let resp = ComposioExecuteResponse {
|
||||
data: json!({ "ts": "123.456" }),
|
||||
successful: true,
|
||||
error: None,
|
||||
cost_usd: 0.002,
|
||||
markdown_formatted: None,
|
||||
};
|
||||
let ok = reject_unsuccessful_composio_response("SLACK_SEND_MESSAGE", resp.clone())
|
||||
.expect("successful response must remain Ok");
|
||||
assert!(ok.successful);
|
||||
assert_eq!(ok.data, resp.data);
|
||||
}
|
||||
|
||||
// ── input_context (PR A) ────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user