feat(agent): backend hot-swap profil (A0+A1) + inventaire reprise session (B1) — L15
Cadrage Architecture §15 (figé) : « agent = entité à session persistante ». - A0 (domaine) : Agent::with_profile, LayoutTree::leaf, event AgentProfileChanged (+ DTO miroir DomainEventDto camelCase). - A1 (application) : use case ChangeAgentProfile — no-op si profil identique, mutation manifeste, nettoyage conversation_id/agent_was_running sur layouts persistés, swap à chaud (kill PTY + relance même cellule via composition de LaunchAgent), event AgentProfileChanged. Décision : repartir à neuf (on garde .md + mémoire, on jette l'historique de conversation). - B1 (application) : use case ListResumableAgents (lecture seule) — inventaire des cellules was_running||conversation_id, resume_supported selon profil, best-effort. Aucun nouveau port/adapter (composition de l'existant). Hexagonal strict. Tests : domaine 11 + app-tauri dto + ChangeAgentProfile 9 + ListResumableAgents 8, suite application complète verte (0 régression). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -15,8 +15,8 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, ContextInjectionPlan, EventBus, FileSystem, FsError,
|
||||
IdGenerator, MemoryQuery, MemoryRecall, PreparedContext, ProfileStore, PtyPort, RemotePath,
|
||||
SessionPlan, SkillStore, SpawnSpec, StoreError,
|
||||
IdGenerator, MemoryQuery, MemoryRecall, PreparedContext, ProfileStore, ProjectStore, PtyPort,
|
||||
RemotePath, SessionPlan, SkillStore, SpawnSpec, StoreError,
|
||||
};
|
||||
use domain::{
|
||||
Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile, ContextInjection, DomainEvent,
|
||||
@ -25,6 +25,7 @@ use domain::{
|
||||
};
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::layout::{persist_doc, resolve_doc};
|
||||
use crate::project::project_context_path;
|
||||
use crate::terminal::TerminalSessions;
|
||||
|
||||
@ -269,6 +270,242 @@ impl UpdateAgentContext {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ChangeAgentProfile
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`ChangeAgentProfile::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ChangeAgentProfileInput {
|
||||
/// The owning project.
|
||||
pub project: Project,
|
||||
/// The agent whose runtime profile to hot-swap.
|
||||
pub agent_id: AgentId,
|
||||
/// The new runtime profile.
|
||||
pub profile_id: ProfileId,
|
||||
/// Terminal height in rows for a possible hot relaunch.
|
||||
pub rows: u16,
|
||||
/// Terminal width in columns for a possible hot relaunch.
|
||||
pub cols: u16,
|
||||
}
|
||||
|
||||
/// Output of [`ChangeAgentProfile::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ChangeAgentProfileOutput {
|
||||
/// The mutated agent (now carrying the new profile).
|
||||
pub agent: Agent,
|
||||
/// The freshly relaunched session, when a live session was hot-swapped.
|
||||
pub relaunched: Option<TerminalSession>,
|
||||
}
|
||||
|
||||
/// Hot-swaps an existing agent's runtime profile (ARCHITECTURE §15.1).
|
||||
///
|
||||
/// **Single Responsibility**: mutate the profile in the manifest, clear the now
|
||||
/// foreign conversation id on every persisted layout cell hosting the agent, and
|
||||
/// — if the agent is live — kill its PTY and re-sequence the session in the same
|
||||
/// cell with the new engine. The relaunch is **composed**, not duplicated: this
|
||||
/// use case *calls* [`LaunchAgent::execute`] rather than re-implementing the spawn.
|
||||
///
|
||||
/// Ports consumed (ISP — only what is needed): [`AgentContextStore`] (manifest),
|
||||
/// [`ProfileStore`] (validate the target profile), [`ProjectStore`] +
|
||||
/// [`FileSystem`] (clean the conversation id on persisted layouts, exactly like
|
||||
/// [`crate::layout::SnapshotRunningAgents`]), [`TerminalSessions`] + [`PtyPort`]
|
||||
/// (detect/kill a live session), an [`Arc<LaunchAgent>`] for the hot relaunch, and
|
||||
/// [`EventBus`] to publish.
|
||||
pub struct ChangeAgentProfile {
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
profiles: Arc<dyn ProfileStore>,
|
||||
projects: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
sessions: Arc<TerminalSessions>,
|
||||
pty: Arc<dyn PtyPort>,
|
||||
launch: Arc<LaunchAgent>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl ChangeAgentProfile {
|
||||
/// Builds the use case from its injected ports (and the composed launcher).
|
||||
#[must_use]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
profiles: Arc<dyn ProfileStore>,
|
||||
projects: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
sessions: Arc<TerminalSessions>,
|
||||
pty: Arc<dyn PtyPort>,
|
||||
launch: Arc<LaunchAgent>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
Self {
|
||||
contexts,
|
||||
profiles,
|
||||
projects,
|
||||
fs,
|
||||
sessions,
|
||||
pty,
|
||||
launch,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the hot-swap, following the 7-step algorithm of §15.1.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::NotFound`] if the agent or the target profile is unknown,
|
||||
/// - [`AppError::Invalid`] on a manifest/layout invariant violation,
|
||||
/// - [`AppError::Store`] / [`AppError::FileSystem`] / [`AppError::Process`] on
|
||||
/// the respective port failures (manifest, layouts, PTY kill, relaunch).
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: ChangeAgentProfileInput,
|
||||
) -> Result<ChangeAgentProfileOutput, AppError> {
|
||||
// 1. Load the manifest and resolve the agent's entry (NotFound otherwise).
|
||||
let manifest = self.contexts.load_manifest(&input.project).await?;
|
||||
let entry = manifest
|
||||
.entries
|
||||
.iter()
|
||||
.find(|e| e.agent_id == input.agent_id)
|
||||
.ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;
|
||||
|
||||
// 2. Same profile ⇒ no-op: return the agent unchanged, no kill/relaunch,
|
||||
// no event.
|
||||
if entry.profile_id == input.profile_id {
|
||||
let agent = entry
|
||||
.to_agent()
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
return Ok(ChangeAgentProfileOutput {
|
||||
agent,
|
||||
relaunched: None,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Validate that the target profile is a known one (ProfileStore.list).
|
||||
let known = self
|
||||
.profiles
|
||||
.list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.any(|p| p.id == input.profile_id);
|
||||
if !known {
|
||||
return Err(AppError::NotFound(format!("profile {}", input.profile_id)));
|
||||
}
|
||||
|
||||
// 4. Mutate the entry (new profile), re-validate (to_agent + manifest)
|
||||
// and persist.
|
||||
let mut entries = manifest.entries;
|
||||
let mut mutated_agent = None;
|
||||
for e in &mut entries {
|
||||
if e.agent_id == input.agent_id {
|
||||
e.profile_id = input.profile_id;
|
||||
let agent = e
|
||||
.to_agent()
|
||||
.map_err(|err| AppError::Invalid(err.to_string()))?;
|
||||
mutated_agent = Some(agent);
|
||||
}
|
||||
}
|
||||
let agent = mutated_agent
|
||||
.ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;
|
||||
let manifest = AgentManifest::new(manifest.version, entries)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
self.contexts
|
||||
.save_manifest(&input.project, &manifest)
|
||||
.await?;
|
||||
|
||||
// 5. Clean the conversation on every persisted layout: for each leaf
|
||||
// hosting this agent, drop the (now foreign) conversation id and reset
|
||||
// the running flag. Mirrors `SnapshotRunningAgents`:
|
||||
// resolve_doc → walk agent_leaves → mutate → persist_doc (if changed).
|
||||
self.clean_conversation(&input.project, &input.agent_id)
|
||||
.await?;
|
||||
|
||||
// 6. A live session? Kill its PTY then relaunch in the same cell with the
|
||||
// new profile and a discarded conversation id.
|
||||
let relaunched = self.relaunch_if_live(&input).await?;
|
||||
|
||||
// 7. Publish the profile change and return.
|
||||
self.events.publish(DomainEvent::AgentProfileChanged {
|
||||
agent_id: input.agent_id,
|
||||
profile_id: input.profile_id,
|
||||
});
|
||||
|
||||
Ok(ChangeAgentProfileOutput { agent, relaunched })
|
||||
}
|
||||
|
||||
/// Clears the conversation id and resets the running flag on every persisted
|
||||
/// layout leaf hosting `agent_id` (step 5). Persists only when something
|
||||
/// actually changed (a project with no such leaf is a no-op write).
|
||||
async fn clean_conversation(
|
||||
&self,
|
||||
project: &Project,
|
||||
agent_id: &AgentId,
|
||||
) -> Result<(), AppError> {
|
||||
let project = self.projects.load_project(project.id).await?;
|
||||
let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;
|
||||
let mut changed = false;
|
||||
|
||||
for named in &mut doc.layouts {
|
||||
for (leaf_id, leaf_agent) in named.tree.agent_leaves() {
|
||||
if &leaf_agent != agent_id {
|
||||
continue;
|
||||
}
|
||||
// Pure ops — only NodeNotFound is possible, which cannot happen
|
||||
// since `leaf_id` came from this very tree.
|
||||
named.tree = named
|
||||
.tree
|
||||
.set_cell_conversation(leaf_id, None)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
named.tree = named
|
||||
.tree
|
||||
.set_agent_running(leaf_id, false)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
persist_doc(self.fs.as_ref(), &project, &doc).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Kills the agent's live PTY (if any) and relaunches it in the same cell with
|
||||
/// the new profile (step 6), composing [`LaunchAgent::execute`]. Returns the
|
||||
/// relaunched session, or `None` when the agent had no live session.
|
||||
async fn relaunch_if_live(
|
||||
&self,
|
||||
input: &ChangeAgentProfileInput,
|
||||
) -> Result<Option<TerminalSession>, AppError> {
|
||||
let Some(session_id) = self.sessions.session_for_agent(&input.agent_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
// The hosting cell of the live session — the relaunch reopens here.
|
||||
let node_id = self.sessions.node_for_agent(&input.agent_id);
|
||||
|
||||
// Kill the PTY: remove from the registry first (so the relaunch's
|
||||
// one-live-session-per-agent guard sees no live session), then kill the
|
||||
// process.
|
||||
if let Some(handle) = self.sessions.remove(&session_id) {
|
||||
self.pty.kill(&handle).await?;
|
||||
}
|
||||
|
||||
let output = self
|
||||
.launch
|
||||
.execute(LaunchAgentInput {
|
||||
project: input.project.clone(),
|
||||
agent_id: input.agent_id,
|
||||
rows: input.rows,
|
||||
cols: input.cols,
|
||||
node_id,
|
||||
// Conversation id discarded: the previous one belonged to the old
|
||||
// engine; the new profile starts (or assigns) a fresh one.
|
||||
conversation_id: None,
|
||||
})
|
||||
.await?;
|
||||
Ok(Some(output.session))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DeleteAgent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -9,17 +9,22 @@
|
||||
mod catalogue;
|
||||
mod inspect;
|
||||
mod lifecycle;
|
||||
mod resume;
|
||||
mod usecases;
|
||||
|
||||
pub(crate) use lifecycle::unique_md_path;
|
||||
|
||||
pub use catalogue::{reference_profile_id, reference_profiles};
|
||||
pub use inspect::{InspectConversation, InspectConversationInput, InspectConversationOutput};
|
||||
pub use resume::{
|
||||
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,
|
||||
};
|
||||
pub use lifecycle::{
|
||||
CreateAgentFromScratch, CreateAgentInput, CreateAgentOutput, DeleteAgent, DeleteAgentInput,
|
||||
LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput,
|
||||
ListAgentsOutput, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
||||
UpdateAgentContext, UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET,
|
||||
ChangeAgentProfile, ChangeAgentProfileInput, ChangeAgentProfileOutput, CreateAgentFromScratch,
|
||||
CreateAgentInput, CreateAgentOutput, DeleteAgent, DeleteAgentInput, LaunchAgent,
|
||||
LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput,
|
||||
ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput, UpdateAgentContext,
|
||||
UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET,
|
||||
};
|
||||
pub use usecases::{
|
||||
ConfigureProfiles, ConfigureProfilesInput, ConfigureProfilesOutput, DeleteProfile,
|
||||
|
||||
185
crates/application/src/agent/resume.rs
Normal file
185
crates/application/src/agent/resume.rs
Normal file
@ -0,0 +1,185 @@
|
||||
//! [`ListResumableAgents`] — inventaire, en lecture seule, des cellules d'agent
|
||||
//! reprenables à la réouverture d'un projet (ARCHITECTURE §15.2, chantier B,
|
||||
//! lot B1).
|
||||
//!
|
||||
//! À la réouverture d'un projet, [`crate::OpenProject`] a déjà rechargé le
|
||||
//! manifeste et les layouts persistés : chaque [`domain::LeafCell`] retrouve
|
||||
//! donc son `conversation_id` et son `agent_was_running` gelés à la fermeture.
|
||||
//! Ce use case **calcule l'inventaire** des cellules d'agent reprenables pour
|
||||
//! que la couche supérieure (commande Tauri + `ResumeProjectPanel`, lot B2)
|
||||
//! puisse proposer un panneau de reprise groupé.
|
||||
//!
|
||||
//! Contraintes (§15.2) :
|
||||
//! - **Lecture seule** : aucun PTY, aucun spawn, aucune persistance. On
|
||||
//! compose `resolve_doc` (lecture des layouts), le manifeste (nom +
|
||||
//! `profile_id`) et la liste des profils (`resume_supported`).
|
||||
//! - **Best-effort, jamais d'erreur** : projet / mémoire / agent absent ⇒
|
||||
//! **liste vide**, jamais de panique ni d'`AppError`. C'est un inventaire
|
||||
//! indicatif à l'ouverture, pas une opération critique.
|
||||
//! - **Filtre** : on ne retient qu'une leaf d'agent dont `agent_was_running`
|
||||
//! est vrai **ou** qui porte un `conversation_id`. Une cellule d'agent jamais
|
||||
//! lancée (ni id, ni flag) n'apparaît pas : elle se lancera normalement au
|
||||
//! clic, sans popup.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{AgentContextStore, FileSystem, ProfileStore, ProjectStore};
|
||||
use domain::{AgentId, NodeId, Project};
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::layout::resolve_doc;
|
||||
|
||||
/// Input de [`ListResumableAgents::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListResumableAgentsInput {
|
||||
/// Le projet dont on inventorie les cellules reprenables.
|
||||
pub project: Project,
|
||||
}
|
||||
|
||||
/// Une cellule d'agent reprenable, telle qu'exposée à la couche supérieure.
|
||||
///
|
||||
/// Champs alignés sur la spec §15.2 : l'identité de l'agent et de sa cellule
|
||||
/// hôte, l'id de conversation à reprendre (`None` ⇒ relance à neuf), l'état
|
||||
/// « tournait » gelé à la fermeture, et si son profil sait reprendre une
|
||||
/// conversation CLI.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ResumableAgent {
|
||||
/// Identifiant de l'agent.
|
||||
pub agent_id: AgentId,
|
||||
/// Nom d'affichage de l'agent (résolu via le manifeste).
|
||||
pub name: String,
|
||||
/// Cellule hôte où relancer/reprendre l'agent.
|
||||
pub node_id: NodeId,
|
||||
/// Id de conversation CLI persistant porté par la cellule. `None` ⇒ relance
|
||||
/// à neuf (pas d'historique à reprendre).
|
||||
pub conversation_id: Option<String>,
|
||||
/// Valeur de `agent_was_running` gelée à la fermeture de la cellule.
|
||||
pub was_running: bool,
|
||||
/// `true` si le profil de l'agent possède une [`domain::SessionStrategy`]
|
||||
/// exploitable (présence d'un `resume_flag`).
|
||||
pub resume_supported: bool,
|
||||
}
|
||||
|
||||
/// Output de [`ListResumableAgents::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct ListResumableAgentsOutput {
|
||||
/// Les cellules d'agent reprenables, dans l'ordre de parcours des layouts.
|
||||
pub resumable: Vec<ResumableAgent>,
|
||||
}
|
||||
|
||||
/// Inventorie, en lecture seule, les cellules d'agent reprenables d'un projet.
|
||||
///
|
||||
/// Compose trois ports déjà injectés au composition root, **sans** en ajouter
|
||||
/// de nouveau :
|
||||
/// - [`ProjectStore`] + [`FileSystem`] : charger les layouts persistés
|
||||
/// (`resolve_doc`),
|
||||
/// - [`AgentContextStore`] : le manifeste, pour le nom et le `profile_id` de
|
||||
/// chaque agent,
|
||||
/// - [`ProfileStore`] : pour déterminer `resume_supported`.
|
||||
pub struct ListResumableAgents {
|
||||
#[allow(dead_code)]
|
||||
store: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
profiles: Arc<dyn ProfileStore>,
|
||||
}
|
||||
|
||||
impl ListResumableAgents {
|
||||
/// Construit le use case à partir de ses ports injectés.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
store: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
profiles: Arc<dyn ProfileStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
fs,
|
||||
contexts,
|
||||
profiles,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calcule l'inventaire des cellules reprenables.
|
||||
///
|
||||
/// Parcourt chaque layout du projet et, pour chaque leaf portant un agent,
|
||||
/// lit `conversation_id`/`agent_was_running` (via l'accessor pur
|
||||
/// [`domain::LayoutTree::leaf`]). Ne retient que les leaves passant le
|
||||
/// filtre `was_running || conversation_id.is_some()`, résout le nom via le
|
||||
/// manifeste et `resume_supported` via le profil.
|
||||
///
|
||||
/// **Best-effort** : toute défaillance de lecture (projet/mémoire absent,
|
||||
/// layouts illisibles, manifeste/profils en erreur) dégrade vers une
|
||||
/// **liste vide** ; cette fonction ne renvoie donc jamais d'erreur, mais sa
|
||||
/// signature reste `Result` pour rester homogène avec les autres use cases.
|
||||
///
|
||||
/// # Errors
|
||||
/// N'échoue jamais en pratique (best-effort) ; la signature `Result` est
|
||||
/// conservée par cohérence.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: ListResumableAgentsInput,
|
||||
) -> Result<ListResumableAgentsOutput, AppError> {
|
||||
// Layouts persistés. Toute erreur ⇒ inventaire vide (best-effort).
|
||||
let Ok(doc) = resolve_doc(self.fs.as_ref(), &input.project).await else {
|
||||
return Ok(ListResumableAgentsOutput::default());
|
||||
};
|
||||
|
||||
// Manifeste (nom + profile_id). Absent/illisible ⇒ inventaire vide :
|
||||
// sans manifeste on ne peut résoudre ni nom ni profil.
|
||||
let Ok(manifest) = self.contexts.load_manifest(&input.project).await else {
|
||||
return Ok(ListResumableAgentsOutput::default());
|
||||
};
|
||||
|
||||
// Profils disponibles, pour `resume_supported`. Indisponibles ⇒ on
|
||||
// considère qu'aucun profil ne sait reprendre (best-effort), sans
|
||||
// pour autant masquer les agents reprenables par `conversation_id`.
|
||||
let profiles = self.profiles.list().await.unwrap_or_default();
|
||||
|
||||
let mut resumable = Vec::new();
|
||||
|
||||
for named in &doc.layouts {
|
||||
for (node_id, agent_id) in named.tree.agent_leaves() {
|
||||
// Lecture pure de la cellule hôte pour ses champs de reprise.
|
||||
let Some(leaf) = named.tree.leaf(node_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let conversation_id = leaf.conversation_id.clone();
|
||||
let was_running = leaf.agent_was_running;
|
||||
|
||||
// Filtre §15.2 : reprenable au sens strict uniquement.
|
||||
if !was_running && conversation_id.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Nom via le manifeste ; agent absent ⇒ on ignore la leaf
|
||||
// (best-effort : pas d'entrée orpheline dans l'inventaire).
|
||||
let Some(entry) = manifest.entries.iter().find(|e| e.agent_id == agent_id) else {
|
||||
continue;
|
||||
};
|
||||
let name = entry.name.clone();
|
||||
|
||||
// `resume_supported` : le profil de l'agent porte-t-il une
|
||||
// SessionStrategy exploitable (resume_flag présent) ?
|
||||
let resume_supported = entry
|
||||
.to_agent()
|
||||
.ok()
|
||||
.and_then(|agent| profiles.iter().find(|p| p.id == agent.profile_id))
|
||||
.is_some_and(|profile| profile.session.is_some());
|
||||
|
||||
resumable.push(ResumableAgent {
|
||||
agent_id,
|
||||
name,
|
||||
node_id,
|
||||
conversation_id,
|
||||
was_running,
|
||||
resume_supported,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ListResumableAgentsOutput { resumable })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user