[codex-analytics] guardian review analytics events emission

This commit is contained in:
rhan-oai
2026-04-13 13:40:57 -07:00
parent d1315f8e5e
commit 509a4df311
2 changed files with 547 additions and 35 deletions
+4 -2
View File
@@ -3,6 +3,7 @@ use std::sync::Arc;
use async_channel::Receiver;
use async_channel::Sender;
use codex_analytics::GuardianApprovalRequestSource;
use codex_async_utils::OrCancelExt;
use codex_exec_server::EnvironmentManager;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
@@ -40,7 +41,7 @@ use crate::codex::emit_subagent_session_started;
use crate::config::Config;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::new_guardian_review_id;
use crate::guardian::review_approval_request_with_cancel;
use crate::guardian::review_approval_request_with_source_and_cancel;
use crate::guardian::routes_approval_to_guardian;
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT;
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION;
@@ -747,12 +748,13 @@ fn spawn_guardian_review(
let _ = tx.send(ReviewDecision::Denied);
return;
};
let decision = runtime.block_on(review_approval_request_with_cancel(
let decision = runtime.block_on(review_approval_request_with_source_and_cancel(
&session,
&turn,
review_id,
request,
retry_reason,
GuardianApprovalRequestSource::DelegatedSubagent,
cancel_token,
));
let _ = tx.send(decision);
+543 -33
View File
@@ -1,31 +1,43 @@
use std::sync::Arc;
use std::time::Instant;
use codex_analytics::GuardianApprovalRequestSource;
use codex_analytics::GuardianReviewDecision;
use codex_analytics::GuardianReviewFailureReason;
use codex_analytics::GuardianReviewSessionKind;
use codex_analytics::GuardianReviewTerminalStatus;
use codex_analytics::GuardianReviewedAction;
use codex_analytics::now_unix_seconds;
use codex_features::Feature;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GuardianAssessmentDecisionSource;
use codex_protocol::protocol::GuardianAssessmentEvent;
use codex_protocol::protocol::GuardianAssessmentOutcome;
use codex_protocol::protocol::GuardianAssessmentStatus;
use codex_protocol::protocol::GuardianRiskLevel;
use codex_protocol::protocol::GuardianUserAuthorization;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::WarningEvent;
use tokio_util::sync::CancellationToken;
use crate::codex::Session;
use crate::codex::TurnContext;
use super::GUARDIAN_REVIEW_TIMEOUT;
use super::GUARDIAN_REVIEWER_NAME;
use super::GuardianApprovalRequest;
use super::GuardianAssessment;
use super::GuardianAssessmentOutcome;
use super::GuardianRejection;
use super::approval_request::guardian_assessment_action;
use super::approval_request::guardian_request_target_item_id;
use super::approval_request::guardian_request_turn_id;
use super::prompt::guardian_output_schema;
use super::prompt::parse_guardian_assessment;
use super::review_session::GuardianReviewSessionMetadata;
use super::review_session::GuardianReviewSessionOutcome;
use super::review_session::GuardianReviewSessionParams;
use super::review_session::build_guardian_review_session_config;
@@ -65,9 +77,36 @@ pub(crate) async fn guardian_rejection_message(session: &Session, review_id: &st
#[derive(Debug)]
pub(super) enum GuardianReviewOutcome {
Completed(anyhow::Result<GuardianAssessment>),
TimedOut,
Aborted,
Completed(
anyhow::Result<GuardianAssessment>,
Option<GuardianReviewSessionMetadata>,
),
Failed(GuardianReviewFailure, Option<GuardianReviewSessionMetadata>),
TimedOut(Option<GuardianReviewSessionMetadata>),
Aborted(Option<GuardianReviewSessionMetadata>),
}
#[derive(Debug)]
pub(super) enum GuardianReviewFailure {
PromptBuild(anyhow::Error),
Session(anyhow::Error),
Parse(anyhow::Error),
}
impl GuardianReviewFailure {
fn reason(&self) -> GuardianReviewFailureReason {
match self {
Self::PromptBuild(_) => GuardianReviewFailureReason::PromptBuildError,
Self::Session(_) => GuardianReviewFailureReason::SessionError,
Self::Parse(_) => GuardianReviewFailureReason::ParseError,
}
}
fn error(&self) -> &anyhow::Error {
match self {
Self::PromptBuild(err) | Self::Session(err) | Self::Parse(err) => err,
}
}
}
fn guardian_risk_level_str(level: GuardianRiskLevel) -> &'static str {
@@ -79,6 +118,220 @@ fn guardian_risk_level_str(level: GuardianRiskLevel) -> &'static str {
}
}
fn guardian_reviewed_action(request: &GuardianApprovalRequest) -> GuardianReviewedAction {
match request {
GuardianApprovalRequest::Shell {
command,
cwd,
sandbox_permissions,
additional_permissions,
justification,
..
} => GuardianReviewedAction::Shell {
command: command.clone(),
command_display: codex_shell_command::parse_command::shlex_join(command),
cwd: cwd.to_string_lossy().into_owned(),
sandbox_permissions: *sandbox_permissions,
additional_permissions: additional_permissions.clone(),
justification: justification.clone(),
},
GuardianApprovalRequest::ExecCommand {
command,
cwd,
sandbox_permissions,
additional_permissions,
justification,
tty,
..
} => GuardianReviewedAction::UnifiedExec {
command: command.clone(),
command_display: codex_shell_command::parse_command::shlex_join(command),
cwd: cwd.to_string_lossy().into_owned(),
sandbox_permissions: *sandbox_permissions,
additional_permissions: additional_permissions.clone(),
justification: justification.clone(),
tty: *tty,
},
#[cfg(unix)]
GuardianApprovalRequest::Execve {
source,
program,
argv,
cwd,
additional_permissions,
..
} => GuardianReviewedAction::Execve {
source: *source,
program: program.clone(),
argv: argv.clone(),
cwd: cwd.to_string_lossy().into_owned(),
additional_permissions: additional_permissions.clone(),
},
GuardianApprovalRequest::ApplyPatch { cwd, files, .. } => {
GuardianReviewedAction::ApplyPatch {
cwd: cwd.to_string_lossy().into_owned(),
files: files
.iter()
.map(|file| file.to_string_lossy().into_owned())
.collect(),
}
}
GuardianApprovalRequest::NetworkAccess {
target,
host,
protocol,
port,
..
} => GuardianReviewedAction::NetworkAccess {
target: target.clone(),
host: host.clone(),
protocol: *protocol,
port: *port,
},
GuardianApprovalRequest::McpToolCall {
server,
tool_name,
connector_id,
connector_name,
tool_title,
..
} => GuardianReviewedAction::McpToolCall {
server: server.clone(),
tool_name: tool_name.clone(),
connector_id: connector_id.clone(),
connector_name: connector_name.clone(),
tool_title: tool_title.clone(),
},
}
}
struct GuardianReviewAnalyticsContext {
thread_id: String,
turn_id: String,
review_id: String,
target_item_id: Option<String>,
retry_reason: Option<String>,
approval_request_source: GuardianApprovalRequestSource,
reviewed_action: GuardianReviewedAction,
started_at: u64,
started_instant: Instant,
}
struct GuardianReviewAnalyticsTerminal {
decision: GuardianReviewDecision,
terminal_status: GuardianReviewTerminalStatus,
failure_reason: Option<GuardianReviewFailureReason>,
risk_level: Option<GuardianRiskLevel>,
user_authorization: Option<GuardianUserAuthorization>,
outcome: Option<GuardianAssessmentOutcome>,
rationale: Option<String>,
guardian_thread_id: Option<String>,
guardian_session_kind: Option<GuardianReviewSessionKind>,
guardian_model: Option<String>,
guardian_reasoning_effort: Option<String>,
had_prior_review_context: Option<bool>,
reviewed_action_truncated: bool,
token_usage: Option<TokenUsage>,
time_to_first_token_ms: Option<u64>,
completed_at: u64,
}
#[derive(Default)]
struct GuardianReviewMetadataFields {
guardian_thread_id: Option<String>,
guardian_session_kind: Option<GuardianReviewSessionKind>,
guardian_model: Option<String>,
guardian_reasoning_effort: Option<String>,
had_prior_review_context: Option<bool>,
reviewed_action_truncated: bool,
token_usage: Option<TokenUsage>,
time_to_first_token_ms: Option<u64>,
}
fn guardian_review_metadata_fields(
metadata: Option<GuardianReviewSessionMetadata>,
) -> GuardianReviewMetadataFields {
match metadata {
Some(metadata) => GuardianReviewMetadataFields {
guardian_thread_id: Some(metadata.guardian_thread_id),
guardian_session_kind: Some(metadata.guardian_session_kind),
guardian_model: Some(metadata.guardian_model),
guardian_reasoning_effort: metadata.guardian_reasoning_effort,
had_prior_review_context: Some(metadata.had_prior_review_context),
reviewed_action_truncated: metadata.reviewed_action_truncated,
token_usage: metadata.token_usage,
time_to_first_token_ms: metadata.time_to_first_token_ms,
},
None => GuardianReviewMetadataFields::default(),
}
}
impl GuardianReviewAnalyticsContext {
fn track(
&self,
session: &Session,
turn: &TurnContext,
terminal: GuardianReviewAnalyticsTerminal,
) {
if !turn.config.features.enabled(Feature::GeneralAnalytics) {
return;
}
let completion_latency_ms = self.started_instant.elapsed().as_millis() as u64;
session
.services
.analytics_events_client
.track_guardian_review(codex_analytics::GuardianReviewEventParams {
thread_id: self.thread_id.clone(),
turn_id: self.turn_id.clone(),
review_id: self.review_id.clone(),
target_item_id: self.target_item_id.clone(),
retry_reason: self.retry_reason.clone(),
approval_request_source: self.approval_request_source,
reviewed_action: self.reviewed_action.clone(),
reviewed_action_truncated: terminal.reviewed_action_truncated,
decision: terminal.decision,
terminal_status: terminal.terminal_status,
failure_reason: terminal.failure_reason,
risk_level: terminal.risk_level,
user_authorization: terminal.user_authorization,
outcome: terminal.outcome,
rationale: terminal.rationale,
guardian_thread_id: terminal.guardian_thread_id,
guardian_session_kind: terminal.guardian_session_kind,
guardian_model: terminal.guardian_model,
guardian_reasoning_effort: terminal.guardian_reasoning_effort,
had_prior_review_context: terminal.had_prior_review_context,
review_timeout_ms: GUARDIAN_REVIEW_TIMEOUT.as_millis() as u64,
// TODO(rhan-oai): plumb nested Guardian review session tool-call counts.
tool_call_count: None,
time_to_first_token_ms: terminal.time_to_first_token_ms,
completion_latency_ms: Some(completion_latency_ms),
started_at: self.started_at,
completed_at: Some(terminal.completed_at),
input_tokens: terminal
.token_usage
.as_ref()
.map(|usage| usage.input_tokens),
cached_input_tokens: terminal
.token_usage
.as_ref()
.map(|usage| usage.cached_input_tokens),
output_tokens: terminal
.token_usage
.as_ref()
.map(|usage| usage.output_tokens),
reasoning_output_tokens: terminal
.token_usage
.as_ref()
.map(|usage| usage.reasoning_output_tokens),
total_tokens: terminal
.token_usage
.as_ref()
.map(|usage| usage.total_tokens),
});
}
}
/// Whether this turn should route `on-request` approval prompts through the
/// guardian reviewer instead of surfacing them to the user. ARC may still
/// block actions earlier in the flow.
@@ -105,11 +358,25 @@ async fn run_guardian_review(
review_id: String,
request: GuardianApprovalRequest,
retry_reason: Option<String>,
approval_request_source: GuardianApprovalRequestSource,
external_cancel: Option<CancellationToken>,
) -> ReviewDecision {
let started_at = now_unix_seconds();
let started_instant = Instant::now();
let target_item_id = guardian_request_target_item_id(&request).map(str::to_string);
let assessment_turn_id = guardian_request_turn_id(&request, &turn.sub_id).to_string();
let action_summary = guardian_assessment_action(&request);
let analytics_context = GuardianReviewAnalyticsContext {
thread_id: session.conversation_id.to_string(),
turn_id: assessment_turn_id.clone(),
review_id: review_id.clone(),
target_item_id: target_item_id.clone(),
retry_reason: retry_reason.clone(),
approval_request_source,
reviewed_action: guardian_reviewed_action(&request),
started_at,
started_instant,
};
session
.send_event(
turn.as_ref(),
@@ -131,6 +398,28 @@ async fn run_guardian_review(
.as_ref()
.is_some_and(CancellationToken::is_cancelled)
{
analytics_context.track(
session.as_ref(),
turn.as_ref(),
GuardianReviewAnalyticsTerminal {
decision: GuardianReviewDecision::Aborted,
terminal_status: GuardianReviewTerminalStatus::Aborted,
failure_reason: Some(GuardianReviewFailureReason::Cancelled),
risk_level: None,
user_authorization: None,
outcome: None,
rationale: None,
guardian_thread_id: None,
guardian_session_kind: None,
guardian_model: None,
guardian_reasoning_effort: None,
had_prior_review_context: None,
reviewed_action_truncated: false,
token_usage: None,
time_to_first_token_ms: None,
completed_at: now_unix_seconds(),
},
);
session
.send_event(
turn.as_ref(),
@@ -156,29 +445,173 @@ async fn run_guardian_review(
session.clone(),
turn.clone(),
request,
retry_reason,
retry_reason.clone(),
schema,
external_cancel,
)
.await;
let completed_at = now_unix_seconds();
let assessment = match outcome {
GuardianReviewOutcome::Completed(Ok(assessment)) => assessment,
GuardianReviewOutcome::Completed(Err(err)) => GuardianAssessment {
risk_level: GuardianRiskLevel::High,
user_authorization: GuardianUserAuthorization::Unknown,
outcome: GuardianAssessmentOutcome::Deny,
rationale: format!("Automatic approval review failed: {err}"),
},
GuardianReviewOutcome::TimedOut => GuardianAssessment {
risk_level: GuardianRiskLevel::High,
user_authorization: GuardianUserAuthorization::Unknown,
outcome: GuardianAssessmentOutcome::Deny,
rationale:
GuardianReviewOutcome::Completed(Ok(assessment), metadata) => {
let metadata = guardian_review_metadata_fields(metadata);
analytics_context.track(
session.as_ref(),
turn.as_ref(),
GuardianReviewAnalyticsTerminal {
decision: if matches!(assessment.outcome, GuardianAssessmentOutcome::Allow) {
GuardianReviewDecision::Approved
} else {
GuardianReviewDecision::Denied
},
terminal_status: if matches!(
assessment.outcome,
GuardianAssessmentOutcome::Allow
) {
GuardianReviewTerminalStatus::Approved
} else {
GuardianReviewTerminalStatus::Denied
},
failure_reason: None,
risk_level: Some(assessment.risk_level),
user_authorization: Some(assessment.user_authorization),
outcome: Some(assessment.outcome),
rationale: Some(assessment.rationale.clone()),
guardian_thread_id: metadata.guardian_thread_id,
guardian_session_kind: metadata.guardian_session_kind,
guardian_model: metadata.guardian_model,
guardian_reasoning_effort: metadata.guardian_reasoning_effort,
had_prior_review_context: metadata.had_prior_review_context,
reviewed_action_truncated: metadata.reviewed_action_truncated,
token_usage: metadata.token_usage,
time_to_first_token_ms: metadata.time_to_first_token_ms,
completed_at,
},
);
assessment
}
GuardianReviewOutcome::Completed(Err(err), metadata) => {
let metadata = guardian_review_metadata_fields(metadata);
let rationale = format!("Automatic approval review failed: {err}");
analytics_context.track(
session.as_ref(),
turn.as_ref(),
GuardianReviewAnalyticsTerminal {
decision: GuardianReviewDecision::Denied,
terminal_status: GuardianReviewTerminalStatus::FailedClosed,
failure_reason: Some(GuardianReviewFailureReason::SessionError),
risk_level: None,
user_authorization: None,
outcome: None,
rationale: None,
guardian_thread_id: metadata.guardian_thread_id,
guardian_session_kind: metadata.guardian_session_kind,
guardian_model: metadata.guardian_model,
guardian_reasoning_effort: metadata.guardian_reasoning_effort,
had_prior_review_context: metadata.had_prior_review_context,
reviewed_action_truncated: metadata.reviewed_action_truncated,
token_usage: metadata.token_usage,
time_to_first_token_ms: metadata.time_to_first_token_ms,
completed_at,
},
);
GuardianAssessment {
risk_level: GuardianRiskLevel::High,
user_authorization: GuardianUserAuthorization::Unknown,
outcome: GuardianAssessmentOutcome::Deny,
rationale,
}
}
GuardianReviewOutcome::Failed(failure, metadata) => {
let metadata = guardian_review_metadata_fields(metadata);
let rationale = format!("Automatic approval review failed: {}", failure.error());
analytics_context.track(
session.as_ref(),
turn.as_ref(),
GuardianReviewAnalyticsTerminal {
decision: GuardianReviewDecision::Denied,
terminal_status: GuardianReviewTerminalStatus::FailedClosed,
failure_reason: Some(failure.reason()),
risk_level: None,
user_authorization: None,
outcome: None,
rationale: None,
guardian_thread_id: metadata.guardian_thread_id,
guardian_session_kind: metadata.guardian_session_kind,
guardian_model: metadata.guardian_model,
guardian_reasoning_effort: metadata.guardian_reasoning_effort,
had_prior_review_context: metadata.had_prior_review_context,
reviewed_action_truncated: metadata.reviewed_action_truncated,
token_usage: metadata.token_usage,
time_to_first_token_ms: metadata.time_to_first_token_ms,
completed_at,
},
);
GuardianAssessment {
risk_level: GuardianRiskLevel::High,
user_authorization: GuardianUserAuthorization::Unknown,
outcome: GuardianAssessmentOutcome::Deny,
rationale,
}
}
GuardianReviewOutcome::TimedOut(metadata) => {
let metadata = guardian_review_metadata_fields(metadata);
let rationale =
"Automatic approval review timed out while evaluating the requested approval."
.to_string(),
},
GuardianReviewOutcome::Aborted => {
.to_string();
analytics_context.track(
session.as_ref(),
turn.as_ref(),
GuardianReviewAnalyticsTerminal {
decision: GuardianReviewDecision::Denied,
terminal_status: GuardianReviewTerminalStatus::TimedOut,
failure_reason: Some(GuardianReviewFailureReason::Timeout),
risk_level: None,
user_authorization: None,
outcome: None,
rationale: None,
guardian_thread_id: metadata.guardian_thread_id,
guardian_session_kind: metadata.guardian_session_kind,
guardian_model: metadata.guardian_model,
guardian_reasoning_effort: metadata.guardian_reasoning_effort,
had_prior_review_context: metadata.had_prior_review_context,
reviewed_action_truncated: metadata.reviewed_action_truncated,
token_usage: metadata.token_usage,
time_to_first_token_ms: metadata.time_to_first_token_ms,
completed_at,
},
);
GuardianAssessment {
risk_level: GuardianRiskLevel::High,
user_authorization: GuardianUserAuthorization::Unknown,
outcome: GuardianAssessmentOutcome::Deny,
rationale,
}
}
GuardianReviewOutcome::Aborted(metadata) => {
let metadata = guardian_review_metadata_fields(metadata);
analytics_context.track(
session.as_ref(),
turn.as_ref(),
GuardianReviewAnalyticsTerminal {
decision: GuardianReviewDecision::Aborted,
terminal_status: GuardianReviewTerminalStatus::Aborted,
failure_reason: Some(GuardianReviewFailureReason::Cancelled),
risk_level: None,
user_authorization: None,
outcome: None,
rationale: None,
guardian_thread_id: metadata.guardian_thread_id,
guardian_session_kind: metadata.guardian_session_kind,
guardian_model: metadata.guardian_model,
guardian_reasoning_effort: metadata.guardian_reasoning_effort,
had_prior_review_context: metadata.had_prior_review_context,
reviewed_action_truncated: metadata.reviewed_action_truncated,
token_usage: metadata.token_usage,
time_to_first_token_ms: metadata.time_to_first_token_ms,
completed_at,
},
);
session
.send_event(
turn.as_ref(),
@@ -276,11 +709,34 @@ pub(crate) async fn review_approval_request(
review_id,
request,
retry_reason,
GuardianApprovalRequestSource::MainTurn,
/*external_cancel*/ None,
)
.await
}
pub(crate) async fn review_approval_request_with_source_and_cancel(
session: &Arc<Session>,
turn: &Arc<TurnContext>,
review_id: String,
request: GuardianApprovalRequest,
retry_reason: Option<String>,
approval_request_source: GuardianApprovalRequestSource,
cancel_token: CancellationToken,
) -> ReviewDecision {
run_guardian_review(
Arc::clone(session),
Arc::clone(turn),
review_id,
request,
retry_reason,
approval_request_source,
Some(cancel_token),
)
.await
}
#[cfg(test)]
pub(crate) async fn review_approval_request_with_cancel(
session: &Arc<Session>,
turn: &Arc<TurnContext>,
@@ -295,6 +751,7 @@ pub(crate) async fn review_approval_request_with_cancel(
review_id,
request,
retry_reason,
GuardianApprovalRequestSource::MainTurn,
Some(cancel_token),
)
.await
@@ -325,7 +782,12 @@ pub(super) async fn run_guardian_review_session(
let live_network_config = match session.services.network_proxy.as_ref() {
Some(network_proxy) => match network_proxy.proxy().current_cfg().await {
Ok(config) => Some(config),
Err(err) => return GuardianReviewOutcome::Completed(Err(err)),
Err(err) => {
return GuardianReviewOutcome::Failed(
GuardianReviewFailure::PromptBuild(err),
None,
);
}
},
None => None,
};
@@ -375,10 +837,12 @@ pub(super) async fn run_guardian_review_session(
);
let guardian_config = match guardian_config {
Ok(config) => config,
Err(err) => return GuardianReviewOutcome::Completed(Err(err)),
Err(err) => {
return GuardianReviewOutcome::Failed(GuardianReviewFailure::PromptBuild(err), None);
}
};
match session
let (session_outcome, session_metadata) = session
.guardian_review_session
.run_review(GuardianReviewSessionParams {
parent_session: Arc::clone(&session),
@@ -393,17 +857,63 @@ pub(super) async fn run_guardian_review_session(
personality: turn.personality,
external_cancel,
})
.await
{
GuardianReviewSessionOutcome::Completed(Ok(last_agent_message)) => {
GuardianReviewOutcome::Completed(parse_guardian_assessment(
last_agent_message.as_deref(),
))
}
.await;
match session_outcome {
GuardianReviewSessionOutcome::Completed(Ok(last_agent_message)) => match last_agent_message
{
Some(last_agent_message) => {
match parse_guardian_assessment(Some(&last_agent_message)) {
Ok(assessment) => {
GuardianReviewOutcome::Completed(Ok(assessment), session_metadata)
}
Err(err) => GuardianReviewOutcome::Failed(
GuardianReviewFailure::Parse(err),
session_metadata,
),
}
}
None => GuardianReviewOutcome::Failed(
GuardianReviewFailure::Session(anyhow::anyhow!(
"guardian review completed without an assessment payload"
)),
session_metadata,
),
},
GuardianReviewSessionOutcome::Completed(Err(err)) => {
GuardianReviewOutcome::Completed(Err(err))
GuardianReviewOutcome::Failed(GuardianReviewFailure::Session(err), session_metadata)
}
GuardianReviewSessionOutcome::TimedOut => GuardianReviewOutcome::TimedOut,
GuardianReviewSessionOutcome::Aborted => GuardianReviewOutcome::Aborted,
GuardianReviewSessionOutcome::PromptBuildFailed(err) => {
GuardianReviewOutcome::Failed(GuardianReviewFailure::PromptBuild(err), session_metadata)
}
GuardianReviewSessionOutcome::TimedOut => GuardianReviewOutcome::TimedOut(session_metadata),
GuardianReviewSessionOutcome::Aborted => GuardianReviewOutcome::Aborted(session_metadata),
}
}
#[cfg(test)]
mod review_tests {
use super::*;
#[test]
fn guardian_review_failure_reason_distinguishes_failure_kinds() {
let parse_failure = GuardianReviewFailure::Parse(anyhow::anyhow!("bad guardian JSON"));
let prompt_failure =
GuardianReviewFailure::PromptBuild(anyhow::anyhow!("bad prompt/config"));
let session_failure =
GuardianReviewFailure::Session(anyhow::anyhow!("guardian runtime failed"));
assert!(matches!(
parse_failure.reason(),
GuardianReviewFailureReason::ParseError
));
assert!(matches!(
prompt_failure.reason(),
GuardianReviewFailureReason::PromptBuildError
));
assert!(matches!(
session_failure.reason(),
GuardianReviewFailureReason::SessionError
));
}
}