feat(security): add approval modes with CLI controls
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
# Agents Upgrade Plan
|
# Agents Upgrade Plan
|
||||||
|
|
||||||
All tracked items completed.
|
- feat: implement resumable command queue, automatic thought summaries, and queued execution controls to match Codex CLI session management and Claude Code’s scripted workflows
|
||||||
|
- feat: add first-class prompt, agent, and sub-agent configuration via `.owlen/agents` plus reusable prompt libraries, mirroring Codex custom prompts and Claude’s configurable agents
|
||||||
|
- feat: deliver official VS Code extension and browser workspace so Owlen runs alongside Codex’s IDE plugin and Claude Code’s app-based surfaces
|
||||||
|
- feat: support multimodal inputs (images, rich artifacts) and preview panes so non-text context matches Codex CLI image handling and Claude Code’s artifact outputs
|
||||||
|
- feat: integrate repository automation (GitHub PR review, commit templating, Claude SDK-style automation APIs) to reach parity with Codex CLI’s GitHub integration and Claude Code’s CLI/SDK automation
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
|
|
||||||
pub mod cloud;
|
pub mod cloud;
|
||||||
pub mod providers;
|
pub mod providers;
|
||||||
|
pub mod security;
|
||||||
pub mod tools;
|
pub mod tools;
|
||||||
|
|||||||
61
crates/owlen-cli/src/commands/security.rs
Normal file
61
crates/owlen-cli/src/commands/security.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Subcommand, ValueEnum};
|
||||||
|
use owlen_core::config::ApprovalMode;
|
||||||
|
use owlen_tui::config as tui_config;
|
||||||
|
|
||||||
|
/// Security-related configuration commands.
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum SecurityCommand {
|
||||||
|
/// Display the current approval mode.
|
||||||
|
Show,
|
||||||
|
/// Set the approval mode (auto, read-only, plan-first).
|
||||||
|
Approval {
|
||||||
|
/// Approval policy to apply to new sessions.
|
||||||
|
#[arg(value_enum)]
|
||||||
|
mode: ApprovalModeArg,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||||
|
pub enum ApprovalModeArg {
|
||||||
|
Auto,
|
||||||
|
#[clap(name = "read-only")]
|
||||||
|
ReadOnly,
|
||||||
|
#[clap(name = "plan-first")]
|
||||||
|
PlanFirst,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ApprovalModeArg> for ApprovalMode {
|
||||||
|
fn from(value: ApprovalModeArg) -> Self {
|
||||||
|
match value {
|
||||||
|
ApprovalModeArg::Auto => ApprovalMode::Auto,
|
||||||
|
ApprovalModeArg::ReadOnly => ApprovalMode::ReadOnly,
|
||||||
|
ApprovalModeArg::PlanFirst => ApprovalMode::PlanFirst,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_security_command(command: SecurityCommand) -> Result<()> {
|
||||||
|
match command {
|
||||||
|
SecurityCommand::Show => show_approval_mode(),
|
||||||
|
SecurityCommand::Approval { mode } => set_approval_mode(mode.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_approval_mode() -> Result<()> {
|
||||||
|
let config = tui_config::try_load_config().unwrap_or_default();
|
||||||
|
println!(
|
||||||
|
"Current approval mode: {}",
|
||||||
|
config.security.approval_mode.as_str()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_approval_mode(mode: ApprovalMode) -> Result<()> {
|
||||||
|
let mut config = tui_config::try_load_config().unwrap_or_default();
|
||||||
|
config.security.approval_mode = mode;
|
||||||
|
config.validate()?;
|
||||||
|
tui_config::save_config(&config)?;
|
||||||
|
println!("Set approval mode to {}.", mode.as_str());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ use clap::{Parser, Subcommand};
|
|||||||
use commands::{
|
use commands::{
|
||||||
cloud::{CloudCommand, run_cloud_command},
|
cloud::{CloudCommand, run_cloud_command},
|
||||||
providers::{ModelsArgs, ProvidersCommand, run_models_command, run_providers_command},
|
providers::{ModelsArgs, ProvidersCommand, run_models_command, run_providers_command},
|
||||||
|
security::{SecurityCommand, run_security_command},
|
||||||
tools::{ToolsCommand, run_tools_command},
|
tools::{ToolsCommand, run_tools_command},
|
||||||
};
|
};
|
||||||
use mcp::{McpCommand, run_mcp_command};
|
use mcp::{McpCommand, run_mcp_command};
|
||||||
@@ -60,6 +61,9 @@ enum OwlenCommand {
|
|||||||
/// Manage MCP tool presets
|
/// Manage MCP tool presets
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
Tools(ToolsCommand),
|
Tools(ToolsCommand),
|
||||||
|
/// Configure security and approval policies
|
||||||
|
#[command(subcommand)]
|
||||||
|
Security(SecurityCommand),
|
||||||
/// Show manual steps for updating Owlen to the latest revision
|
/// Show manual steps for updating Owlen to the latest revision
|
||||||
Upgrade,
|
Upgrade,
|
||||||
}
|
}
|
||||||
@@ -86,6 +90,7 @@ async fn run_command(command: OwlenCommand) -> Result<()> {
|
|||||||
OwlenCommand::Models(args) => run_models_command(args).await,
|
OwlenCommand::Models(args) => run_models_command(args).await,
|
||||||
OwlenCommand::Mcp(mcp_cmd) => run_mcp_command(mcp_cmd),
|
OwlenCommand::Mcp(mcp_cmd) => run_mcp_command(mcp_cmd),
|
||||||
OwlenCommand::Tools(tools_cmd) => run_tools_command(tools_cmd),
|
OwlenCommand::Tools(tools_cmd) => run_tools_command(tools_cmd),
|
||||||
|
OwlenCommand::Security(sec_cmd) => run_security_command(sec_cmd),
|
||||||
OwlenCommand::Upgrade => {
|
OwlenCommand::Upgrade => {
|
||||||
println!(
|
println!(
|
||||||
"To update Owlen from source:\n git pull\n cargo install --path crates/owlen-cli --force"
|
"To update Owlen from source:\n git pull\n cargo install --path crates/owlen-cli --force"
|
||||||
|
|||||||
@@ -1735,6 +1735,25 @@ impl Default for PrivacySettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Security settings that constrain tool execution
|
/// Security settings that constrain tool execution
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum ApprovalMode {
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
ReadOnly,
|
||||||
|
PlanFirst,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApprovalMode {
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ApprovalMode::Auto => "auto",
|
||||||
|
ApprovalMode::ReadOnly => "read-only",
|
||||||
|
ApprovalMode::PlanFirst => "plan-first",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SecuritySettings {
|
pub struct SecuritySettings {
|
||||||
#[serde(default = "SecuritySettings::default_enable_sandboxing")]
|
#[serde(default = "SecuritySettings::default_enable_sandboxing")]
|
||||||
@@ -1745,6 +1764,8 @@ pub struct SecuritySettings {
|
|||||||
pub max_memory_mb: u64,
|
pub max_memory_mb: u64,
|
||||||
#[serde(default = "SecuritySettings::default_allowed_tools")]
|
#[serde(default = "SecuritySettings::default_allowed_tools")]
|
||||||
pub allowed_tools: Vec<String>,
|
pub allowed_tools: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub approval_mode: ApprovalMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SecuritySettings {
|
impl SecuritySettings {
|
||||||
@@ -1769,6 +1790,10 @@ impl SecuritySettings {
|
|||||||
"file_delete".to_string(),
|
"file_delete".to_string(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn approval_mode(&self) -> ApprovalMode {
|
||||||
|
self.approval_mode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SecuritySettings {
|
impl Default for SecuritySettings {
|
||||||
@@ -1778,6 +1803,7 @@ impl Default for SecuritySettings {
|
|||||||
sandbox_timeout_seconds: Self::default_timeout(),
|
sandbox_timeout_seconds: Self::default_timeout(),
|
||||||
max_memory_mb: Self::default_max_memory(),
|
max_memory_mb: Self::default_max_memory(),
|
||||||
allowed_tools: Self::default_allowed_tools(),
|
allowed_tools: Self::default_allowed_tools(),
|
||||||
|
approval_mode: ApprovalMode::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::config::{
|
use crate::config::{
|
||||||
ChatSettings, CompressionStrategy, Config, LEGACY_OLLAMA_CLOUD_API_KEY_ENV,
|
ApprovalMode, ChatSettings, CompressionStrategy, Config, LEGACY_OLLAMA_CLOUD_API_KEY_ENV,
|
||||||
LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV, McpResourceConfig, McpServerConfig, OLLAMA_API_KEY_ENV,
|
LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV, McpResourceConfig, McpServerConfig, OLLAMA_API_KEY_ENV,
|
||||||
OLLAMA_CLOUD_BASE_URL,
|
OLLAMA_CLOUD_BASE_URL,
|
||||||
};
|
};
|
||||||
@@ -535,6 +535,9 @@ pub struct SessionController {
|
|||||||
credential_manager: Option<Arc<CredentialManager>>,
|
credential_manager: Option<Arc<CredentialManager>>,
|
||||||
ui: Arc<dyn UiController>,
|
ui: Arc<dyn UiController>,
|
||||||
enable_code_tools: bool,
|
enable_code_tools: bool,
|
||||||
|
approval_mode: ApprovalMode,
|
||||||
|
plan_confirmed: bool,
|
||||||
|
plan_instruction_inserted: bool,
|
||||||
current_mode: Mode,
|
current_mode: Mode,
|
||||||
missing_oauth_servers: Vec<String>,
|
missing_oauth_servers: Vec<String>,
|
||||||
event_tx: Option<UnboundedSender<ControllerEvent>>,
|
event_tx: Option<UnboundedSender<ControllerEvent>>,
|
||||||
@@ -565,6 +568,12 @@ async fn build_tools(
|
|||||||
|
|
||||||
let active_provider_id = config_guard.general.default_provider.clone();
|
let active_provider_id = config_guard.general.default_provider.clone();
|
||||||
|
|
||||||
|
let approval_mode = config_guard.security.approval_mode;
|
||||||
|
let read_only = matches!(approval_mode, ApprovalMode::ReadOnly);
|
||||||
|
if read_only {
|
||||||
|
debug!("Approval mode 'read-only' active: suppressing write/delete/code-exec tools.");
|
||||||
|
}
|
||||||
|
|
||||||
let web_search_settings = if config_guard
|
let web_search_settings = if config_guard
|
||||||
.security
|
.security
|
||||||
.allowed_tools
|
.allowed_tools
|
||||||
@@ -603,6 +612,7 @@ async fn build_tools(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if enable_code_tools
|
if enable_code_tools
|
||||||
|
&& !read_only
|
||||||
&& config_guard
|
&& config_guard
|
||||||
.security
|
.security
|
||||||
.allowed_tools
|
.allowed_tools
|
||||||
@@ -622,6 +632,7 @@ async fn build_tools(
|
|||||||
.allowed_tools
|
.allowed_tools
|
||||||
.iter()
|
.iter()
|
||||||
.any(|t| t == "file_write")
|
.any(|t| t == "file_write")
|
||||||
|
&& !read_only
|
||||||
{
|
{
|
||||||
registry.register(ResourcesWriteTool)?;
|
registry.register(ResourcesWriteTool)?;
|
||||||
}
|
}
|
||||||
@@ -630,6 +641,7 @@ async fn build_tools(
|
|||||||
.allowed_tools
|
.allowed_tools
|
||||||
.iter()
|
.iter()
|
||||||
.any(|t| t == "file_delete")
|
.any(|t| t == "file_delete")
|
||||||
|
&& !read_only
|
||||||
{
|
{
|
||||||
registry.register(ResourcesDeleteTool)?;
|
registry.register(ResourcesDeleteTool)?;
|
||||||
}
|
}
|
||||||
@@ -798,6 +810,17 @@ impl SessionController {
|
|||||||
Arc::new(Mutex::new(ConsentManager::new()))
|
Arc::new(Mutex::new(ConsentManager::new()))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let approval_mode = config_guard.security.approval_mode;
|
||||||
|
let requested_code_tools = enable_code_tools;
|
||||||
|
let enable_code_tools = if matches!(approval_mode, ApprovalMode::ReadOnly) {
|
||||||
|
if requested_code_tools {
|
||||||
|
warn!("Read-only approval mode disables code tools.");
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
requested_code_tools
|
||||||
|
};
|
||||||
|
|
||||||
let conversation = ConversationManager::with_history_capacity(
|
let conversation = ConversationManager::with_history_capacity(
|
||||||
model,
|
model,
|
||||||
config_guard.storage.max_saved_sessions,
|
config_guard.storage.max_saved_sessions,
|
||||||
@@ -860,7 +883,7 @@ impl SessionController {
|
|||||||
};
|
};
|
||||||
let usage_ledger = Arc::new(TokioMutex::new(usage_ledger_instance));
|
let usage_ledger = Arc::new(TokioMutex::new(usage_ledger_instance));
|
||||||
|
|
||||||
Ok(Self {
|
let mut controller = Self {
|
||||||
provider,
|
provider,
|
||||||
conversation,
|
conversation,
|
||||||
model_manager,
|
model_manager,
|
||||||
@@ -878,6 +901,9 @@ impl SessionController {
|
|||||||
credential_manager,
|
credential_manager,
|
||||||
ui,
|
ui,
|
||||||
enable_code_tools,
|
enable_code_tools,
|
||||||
|
approval_mode,
|
||||||
|
plan_confirmed: true,
|
||||||
|
plan_instruction_inserted: false,
|
||||||
current_mode: initial_mode,
|
current_mode: initial_mode,
|
||||||
missing_oauth_servers,
|
missing_oauth_servers,
|
||||||
event_tx,
|
event_tx,
|
||||||
@@ -885,7 +911,11 @@ impl SessionController {
|
|||||||
stream_states: HashMap::new(),
|
stream_states: HashMap::new(),
|
||||||
usage_ledger,
|
usage_ledger,
|
||||||
last_compression: None,
|
last_compression: None,
|
||||||
})
|
};
|
||||||
|
|
||||||
|
controller.reset_plan_state();
|
||||||
|
|
||||||
|
Ok(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn conversation(&self) -> &Conversation {
|
pub fn conversation(&self) -> &Conversation {
|
||||||
@@ -1669,6 +1699,11 @@ impl SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_code_tools_enabled(&mut self, enabled: bool) -> Result<()> {
|
pub async fn set_code_tools_enabled(&mut self, enabled: bool) -> Result<()> {
|
||||||
|
if matches!(self.approval_mode, ApprovalMode::ReadOnly) && enabled {
|
||||||
|
warn!("Read-only approval mode prevents enabling code tools; ignoring request.");
|
||||||
|
self.enable_code_tools = false;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if self.enable_code_tools == enabled {
|
if self.enable_code_tools == enabled {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -1677,11 +1712,45 @@ impl SessionController {
|
|||||||
self.rebuild_tools().await
|
self.rebuild_tools().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reset_plan_state(&mut self) {
|
||||||
|
self.plan_instruction_inserted = false;
|
||||||
|
match self.approval_mode {
|
||||||
|
ApprovalMode::PlanFirst => {
|
||||||
|
self.plan_confirmed = false;
|
||||||
|
self.ensure_plan_prompt();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.plan_confirmed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_plan_prompt(&mut self) {
|
||||||
|
if !matches!(self.approval_mode, ApprovalMode::PlanFirst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.plan_instruction_inserted {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let prompt = "You are operating in plan-first approval mode. Outline a concise plan before using any tools or modifying files, and wait for the user to send '#approve' before proceeding.";
|
||||||
|
self.conversation.push_system_message(prompt);
|
||||||
|
self.plan_instruction_inserted = true;
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn set_operating_mode(&mut self, mode: Mode) -> Result<()> {
|
pub async fn set_operating_mode(&mut self, mode: Mode) -> Result<()> {
|
||||||
|
if matches!(self.approval_mode, ApprovalMode::ReadOnly) && matches!(mode, Mode::Code) {
|
||||||
|
warn!(
|
||||||
|
"Read-only approval mode prevents switching to code mode; remaining in chat mode."
|
||||||
|
);
|
||||||
|
self.current_mode = Mode::Chat;
|
||||||
|
self.set_code_tools_enabled(false).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
self.current_mode = mode;
|
self.current_mode = mode;
|
||||||
let enable_code_tools = matches!(mode, Mode::Code);
|
let enable_code_tools = matches!(mode, Mode::Code);
|
||||||
self.set_code_tools_enabled(enable_code_tools).await?;
|
self.set_code_tools_enabled(enable_code_tools).await?;
|
||||||
self.mcp_client.set_mode(mode).await
|
self.mcp_client.set_mode(self.current_mode).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_dir(&self, path: &str) -> Result<Vec<String>> {
|
pub async fn list_dir(&self, path: &str) -> Result<Vec<String>> {
|
||||||
@@ -1887,6 +1956,36 @@ impl SessionController {
|
|||||||
) -> Result<SessionOutcome> {
|
) -> Result<SessionOutcome> {
|
||||||
let streaming = { self.config.lock().await.general.enable_streaming || parameters.stream };
|
let streaming = { self.config.lock().await.general.enable_streaming || parameters.stream };
|
||||||
parameters.stream = streaming;
|
parameters.stream = streaming;
|
||||||
|
|
||||||
|
if matches!(self.approval_mode, ApprovalMode::PlanFirst) {
|
||||||
|
self.ensure_plan_prompt();
|
||||||
|
let trimmed = content.trim();
|
||||||
|
let normalized = trimmed.to_ascii_lowercase();
|
||||||
|
if matches!(
|
||||||
|
normalized.as_str(),
|
||||||
|
"#approve" | ":approve" | "/approve" | "approve" | "approve plan"
|
||||||
|
) {
|
||||||
|
self.conversation.push_user_message(trimmed.to_string());
|
||||||
|
let _ = self.maybe_auto_compress().await?;
|
||||||
|
if !self.plan_confirmed {
|
||||||
|
self.plan_confirmed = true;
|
||||||
|
self.conversation
|
||||||
|
.push_system_message("Plan approved by the operator. Tool access enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let ack = Message::assistant(
|
||||||
|
"Plan approved. Ready to continue with execution.".to_string(),
|
||||||
|
);
|
||||||
|
self.conversation.push_message(ack.clone());
|
||||||
|
return Ok(SessionOutcome::Complete(ChatResponse {
|
||||||
|
message: ack,
|
||||||
|
usage: None,
|
||||||
|
is_streaming: false,
|
||||||
|
is_final: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.conversation.push_user_message(content);
|
self.conversation.push_user_message(content);
|
||||||
let _ = self.maybe_auto_compress().await?;
|
let _ = self.maybe_auto_compress().await?;
|
||||||
self.send_request_with_current_conversation(parameters)
|
self.send_request_with_current_conversation(parameters)
|
||||||
@@ -1900,10 +1999,18 @@ impl SessionController {
|
|||||||
let streaming = { self.config.lock().await.general.enable_streaming || parameters.stream };
|
let streaming = { self.config.lock().await.general.enable_streaming || parameters.stream };
|
||||||
parameters.stream = streaming;
|
parameters.stream = streaming;
|
||||||
|
|
||||||
|
if matches!(self.approval_mode, ApprovalMode::PlanFirst) {
|
||||||
|
self.ensure_plan_prompt();
|
||||||
|
}
|
||||||
|
|
||||||
let active_model = self.conversation.active().model.clone();
|
let active_model = self.conversation.active().model.clone();
|
||||||
let registry_tools = self.tool_registry.all();
|
let registry_tools = self.tool_registry.all();
|
||||||
let mut include_tools = !registry_tools.is_empty();
|
let mut include_tools = !registry_tools.is_empty();
|
||||||
|
|
||||||
|
if matches!(self.approval_mode, ApprovalMode::PlanFirst) && !self.plan_confirmed {
|
||||||
|
include_tools = false;
|
||||||
|
}
|
||||||
|
|
||||||
if include_tools {
|
if include_tools {
|
||||||
let cached_support = self.model_manager.select(&active_model).await;
|
let cached_support = self.model_manager.select(&active_model).await;
|
||||||
let supports_tools = match cached_support {
|
let supports_tools = match cached_support {
|
||||||
@@ -2192,11 +2299,13 @@ impl SessionController {
|
|||||||
pub fn start_new_conversation(&mut self, model: Option<String>, name: Option<String>) {
|
pub fn start_new_conversation(&mut self, model: Option<String>, name: Option<String>) {
|
||||||
self.conversation.start_new(model, name);
|
self.conversation.start_new(model, name);
|
||||||
self.stream_states.clear();
|
self.stream_states.clear();
|
||||||
|
self.reset_plan_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.conversation.clear();
|
self.conversation.clear();
|
||||||
self.stream_states.clear();
|
self.stream_states.clear();
|
||||||
|
self.reset_plan_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_conversation_description(&self) -> Result<String> {
|
pub async fn generate_conversation_description(&self) -> Result<String> {
|
||||||
|
|||||||
Reference in New Issue
Block a user