aion_server/assistant/
install.rs1use aion::Engine;
26
27use super::document::{EmbeddedAssistant, embedded_assistant};
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum AssistantInstall {
32 Installed {
35 workflow_type: String,
37 content_hash: String,
39 },
40 AlreadyCurrent {
42 workflow_type: String,
44 content_hash: String,
46 },
47 Deferred {
50 workflow_type: String,
52 embedded_hash: String,
54 routed_hash: Option<String>,
56 },
57 Failed {
60 reason: String,
62 },
63}
64
65impl AssistantInstall {
66 #[must_use]
68 pub const fn outcome(&self) -> &'static str {
69 match self {
70 Self::Installed { .. } => "installed",
71 Self::AlreadyCurrent { .. } => "already_current",
72 Self::Deferred { .. } => "deferred",
73 Self::Failed { .. } => "failed",
74 }
75 }
76}
77
78pub async fn install_embedded_assistant(engine: &Engine) -> AssistantInstall {
85 let embedded = match embedded_assistant() {
86 Ok(embedded) => embedded,
87 Err(error) => {
88 let reason = error.to_string();
89 tracing::error!(
90 operation = "assistant.install",
91 outcome = "failed",
92 %reason,
93 "the embedded assistant document could not be prepared; this server has no \
94 built-in assistant"
95 );
96 return AssistantInstall::Failed { reason };
97 }
98 };
99 let outcome = install_verified(engine, embedded).await;
100 log_outcome(&outcome);
101 outcome
102}
103
104pub async fn install_embedded_assistant_for_server(state: &crate::ServerState) -> AssistantInstall {
110 match state.engine() {
111 Ok(engine) => install_embedded_assistant(engine.as_ref()).await,
112 Err(error) => {
113 let reason = format!("the engine is not available: {error}");
114 let outcome = AssistantInstall::Failed { reason };
115 log_outcome(&outcome);
116 outcome
117 }
118 }
119}
120
121async fn install_verified(engine: &Engine, embedded: &EmbeddedAssistant) -> AssistantInstall {
123 let workflow_type = embedded.workflow_type().to_owned();
124 let embedded_hash = embedded.content_hash().to_string();
125
126 let versions = match engine.list_workflow_versions() {
127 Ok(versions) => versions,
128 Err(error) => {
129 return AssistantInstall::Failed {
130 reason: format!("the engine catalog could not be read: {error}"),
131 };
132 }
133 };
134 let resident: Vec<_> = versions
135 .into_iter()
136 .filter(|version| version.workflow_type == workflow_type)
137 .collect();
138
139 if resident.is_empty() {
140 return match engine.load_package(embedded.package().clone()).await {
141 Ok(_) => AssistantInstall::Installed {
142 workflow_type,
143 content_hash: embedded_hash,
144 },
145 Err(error) => AssistantInstall::Failed {
146 reason: format!(
147 "the embedded assistant package `{embedded_hash}` did not load: {error}"
148 ),
149 },
150 };
151 }
152
153 let routed_hash = resident
154 .iter()
155 .find(|version| version.route_active)
156 .map(|version| version.content_hash.to_string());
157 if routed_hash.as_deref() == Some(embedded_hash.as_str()) {
158 return AssistantInstall::AlreadyCurrent {
159 workflow_type,
160 content_hash: embedded_hash,
161 };
162 }
163 AssistantInstall::Deferred {
164 workflow_type,
165 embedded_hash,
166 routed_hash,
167 }
168}
169
170fn log_outcome(outcome: &AssistantInstall) {
173 match outcome {
174 AssistantInstall::Installed {
175 workflow_type,
176 content_hash,
177 } => tracing::info!(
178 operation = "assistant.install",
179 outcome = outcome.outcome(),
180 %workflow_type,
181 %content_hash,
182 "the built-in assistant was installed and routed on a catalog that held no version \
183 of it"
184 ),
185 AssistantInstall::AlreadyCurrent {
186 workflow_type,
187 content_hash,
188 } => tracing::info!(
189 operation = "assistant.install",
190 outcome = outcome.outcome(),
191 %workflow_type,
192 %content_hash,
193 "the built-in assistant is already the routed version"
194 ),
195 AssistantInstall::Deferred {
196 workflow_type,
197 embedded_hash,
198 routed_hash,
199 } => tracing::warn!(
200 operation = "assistant.install",
201 outcome = outcome.outcome(),
202 %workflow_type,
203 %embedded_hash,
204 routed_hash = routed_hash.as_deref().unwrap_or("none"),
208 "the embedded assistant document is not the routed version on this catalog, and \
209 routing was NOT changed — a restart must never move a route an operator chose. \
210 To cut over deliberately: `aion assistant document --output assistant.awl`, then \
211 `aion deploy assistant.awl` (which loads AND routes it), and only then restart \
212 the worker serving its queue"
213 ),
214 AssistantInstall::Failed { reason } => tracing::error!(
215 operation = "assistant.install",
216 outcome = outcome.outcome(),
217 %reason,
218 "the built-in assistant was not installed"
219 ),
220 }
221}
222
223#[cfg(test)]
224#[path = "install_tests.rs"]
225mod install_tests;