Skip to main content

mj_controller/
github_item_verdict.rs

1//! Controller-side Jev client for GitHub item interest and ownership.
2
3use anyhow::{Context, Result};
4use mj_core::github_item::{GithubItemEvidence, GithubItemVerdict, MAX_BODY_BYTES};
5use serde_json::json;
6
7const DIRECT_ENDPOINT: &str = "https://api.typesafe.ai/v1/systemone";
8const HOSTED_ENDPOINT: &str =
9    "https://mj-jev-proxy.eng-admin-a63.workers.dev/v1/github-item-verdict";
10
11pub async fn classify(evidence: &GithubItemEvidence) -> Result<GithubItemVerdict> {
12    let evidence = evidence.bounded_for_request()?;
13    let key = tokio::task::spawn_blocking(mj_core::activity::verdict::api_key)
14        .await
15        .context("resolve Jev key")?;
16    let (endpoint, source, request_body) = if key.is_some() {
17        (
18            DIRECT_ENDPOINT,
19            "direct",
20            serde_json::to_vec(&evidence.upstream_body())?,
21        )
22    } else {
23        (HOSTED_ENDPOINT, "hosted", serde_json::to_vec(&evidence)?)
24    };
25
26    let attempt = match mj_core::jev::DecisionLog::open(mj_core::jev::controller_log_dir()) {
27        Ok(log) => Some(log.start(
28            &format!("github:{}#{}", evidence.item.repo, evidence.item.number),
29            "github-item-verdict",
30            "Is this GitHub item relevant to this session, and did this agent create it?",
31            &format!(
32                "{} #{}; recent session turns",
33                evidence.item.repo, evidence.item.number
34            ),
35        )),
36        Err(error) => {
37            tracing::warn!(%error, "Jev diagnostic log unavailable for GitHub item classification");
38            None
39        }
40    };
41    if let Some(attempt) = &attempt {
42        let request = serde_json::from_slice::<serde_json::Value>(&request_body)?;
43        attempt.update(
44            None,
45            json!({
46                "request": request,
47                "source": source,
48                "contract": "github-item-verdict-v1",
49                "questions": mj_core::github_item::questions(),
50                "model": "jev-latest"
51            }),
52        );
53    }
54
55    let response = crate::jev_transport::post_bounded_json(
56        endpoint,
57        key.as_deref(),
58        request_body,
59        MAX_BODY_BYTES,
60        "request GitHub item verdict",
61    )
62    .await;
63    let result = response.and_then(|body| GithubItemVerdict::parse(&body));
64    if let Some(attempt) = attempt {
65        match &result {
66            Ok(verdict) => {
67                let answer = format!(
68                    "Interested: {}; created: {}.",
69                    verdict.interested, verdict.created
70                );
71                attempt.update(Some(&answer), json!({"answer": answer}));
72                attempt.finish(
73                    "classified",
74                    "The GitHub item was classified for this session.",
75                );
76            }
77            Err(error) => {
78                attempt.update(
79                    Some("Jev did not return a usable GitHub item verdict."),
80                    json!({"error": format!("{error:#}")}),
81                );
82                attempt.finish(
83                    "failed",
84                    "GitHub item classification failed; no interest or watch decision was applied.",
85                );
86            }
87        }
88    }
89    result
90}