gate4agent_wake/
hosted.rs1use crate::{wake_prompt, ProviderKind, ProviderSession, SessionKind, WakeLetter};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct HostedTarget {
12 pub nick: String,
13 pub provider: ProviderKind,
14 pub live: bool,
16 pub spawn_allowed: bool,
18}
19
20pub trait HostedSessions {
22 fn lookup(&self, nick: &str) -> Option<HostedTarget>;
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum HostedWake {
28 Submit { nick: String, prompt: String },
30 Spawn { nick: String, provider: ProviderKind, prompt: String },
32 Refuse(String),
35}
36
37pub fn plan_hosted_wake(sessions: &dyn HostedSessions, nick: &str, letter: &WakeLetter<'_>) -> HostedWake {
39 let Some(target) = sessions.lookup(nick) else {
40 return HostedWake::Refuse(format!("{nick}: no session of this host"));
41 };
42 let session = ProviderSession {
43 kind: SessionKind::local(target.provider),
44 session_id: String::new(),
45 nick: target.nick.clone(),
46 cwd: None,
47 headless: !target.live,
48 };
49 let prompt = wake_prompt(&session, letter);
50 if target.live {
51 HostedWake::Submit { nick: target.nick, prompt }
52 } else if target.spawn_allowed {
53 HostedWake::Spawn { nick: target.nick, provider: target.provider, prompt }
54 } else {
55 HostedWake::Refuse(format!("{nick}: not live and spawn is off"))
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 struct Fake(Vec<HostedTarget>);
64 impl HostedSessions for Fake {
65 fn lookup(&self, nick: &str) -> Option<HostedTarget> {
66 self.0.iter().find(|t| t.nick == nick).cloned()
67 }
68 }
69 fn t(nick: &str, live: bool, spawn: bool) -> HostedTarget {
70 HostedTarget { nick: nick.into(), provider: ProviderKind::Codex, live, spawn_allowed: spawn }
71 }
72 fn letter() -> WakeLetter<'static> {
73 WakeLetter { body: "ping", from_nick: "hatcheryweb", event_id: "$e1", room: None }
74 }
75
76 #[test]
77 fn live_session_gets_a_framed_turn() {
78 let f = Fake(vec![t("codex-1", true, false)]);
79 match plan_hosted_wake(&f, "codex-1", &letter()) {
80 HostedWake::Submit { nick, prompt } => {
81 assert_eq!(nick, "codex-1");
82 assert!(prompt.ends_with("ping"));
83 assert!(prompt.contains("m4a-send --as codex-1"));
84 }
85 other => panic!("{other:?}"),
86 }
87 }
88
89 #[test]
90 fn offline_session_spawns_only_when_allowed_and_unknown_is_refused() {
91 let f = Fake(vec![t("a", false, true), t("b", false, false)]);
92 assert!(matches!(plan_hosted_wake(&f, "a", &letter()), HostedWake::Spawn { provider: ProviderKind::Codex, .. }));
93 assert!(matches!(plan_hosted_wake(&f, "b", &letter()), HostedWake::Refuse(_)));
94 match plan_hosted_wake(&f, "zzz", &letter()) {
95 HostedWake::Refuse(why) => assert!(!why.contains("ping")),
96 other => panic!("{other:?}"),
97 }
98 }
99}