Skip to main content

jamjet_worker/executors/
agent_discovery.rs

1//! Executor for `AgentDiscovery` workflow nodes (F2.4).
2//!
3//! Fetches a remote Agent Card from `/.well-known/agent.json` and returns
4//! the discovered capabilities as the node output so the workflow can select
5//! an agent and delegate to it dynamically.
6
7use crate::executor::{ExecutionResult, NodeExecutor};
8use async_trait::async_trait;
9use jamjet_state::backend::WorkItem;
10use serde_json::{json, Value};
11use tracing::{debug, instrument};
12
13pub struct AgentDiscoveryExecutor;
14
15#[async_trait]
16impl NodeExecutor for AgentDiscoveryExecutor {
17    #[instrument(skip(self, item), fields(node_id = %item.node_id))]
18    async fn execute(&self, item: &WorkItem) -> Result<ExecutionResult, String> {
19        let start = std::time::Instant::now();
20
21        let agent_url = item
22            .payload
23            .get("agent_url")
24            .and_then(|v| v.as_str())
25            .ok_or("AgentDiscovery: missing 'agent_url' in payload")?;
26
27        debug!(agent_url = %agent_url, "Discovering agent");
28
29        // Handle did: URLs: resolve did:web to HTTPS URL first (I2.4).
30        let resolved_base = if agent_url.starts_with("did:web:") {
31            let rest = agent_url.trim_start_matches("did:web:");
32            let parts: Vec<&str> = rest.splitn(2, ':').collect();
33            let host = parts[0];
34            let path = if parts.len() > 1 {
35                format!("/{}", parts[1].replace(':', "/"))
36            } else {
37                String::new()
38            };
39            format!("https://{host}{path}")
40        } else {
41            agent_url.trim_end_matches('/').to_string()
42        };
43
44        let card_url = format!("{resolved_base}/.well-known/agent.json");
45        let client = reqwest::Client::builder()
46            .timeout(std::time::Duration::from_secs(10))
47            .build()
48            .map_err(|e| format!("HTTP client: {e}"))?;
49
50        let card: Value = client
51            .get(&card_url)
52            .send()
53            .await
54            .map_err(|e| format!("fetch Agent Card from {card_url}: {e}"))?
55            .json()
56            .await
57            .map_err(|e| format!("parse Agent Card: {e}"))?;
58
59        let duration_ms = start.elapsed().as_millis() as u64;
60
61        Ok(ExecutionResult {
62            output: card.clone(),
63            state_patch: json!({ "discovered_agent": card }),
64            duration_ms,
65            gen_ai_system: None,
66            gen_ai_model: None,
67            input_tokens: None,
68            output_tokens: None,
69            finish_reason: None,
70        })
71    }
72}