adding logging, cleaning up configs
This commit is contained in:
@@ -56,10 +56,21 @@ use crate::ai::agent::{
|
||||
CancellationOutcome, CancellationReason, CreateDocumentsResult, EditDocumentsResult,
|
||||
RequestCommandOutputResult,
|
||||
};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use crate::ai::agent::{
|
||||
AskUserQuestionResult, CallMCPToolResult, FetchConversationResult, FileGlobResult,
|
||||
FileGlobV2Result, GrepResult, InsertReviewCommentsResult, ReadDocumentsResult, ReadFilesResult,
|
||||
ReadMCPResourceResult, ReadShellCommandOutputResult, ReadSkillResult, RequestComputerUseResult,
|
||||
RequestFileEditsResult, RunAgentsResult, SearchCodebaseResult, SendMessageToAgentResult,
|
||||
StartAgentResult, TransferShellCommandControlToUserResult, UploadArtifactResult,
|
||||
UseComputerResult, WriteToLongRunningShellCommandResult,
|
||||
};
|
||||
use crate::ai::ai_document_view::DEFAULT_PLANNING_DOCUMENT_TITLE;
|
||||
use crate::ai::blocklist::action_model::execute::suggest_new_conversation::SuggestNewConversationExecutor;
|
||||
use crate::ai::document::ai_document_model::AIDocumentModel;
|
||||
use crate::ai::get_relevant_files::controller::GetRelevantFilesController;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use crate::ai::remote_logging::{self, RemoteLogLevel, RemoteLogRecord};
|
||||
use crate::terminal::model::session::active_session::ActiveSession;
|
||||
use crate::terminal::model_events::ModelEventDispatcher;
|
||||
use crate::terminal::TerminalModel;
|
||||
@@ -280,6 +291,274 @@ fn domain_tool_result(action_result: &AIAgentActionResult, permission_denied: bo
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn action_tool_name(action: &AIAgentAction) -> String {
|
||||
action
|
||||
.tool_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{:?}", AIAgentActionTypeDiscriminants::from(&action.action)))
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn action_result_type_name(result: &AIAgentActionResultType) -> &'static str {
|
||||
match result {
|
||||
AIAgentActionResultType::RequestCommandOutput(_) => "RequestCommandOutput",
|
||||
AIAgentActionResultType::WriteToLongRunningShellCommand(_) => {
|
||||
"WriteToLongRunningShellCommand"
|
||||
}
|
||||
AIAgentActionResultType::RequestFileEdits(_) => "RequestFileEdits",
|
||||
AIAgentActionResultType::ReadFiles(_) => "ReadFiles",
|
||||
AIAgentActionResultType::UploadArtifact(_) => "UploadArtifact",
|
||||
AIAgentActionResultType::SearchCodebase(_) => "SearchCodebase",
|
||||
AIAgentActionResultType::Grep(_) => "Grep",
|
||||
AIAgentActionResultType::FileGlob(_) => "FileGlob",
|
||||
AIAgentActionResultType::FileGlobV2(_) => "FileGlobV2",
|
||||
AIAgentActionResultType::ReadMCPResource(_) => "ReadMCPResource",
|
||||
AIAgentActionResultType::CallMCPTool(_) => "CallMCPTool",
|
||||
AIAgentActionResultType::ReadSkill(_) => "ReadSkill",
|
||||
AIAgentActionResultType::SuggestNewConversation(_) => "SuggestNewConversation",
|
||||
AIAgentActionResultType::SuggestPrompt(_) => "SuggestPrompt",
|
||||
AIAgentActionResultType::OpenCodeReview => "OpenCodeReview",
|
||||
AIAgentActionResultType::InsertReviewComments(_) => "InsertReviewComments",
|
||||
AIAgentActionResultType::InitProject => "InitProject",
|
||||
AIAgentActionResultType::ReadDocuments(_) => "ReadDocuments",
|
||||
AIAgentActionResultType::EditDocuments(_) => "EditDocuments",
|
||||
AIAgentActionResultType::CreateDocuments(_) => "CreateDocuments",
|
||||
AIAgentActionResultType::ReadShellCommandOutput(_) => "ReadShellCommandOutput",
|
||||
AIAgentActionResultType::UseComputer(_) => "UseComputer",
|
||||
AIAgentActionResultType::RequestComputerUse(_) => "RequestComputerUse",
|
||||
AIAgentActionResultType::FetchConversation(_) => "FetchConversation",
|
||||
AIAgentActionResultType::StartAgent(_) => "StartAgent",
|
||||
AIAgentActionResultType::SendMessageToAgent(_) => "SendMessageToAgent",
|
||||
AIAgentActionResultType::TransferShellCommandControlToUser(_) => {
|
||||
"TransferShellCommandControlToUser"
|
||||
}
|
||||
AIAgentActionResultType::AskUserQuestion(_) => "AskUserQuestion",
|
||||
AIAgentActionResultType::RunAgents(_) => "RunAgents",
|
||||
AIAgentActionResultType::WaitForEvents(_) => "WaitForEvents",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn action_result_status(result: &AIAgentActionResultType) -> &'static str {
|
||||
if result.is_successful() {
|
||||
"success"
|
||||
} else if result.is_failed() || action_result_failure_summary(result).is_some() {
|
||||
"error"
|
||||
} else if result.is_cancelled() {
|
||||
"cancelled"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn action_result_log_level(result: &AIAgentActionResultType) -> RemoteLogLevel {
|
||||
if result.is_failed() || action_result_failure_summary(result).is_some() {
|
||||
RemoteLogLevel::Error
|
||||
} else if result.is_cancelled() {
|
||||
RemoteLogLevel::Warn
|
||||
} else {
|
||||
RemoteLogLevel::Info
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn action_result_error_summary(result: &AIAgentActionResultType) -> Option<String> {
|
||||
if let Some(summary) = action_result_failure_summary(result) {
|
||||
Some(remote_logging::sanitize_error(summary))
|
||||
} else if result.is_cancelled() {
|
||||
Some(format!("{} cancelled", action_result_type_name(result)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn action_result_failure_summary(result: &AIAgentActionResultType) -> Option<String> {
|
||||
match result {
|
||||
AIAgentActionResultType::RequestCommandOutput(RequestCommandOutputResult::Completed {
|
||||
exit_code,
|
||||
..
|
||||
}) if !exit_code.was_successful() => {
|
||||
Some(format!("command exited with code {}", exit_code.value()))
|
||||
}
|
||||
AIAgentActionResultType::RequestCommandOutput(RequestCommandOutputResult::Denylisted {
|
||||
..
|
||||
}) => Some("command was denylisted".to_string()),
|
||||
AIAgentActionResultType::WriteToLongRunningShellCommand(
|
||||
WriteToLongRunningShellCommandResult::CommandFinished { exit_code, .. },
|
||||
) if !exit_code.was_successful() => Some(format!(
|
||||
"long-running shell command exited with code {}",
|
||||
exit_code.value()
|
||||
)),
|
||||
AIAgentActionResultType::WriteToLongRunningShellCommand(
|
||||
WriteToLongRunningShellCommandResult::Error(error),
|
||||
) => Some(format!("{error:?}")),
|
||||
AIAgentActionResultType::RequestFileEdits(
|
||||
RequestFileEditsResult::DiffApplicationFailed { error },
|
||||
) => Some(error.clone()),
|
||||
AIAgentActionResultType::ReadFiles(ReadFilesResult::Error(error))
|
||||
| AIAgentActionResultType::UploadArtifact(UploadArtifactResult::Error(error))
|
||||
| AIAgentActionResultType::Grep(GrepResult::Error(error))
|
||||
| AIAgentActionResultType::FileGlob(FileGlobResult::Error(error))
|
||||
| AIAgentActionResultType::FileGlobV2(FileGlobV2Result::Error(error))
|
||||
| AIAgentActionResultType::ReadMCPResource(ReadMCPResourceResult::Error(error))
|
||||
| AIAgentActionResultType::CallMCPTool(CallMCPToolResult::Error(error))
|
||||
| AIAgentActionResultType::ReadSkill(ReadSkillResult::Error(error))
|
||||
| AIAgentActionResultType::ReadDocuments(ReadDocumentsResult::Error(error))
|
||||
| AIAgentActionResultType::EditDocuments(EditDocumentsResult::Error(error))
|
||||
| AIAgentActionResultType::CreateDocuments(CreateDocumentsResult::Error(error))
|
||||
| AIAgentActionResultType::UseComputer(UseComputerResult::Error(error))
|
||||
| AIAgentActionResultType::RequestComputerUse(RequestComputerUseResult::Error(error))
|
||||
| AIAgentActionResultType::FetchConversation(FetchConversationResult::Error(error))
|
||||
| AIAgentActionResultType::SendMessageToAgent(SendMessageToAgentResult::Error(error))
|
||||
| AIAgentActionResultType::AskUserQuestion(AskUserQuestionResult::Error(error)) => {
|
||||
Some(error.clone())
|
||||
}
|
||||
AIAgentActionResultType::SearchCodebase(SearchCodebaseResult::Failed {
|
||||
reason,
|
||||
message,
|
||||
}) => Some(format!("{reason:?}: {message}")),
|
||||
AIAgentActionResultType::ReadShellCommandOutput(
|
||||
ReadShellCommandOutputResult::CommandFinished { exit_code, .. },
|
||||
) if !exit_code.was_successful() => Some(format!(
|
||||
"shell command output exited with code {}",
|
||||
exit_code.value()
|
||||
)),
|
||||
AIAgentActionResultType::ReadShellCommandOutput(ReadShellCommandOutputResult::Error(
|
||||
error,
|
||||
)) => Some(format!("{error:?}")),
|
||||
AIAgentActionResultType::InsertReviewComments(InsertReviewCommentsResult::Error {
|
||||
message,
|
||||
..
|
||||
}) => Some(message.clone()),
|
||||
AIAgentActionResultType::StartAgent(StartAgentResult::Error { error, .. }) => {
|
||||
Some(error.clone())
|
||||
}
|
||||
AIAgentActionResultType::TransferShellCommandControlToUser(
|
||||
TransferShellCommandControlToUserResult::CommandFinished { exit_code, .. },
|
||||
) if !exit_code.was_successful() => Some(format!(
|
||||
"transferred shell command exited with code {}",
|
||||
exit_code.value()
|
||||
)),
|
||||
AIAgentActionResultType::TransferShellCommandControlToUser(
|
||||
TransferShellCommandControlToUserResult::Error(error),
|
||||
) => Some(format!("{error:?}")),
|
||||
AIAgentActionResultType::RunAgents(RunAgentsResult::Denied { reason }) => {
|
||||
Some(reason.clone())
|
||||
}
|
||||
AIAgentActionResultType::RunAgents(RunAgentsResult::Failure { error }) => {
|
||||
Some(error.clone())
|
||||
}
|
||||
AIAgentActionResultType::RequestCommandOutput(
|
||||
RequestCommandOutputResult::Completed { .. }
|
||||
| RequestCommandOutputResult::CancelledBeforeExecution
|
||||
| RequestCommandOutputResult::LongRunningCommandSnapshot { .. },
|
||||
)
|
||||
| AIAgentActionResultType::WriteToLongRunningShellCommand(
|
||||
WriteToLongRunningShellCommandResult::Cancelled
|
||||
| WriteToLongRunningShellCommandResult::CommandFinished { .. }
|
||||
| WriteToLongRunningShellCommandResult::Snapshot { .. },
|
||||
)
|
||||
| AIAgentActionResultType::RequestFileEdits(
|
||||
RequestFileEditsResult::Cancelled | RequestFileEditsResult::Success { .. },
|
||||
)
|
||||
| AIAgentActionResultType::ReadFiles(
|
||||
ReadFilesResult::Success { .. } | ReadFilesResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::UploadArtifact(
|
||||
UploadArtifactResult::Success { .. } | UploadArtifactResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::SearchCodebase(
|
||||
SearchCodebaseResult::Success { .. } | SearchCodebaseResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::Grep(GrepResult::Success { .. } | GrepResult::Cancelled)
|
||||
| AIAgentActionResultType::FileGlob(
|
||||
FileGlobResult::Success { .. } | FileGlobResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::FileGlobV2(
|
||||
FileGlobV2Result::Success { .. } | FileGlobV2Result::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::ReadMCPResource(
|
||||
ReadMCPResourceResult::Success { .. } | ReadMCPResourceResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::CallMCPTool(
|
||||
CallMCPToolResult::Success { .. } | CallMCPToolResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::ReadSkill(
|
||||
ReadSkillResult::Success { .. } | ReadSkillResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::SuggestNewConversation(_)
|
||||
| AIAgentActionResultType::SuggestPrompt(_)
|
||||
| AIAgentActionResultType::OpenCodeReview
|
||||
| AIAgentActionResultType::InsertReviewComments(
|
||||
InsertReviewCommentsResult::Success { .. } | InsertReviewCommentsResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::InitProject
|
||||
| AIAgentActionResultType::ReadDocuments(
|
||||
ReadDocumentsResult::Success { .. } | ReadDocumentsResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::EditDocuments(
|
||||
EditDocumentsResult::Success { .. } | EditDocumentsResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::CreateDocuments(
|
||||
CreateDocumentsResult::Success { .. } | CreateDocumentsResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::ReadShellCommandOutput(
|
||||
ReadShellCommandOutputResult::Cancelled
|
||||
| ReadShellCommandOutputResult::CommandFinished { .. }
|
||||
| ReadShellCommandOutputResult::LongRunningCommandSnapshot { .. },
|
||||
)
|
||||
| AIAgentActionResultType::UseComputer(
|
||||
UseComputerResult::Success(_) | UseComputerResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::RequestComputerUse(
|
||||
RequestComputerUseResult::Approved { .. } | RequestComputerUseResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::FetchConversation(
|
||||
FetchConversationResult::Success { .. } | FetchConversationResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::StartAgent(
|
||||
StartAgentResult::Success { .. } | StartAgentResult::Cancelled { .. },
|
||||
)
|
||||
| AIAgentActionResultType::SendMessageToAgent(
|
||||
SendMessageToAgentResult::Success { .. } | SendMessageToAgentResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::TransferShellCommandControlToUser(
|
||||
TransferShellCommandControlToUserResult::Cancelled
|
||||
| TransferShellCommandControlToUserResult::CommandFinished { .. }
|
||||
| TransferShellCommandControlToUserResult::Snapshot { .. },
|
||||
)
|
||||
| AIAgentActionResultType::AskUserQuestion(
|
||||
AskUserQuestionResult::Success { .. }
|
||||
| AskUserQuestionResult::Cancelled
|
||||
| AskUserQuestionResult::SkippedByAutoApprove { .. },
|
||||
)
|
||||
| AIAgentActionResultType::RunAgents(
|
||||
RunAgentsResult::Launched { .. } | RunAgentsResult::Cancelled,
|
||||
)
|
||||
| AIAgentActionResultType::WaitForEvents(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn log_tool_event(
|
||||
ctx: &mut ModelContext<BlocklistAIActionModel>,
|
||||
level: RemoteLogLevel,
|
||||
message: &str,
|
||||
context: serde_json::Value,
|
||||
) {
|
||||
remote_logging::log_model_event(
|
||||
ctx,
|
||||
RemoteLogRecord {
|
||||
level,
|
||||
message: message.to_string(),
|
||||
context,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub struct BlocklistAIActionModel {
|
||||
executor: ModelHandle<BlocklistAIActionExecutor>,
|
||||
|
||||
@@ -927,6 +1206,21 @@ impl BlocklistAIActionModel {
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if reason.needs_confirmation() {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
log_tool_event(
|
||||
ctx,
|
||||
RemoteLogLevel::Info,
|
||||
"Tool permission requested",
|
||||
serde_json::json!({
|
||||
"event": "tool_permission_requested",
|
||||
"conversation_id": conversation_id.to_string(),
|
||||
"action_id": action.id.to_string(),
|
||||
"task_id": action.task_id.to_string(),
|
||||
"tool_name": action_tool_name(action),
|
||||
"permission_kind": format!("{:?}", permission_kind_for_action(&action.action)),
|
||||
"reason": format!("{reason:?}"),
|
||||
}),
|
||||
);
|
||||
ctx.emit(BlocklistAIActionEvent::ActionBlockedOnUserConfirmation(
|
||||
action.id.clone(),
|
||||
));
|
||||
@@ -1005,7 +1299,27 @@ impl BlocklistAIActionModel {
|
||||
|
||||
let action_id = action.id.clone();
|
||||
let phase = self.action_phase_for_action(&action, ctx);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let remote_log_action_context = serde_json::json!({
|
||||
"conversation_id": conversation_id.to_string(),
|
||||
"action_id": action_id.to_string(),
|
||||
"task_id": action.task_id.to_string(),
|
||||
"tool_name": action_tool_name(&action),
|
||||
"permission_kind": format!("{:?}", permission_kind_for_action(&action.action)),
|
||||
"phase": format!("{phase:?}"),
|
||||
});
|
||||
if is_user_initiated {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
log_tool_event(
|
||||
ctx,
|
||||
RemoteLogLevel::Info,
|
||||
"Tool permission resolved",
|
||||
serde_json::json!({
|
||||
"event": "tool_permission_resolved",
|
||||
"decision": "allow_once",
|
||||
"tool": remote_log_action_context,
|
||||
}),
|
||||
);
|
||||
ctx.emit(BlocklistAIActionEvent::ToolLifecycle {
|
||||
action_id: action_id.clone(),
|
||||
event: ToolEvent::PermissionResolved {
|
||||
@@ -1024,6 +1338,17 @@ impl BlocklistAIActionModel {
|
||||
|
||||
match execute_result {
|
||||
TryExecuteResult::ExecutedAsync => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
log_tool_event(
|
||||
ctx,
|
||||
RemoteLogLevel::Info,
|
||||
"Tool execution started",
|
||||
serde_json::json!({
|
||||
"event": "tool_execution_started",
|
||||
"initiated_by": if is_user_initiated { "user" } else { "auto" },
|
||||
"tool": remote_log_action_context,
|
||||
}),
|
||||
);
|
||||
if !is_wait_for_events {
|
||||
self.update_conversation_in_progress_status(conversation_id, ctx);
|
||||
}
|
||||
@@ -1031,6 +1356,17 @@ impl BlocklistAIActionModel {
|
||||
Some(StartedAction::Async { phase })
|
||||
}
|
||||
TryExecuteResult::ExecutedSync => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
log_tool_event(
|
||||
ctx,
|
||||
RemoteLogLevel::Info,
|
||||
"Tool execution started",
|
||||
serde_json::json!({
|
||||
"event": "tool_execution_started",
|
||||
"initiated_by": if is_user_initiated { "user" } else { "auto" },
|
||||
"tool": remote_log_action_context,
|
||||
}),
|
||||
);
|
||||
if !is_wait_for_events {
|
||||
self.update_conversation_in_progress_status(conversation_id, ctx);
|
||||
}
|
||||
@@ -1079,6 +1415,29 @@ impl BlocklistAIActionModel {
|
||||
std::mem::discriminant(&action.action)
|
||||
);
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
log_tool_event(
|
||||
ctx,
|
||||
RemoteLogLevel::Info,
|
||||
"Tools queued",
|
||||
serde_json::json!({
|
||||
"event": "tools_queued",
|
||||
"conversation_id": conversation_id.to_string(),
|
||||
"tool_count": actions.len(),
|
||||
"tools": actions
|
||||
.iter()
|
||||
.map(|action| {
|
||||
serde_json::json!({
|
||||
"action_id": action.id.to_string(),
|
||||
"task_id": action.task_id.to_string(),
|
||||
"tool_name": action_tool_name(action),
|
||||
"requires_result": action.requires_result,
|
||||
"permission_kind": format!("{:?}", permission_kind_for_action(&action.action)),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
}),
|
||||
);
|
||||
self.action_order.insert(
|
||||
conversation_id,
|
||||
actions
|
||||
@@ -1309,6 +1668,21 @@ impl BlocklistAIActionModel {
|
||||
if permission_denied {
|
||||
self.denied_permissions
|
||||
.insert((conversation_id, pending_action.id.clone()));
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
log_tool_event(
|
||||
ctx,
|
||||
RemoteLogLevel::Warn,
|
||||
"Tool permission resolved",
|
||||
serde_json::json!({
|
||||
"event": "tool_permission_resolved",
|
||||
"decision": "denied",
|
||||
"conversation_id": conversation_id.to_string(),
|
||||
"action_id": pending_action.id.to_string(),
|
||||
"task_id": pending_action.task_id.to_string(),
|
||||
"tool_name": action_tool_name(&pending_action),
|
||||
"permission_kind": format!("{:?}", permission_kind_for_action(&pending_action.action)),
|
||||
}),
|
||||
);
|
||||
ctx.emit(BlocklistAIActionEvent::ToolLifecycle {
|
||||
action_id: pending_action.id.clone(),
|
||||
event: ToolEvent::PermissionResolved {
|
||||
@@ -1476,6 +1850,32 @@ impl BlocklistAIActionModel {
|
||||
.entry(conversation_id)
|
||||
.or_default()
|
||||
.push(tool_result.clone());
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
log_tool_event(
|
||||
ctx,
|
||||
if permission_denied {
|
||||
RemoteLogLevel::Warn
|
||||
} else {
|
||||
action_result_log_level(&action_result.result)
|
||||
},
|
||||
"Tool execution completed",
|
||||
serde_json::json!({
|
||||
"event": "tool_execution_completed",
|
||||
"conversation_id": conversation_id.to_string(),
|
||||
"action_id": action_result.id.to_string(),
|
||||
"task_id": action_result.task_id.to_string(),
|
||||
"result_type": action_result_type_name(&action_result.result),
|
||||
"status": if permission_denied {
|
||||
"denied"
|
||||
} else {
|
||||
action_result_status(&action_result.result)
|
||||
},
|
||||
"tool_result_status": format!("{:?}", tool_result.status),
|
||||
"permission_denied": permission_denied,
|
||||
"cancellation_reason": cancellation_reason.map(|reason| format!("{reason:?}")),
|
||||
"error": action_result_error_summary(&action_result.result),
|
||||
}),
|
||||
);
|
||||
ctx.emit(BlocklistAIActionEvent::ToolLifecycle {
|
||||
action_id: action_result.id.clone(),
|
||||
event: ToolEvent::Completed {
|
||||
|
||||
@@ -26,6 +26,7 @@ use itertools::Itertools;
|
||||
use parking_lot::FairMutex;
|
||||
use pending_response_streams::PendingResponseStreams;
|
||||
use session_sharing_protocol::common::ParticipantId;
|
||||
use settings::Setting;
|
||||
pub use slash_command::*;
|
||||
use warp_multi_agent_api::{message, Task, ToolType};
|
||||
use warpui::r#async::{SpawnedFutureHandle, Timer};
|
||||
@@ -220,9 +221,18 @@ enum RunningCommandDetection {
|
||||
fn acp_backend_model_id(backend: &AgentBackend) -> Option<LLMId> {
|
||||
match backend {
|
||||
AgentBackend::Provider => None,
|
||||
AgentBackend::Acp(acp) => {
|
||||
Some(crate::ai::acp::acp_selection_identity(&acp.agent_id, &acp.config_values).into())
|
||||
}
|
||||
AgentBackend::Acp(acp) => Some(
|
||||
if acp.provider_id.is_empty() {
|
||||
crate::ai::acp::acp_selection_identity(&acp.agent_id, &acp.config_values)
|
||||
} else {
|
||||
crate::ai::acp::acp_provider_selection_identity(
|
||||
&acp.provider_id,
|
||||
&acp.agent_id,
|
||||
&acp.config_values,
|
||||
)
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3837,7 +3847,9 @@ impl BlocklistAIController {
|
||||
.map(|conversation| {
|
||||
(
|
||||
match conversation.agent_backend() {
|
||||
AgentBackend::Acp(acp) => Some(acp.agent_id.clone()),
|
||||
AgentBackend::Acp(acp) => {
|
||||
Some((acp.provider_id.clone(), acp.agent_id.clone()))
|
||||
}
|
||||
AgentBackend::Provider => None,
|
||||
},
|
||||
conversation
|
||||
@@ -3853,10 +3865,29 @@ impl BlocklistAIController {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
if let Some(metadata) = response_stream.as_ref(ctx).acp_session_metadata() {
|
||||
if !metadata.config_options.is_empty() {
|
||||
if let Some(agent_id) = &agent_id {
|
||||
if let Some((provider_id, agent_id)) = &agent_id {
|
||||
crate::settings::AISettings::handle(ctx).update(
|
||||
ctx,
|
||||
|settings, ctx| {
|
||||
let normalized_options =
|
||||
crate::ai::acp::AcpRuntimeModel::normalize_config_options(
|
||||
metadata.config_options.clone(),
|
||||
);
|
||||
let mut providers =
|
||||
settings.acp_providers.value().clone();
|
||||
if let Some(provider) = providers
|
||||
.iter_mut()
|
||||
.find(|provider| provider.id == provider_id.as_str())
|
||||
{
|
||||
provider.config_options = normalized_options;
|
||||
if let Err(error) =
|
||||
settings.acp_providers.set_value(providers, ctx)
|
||||
{
|
||||
log::warn!(
|
||||
"Failed to persist ACP provider runtime config: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Err(error) =
|
||||
crate::ai::acp::AcpRuntimeModel::persist_runtime_options(
|
||||
settings,
|
||||
|
||||
@@ -38,6 +38,8 @@ use crate::ai::blocklist::BlocklistAIPermissions;
|
||||
use crate::ai::llms::{LLMId, LLMPreferences};
|
||||
use crate::ai::openai::client::OpenAIClientConfig;
|
||||
use crate::ai::provider::ProviderConfig;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use crate::ai::remote_logging::{self, RemoteLogLevel, RemoteLogRecord};
|
||||
use crate::ai::runtime::ProviderRuntime;
|
||||
use crate::network::NetworkStatus;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
@@ -142,6 +144,10 @@ pub struct ResponseStream {
|
||||
has_received_client_actions: bool,
|
||||
/// AI identifiers for telemetry emission
|
||||
ai_identifiers: AIIdentifiers,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
remote_log_backend: String,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
remote_log_provider: String,
|
||||
|
||||
/// Whether this request can attempt to resume the conversation on error.
|
||||
/// This is true for all requests except those that are themselves the result of a resume
|
||||
@@ -208,6 +214,10 @@ impl ResponseStream {
|
||||
original_error: None,
|
||||
has_received_client_actions: false,
|
||||
ai_identifiers: AIIdentifiers::default(),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
remote_log_backend: "provider".to_string(),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
remote_log_provider: "test".to_string(),
|
||||
can_attempt_resume_on_error: false,
|
||||
should_resume_conversation_after_stream_finished: false,
|
||||
stream_finished_received: false,
|
||||
@@ -292,6 +302,250 @@ impl ResponseStream {
|
||||
ProviderConfig::None
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn remote_log_provider_for_config(provider_config: &ProviderConfig) -> String {
|
||||
match provider_config {
|
||||
ProviderConfig::Bedrock(config) => {
|
||||
let region = if config.region.trim().is_empty() {
|
||||
"auto"
|
||||
} else {
|
||||
config.region.as_str()
|
||||
};
|
||||
format!("bedrock:{:?}:region={region}", config.auth_method)
|
||||
}
|
||||
ProviderConfig::OpenAI(config) => {
|
||||
format!("openai:{:?}:rig={}", config.kind, config.use_rig)
|
||||
}
|
||||
ProviderConfig::None => "none".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn common_remote_log_context(
|
||||
&self,
|
||||
event: &str,
|
||||
request_id: Uuid,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut context = serde_json::Map::new();
|
||||
context.insert("event".to_string(), serde_json::json!(event));
|
||||
context.insert("stream_id".to_string(), serde_json::json!(self.id.0));
|
||||
context.insert(
|
||||
"request_id".to_string(),
|
||||
serde_json::json!(request_id.to_string()),
|
||||
);
|
||||
context.insert(
|
||||
"model_id".to_string(),
|
||||
serde_json::json!(self.params.model.as_str()),
|
||||
);
|
||||
context.insert(
|
||||
"backend".to_string(),
|
||||
serde_json::json!(self.remote_log_backend),
|
||||
);
|
||||
context.insert(
|
||||
"provider".to_string(),
|
||||
serde_json::json!(self.remote_log_provider),
|
||||
);
|
||||
context.insert(
|
||||
"retry_count".to_string(),
|
||||
serde_json::json!(self.retry_count),
|
||||
);
|
||||
context.insert(
|
||||
"has_received_client_actions".to_string(),
|
||||
serde_json::json!(self.has_received_client_actions),
|
||||
);
|
||||
context.insert(
|
||||
"can_attempt_resume_on_error".to_string(),
|
||||
serde_json::json!(self.can_attempt_resume_on_error),
|
||||
);
|
||||
context.insert(
|
||||
"identifiers".to_string(),
|
||||
serde_json::to_value(&self.ai_identifiers).unwrap_or_else(|_| serde_json::json!({})),
|
||||
);
|
||||
context
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn log_llm_request_started(
|
||||
ctx: &mut ModelContext<Self>,
|
||||
stream_id: &ResponseStreamId,
|
||||
request_id: Uuid,
|
||||
params: &api::RequestParams,
|
||||
ai_identifiers: &AIIdentifiers,
|
||||
backend: &str,
|
||||
provider: &str,
|
||||
can_attempt_resume_on_error: bool,
|
||||
) {
|
||||
remote_logging::log_model_event(
|
||||
ctx,
|
||||
RemoteLogRecord {
|
||||
level: RemoteLogLevel::Info,
|
||||
message: "LLM request started".to_string(),
|
||||
context: serde_json::json!({
|
||||
"event": "llm_request_started",
|
||||
"stream_id": stream_id.0,
|
||||
"request_id": request_id.to_string(),
|
||||
"model_id": params.model.as_str(),
|
||||
"coding_model_id": params.coding_model.as_str(),
|
||||
"backend": backend,
|
||||
"provider": provider,
|
||||
"input_count": params.input.len(),
|
||||
"tool_result_count": params.tool_results.len(),
|
||||
"task_count": params.tasks.len(),
|
||||
"message_history_count": params.message_history.len(),
|
||||
"has_progressive_summary": params.progressive_summary.is_some(),
|
||||
"memory_enabled": params.is_memory_enabled,
|
||||
"warp_drive_context_enabled": params.warp_drive_context_enabled,
|
||||
"planning_enabled": params.planning_enabled,
|
||||
"web_search_enabled": params.web_search_enabled,
|
||||
"computer_use_enabled": params.computer_use_enabled,
|
||||
"ask_user_question_enabled": params.ask_user_question_enabled,
|
||||
"orchestration_enabled": params.orchestration_enabled,
|
||||
"is_remote_session": params.session_context.is_remote(),
|
||||
"can_attempt_resume_on_error": can_attempt_resume_on_error,
|
||||
"identifiers": serde_json::to_value(ai_identifiers).unwrap_or_else(|_| serde_json::json!({})),
|
||||
}),
|
||||
},
|
||||
);
|
||||
if let Some(raw_payload) =
|
||||
remote_logging::raw_model_payload_context(ctx, raw_model_request_payload(params))
|
||||
{
|
||||
remote_logging::log_model_event(
|
||||
ctx,
|
||||
RemoteLogRecord {
|
||||
level: RemoteLogLevel::Info,
|
||||
message: "Raw model request".to_string(),
|
||||
context: serde_json::json!({
|
||||
"event": "raw_model_request",
|
||||
"stream_id": stream_id.0,
|
||||
"request_id": request_id.to_string(),
|
||||
"model_id": params.model.as_str(),
|
||||
"backend": backend,
|
||||
"provider": provider,
|
||||
"raw_payload": raw_payload,
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn log_galaxy_decision(
|
||||
&self,
|
||||
request_id: Uuid,
|
||||
decision: &str,
|
||||
details: serde_json::Value,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let mut context = self.common_remote_log_context("galaxy_decision", request_id);
|
||||
context.insert("decision".to_string(), serde_json::json!(decision));
|
||||
context.insert("details".to_string(), details);
|
||||
remote_logging::log_model_event(
|
||||
ctx,
|
||||
RemoteLogRecord {
|
||||
level: RemoteLogLevel::Info,
|
||||
message: format!("Galaxy decision: {decision}"),
|
||||
context: serde_json::Value::Object(context),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn log_llm_response_finished(
|
||||
&self,
|
||||
request_id: Uuid,
|
||||
finished_event: &warp_multi_agent_api::response_event::StreamFinished,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let mut context = self.common_remote_log_context("llm_response_finished", request_id);
|
||||
context.insert(
|
||||
"reason".to_string(),
|
||||
serde_json::json!(stream_finished_reason_name(&finished_event.reason)),
|
||||
);
|
||||
context.insert(
|
||||
"elapsed_ms".to_string(),
|
||||
serde_json::json!(self.time_to_latest_event.num_milliseconds()),
|
||||
);
|
||||
context.insert(
|
||||
"should_refresh_model_config".to_string(),
|
||||
serde_json::json!(finished_event.should_refresh_model_config),
|
||||
);
|
||||
context.insert(
|
||||
"token_usage".to_string(),
|
||||
token_usage_context(&finished_event.token_usage),
|
||||
);
|
||||
if let Some(cost) = finished_event.request_cost.as_ref() {
|
||||
context.insert(
|
||||
"request_cost".to_string(),
|
||||
serde_json::json!({
|
||||
"exact": cost.exact,
|
||||
"platform_credits": cost.platform_credits,
|
||||
}),
|
||||
);
|
||||
}
|
||||
remote_logging::log_model_event(
|
||||
ctx,
|
||||
RemoteLogRecord {
|
||||
level: RemoteLogLevel::Info,
|
||||
message: "LLM response finished".to_string(),
|
||||
context: serde_json::Value::Object(context),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn log_llm_request_error(
|
||||
&self,
|
||||
request_id: Uuid,
|
||||
error: impl std::fmt::Display,
|
||||
recovery: &str,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let mut context = self.common_remote_log_context("llm_request_error", request_id);
|
||||
context.insert(
|
||||
"elapsed_ms".to_string(),
|
||||
serde_json::json!(self.time_to_latest_event.num_milliseconds()),
|
||||
);
|
||||
context.insert("recovery".to_string(), serde_json::json!(recovery));
|
||||
context.insert(
|
||||
"error".to_string(),
|
||||
serde_json::json!(remote_logging::sanitize_error(error)),
|
||||
);
|
||||
remote_logging::log_model_event(
|
||||
ctx,
|
||||
RemoteLogRecord {
|
||||
level: RemoteLogLevel::Warn,
|
||||
message: format!("LLM request error: {recovery}"),
|
||||
context: serde_json::Value::Object(context),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn log_raw_model_response(
|
||||
&self,
|
||||
request_id: Uuid,
|
||||
payload_kind: &str,
|
||||
raw_payload: impl AsRef<str>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let Some(raw_payload) =
|
||||
remote_logging::raw_model_payload_context(ctx, raw_payload.as_ref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut context = self.common_remote_log_context("raw_model_response", request_id);
|
||||
context.insert("payload_kind".to_string(), serde_json::json!(payload_kind));
|
||||
context.insert("raw_payload".to_string(), raw_payload);
|
||||
remote_logging::log_model_event(
|
||||
ctx,
|
||||
RemoteLogRecord {
|
||||
level: RemoteLogLevel::Info,
|
||||
message: "Raw model response".to_string(),
|
||||
context: serde_json::Value::Object(context),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn resolve_acp_manager(
|
||||
backend: &crate::persistence::model::AcpConversationData,
|
||||
@@ -300,23 +554,23 @@ impl ResponseStream {
|
||||
use galaxy_acp::AcpManagerConfig;
|
||||
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
let configured_agent_id = settings.acp_agent_id.value().trim();
|
||||
let provider = if backend.provider_id.is_empty() {
|
||||
settings.legacy_acp_provider()
|
||||
} else {
|
||||
settings.enabled_acp_provider_by_id(&backend.provider_id)
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
"The ACP connection for this conversation is no longer configured in Settings. Add it again or start a new ACP conversation."
|
||||
.to_string()
|
||||
})?;
|
||||
let configured_agent_id = provider.agent_id.trim();
|
||||
let configured_agent_id = if configured_agent_id.is_empty() {
|
||||
"codex"
|
||||
} else {
|
||||
configured_agent_id
|
||||
};
|
||||
let launch = resolve_acp_launch(
|
||||
configured_agent_id,
|
||||
settings.acp_agent_command.value(),
|
||||
settings.acp_agent_args.value(),
|
||||
)?;
|
||||
validate_acp_launch_identity(
|
||||
backend,
|
||||
configured_agent_id,
|
||||
settings.acp_agent_command.value(),
|
||||
&launch,
|
||||
)?;
|
||||
let launch = resolve_acp_launch(configured_agent_id, &provider.command, &provider.args)?;
|
||||
validate_acp_launch_identity(backend, configured_agent_id, &provider.command, &launch)?;
|
||||
let config = AcpManagerConfig::new(launch);
|
||||
AcpRuntimeModel::handle(ctx).update(ctx, |runtime, _| runtime.manager(config))
|
||||
}
|
||||
@@ -447,6 +701,7 @@ impl ResponseStream {
|
||||
let start_time = Local::now();
|
||||
|
||||
let request_id = Uuid::new_v4();
|
||||
let response_stream_id = ResponseStreamId(Uuid::new_v4().to_string());
|
||||
let runtime_capabilities = match &agent_backend {
|
||||
AgentBackend::Provider => RuntimeCapabilities::provider(),
|
||||
AgentBackend::Acp(_) => RuntimeCapabilities::session_runtime(),
|
||||
@@ -455,9 +710,28 @@ impl ResponseStream {
|
||||
let acp_session_metadata = Arc::new(Mutex::new(AcpSessionMetadata::default()));
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let acp_turn_control = Arc::new(Mutex::new(None));
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let remote_log_backend;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let remote_log_provider;
|
||||
match &agent_backend {
|
||||
AgentBackend::Provider => {
|
||||
let provider_config = Self::resolve_provider_config(params.model.as_str(), ctx);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
remote_log_backend = "provider".to_string();
|
||||
remote_log_provider = Self::remote_log_provider_for_config(&provider_config);
|
||||
Self::log_llm_request_started(
|
||||
ctx,
|
||||
&response_stream_id,
|
||||
request_id,
|
||||
¶ms,
|
||||
&ai_identifiers,
|
||||
&remote_log_backend,
|
||||
&remote_log_provider,
|
||||
can_attempt_resume_on_error,
|
||||
);
|
||||
}
|
||||
Self::spawn_provider_request(
|
||||
params.clone(),
|
||||
provider_config,
|
||||
@@ -467,6 +741,25 @@ impl ResponseStream {
|
||||
);
|
||||
}
|
||||
AgentBackend::Acp(backend) => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
remote_log_backend = "acp".to_string();
|
||||
remote_log_provider = if backend.agent_id.is_empty() {
|
||||
"acp".to_string()
|
||||
} else {
|
||||
format!("acp:{}", backend.agent_id)
|
||||
};
|
||||
Self::log_llm_request_started(
|
||||
ctx,
|
||||
&response_stream_id,
|
||||
request_id,
|
||||
¶ms,
|
||||
&ai_identifiers,
|
||||
&remote_log_backend,
|
||||
&remote_log_provider,
|
||||
can_attempt_resume_on_error,
|
||||
);
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
Self::spawn_acp_request(
|
||||
backend.clone(),
|
||||
@@ -500,7 +793,7 @@ impl ResponseStream {
|
||||
}
|
||||
}
|
||||
Self {
|
||||
id: ResponseStreamId(Uuid::new_v4().to_string()),
|
||||
id: response_stream_id,
|
||||
runtime_capabilities,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
acp_session_metadata,
|
||||
@@ -515,6 +808,10 @@ impl ResponseStream {
|
||||
original_error: None,
|
||||
has_received_client_actions: false,
|
||||
ai_identifiers,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
remote_log_backend,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
remote_log_provider,
|
||||
can_attempt_resume_on_error,
|
||||
should_resume_conversation_after_stream_finished: false,
|
||||
stream_finished_received: false,
|
||||
@@ -640,6 +937,16 @@ impl ResponseStream {
|
||||
|
||||
let request_id = Uuid::new_v4();
|
||||
self.current_request_id = Some(request_id);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_galaxy_decision(
|
||||
request_id,
|
||||
"retry_request",
|
||||
serde_json::json!({
|
||||
"retry_count": self.retry_count,
|
||||
"model_id": self.params.model.as_str(),
|
||||
}),
|
||||
ctx,
|
||||
);
|
||||
let params = self.params.clone();
|
||||
let provider_config = Self::resolve_provider_config(params.model.as_str(), ctx);
|
||||
let _ = ctx.spawn(
|
||||
@@ -675,6 +982,18 @@ impl ResponseStream {
|
||||
}
|
||||
|
||||
fn retry_with_coding_model(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
if let Some(request_id) = self.current_request_id {
|
||||
self.log_galaxy_decision(
|
||||
request_id,
|
||||
"fallback_to_coding_model",
|
||||
serde_json::json!({
|
||||
"from_model_id": self.params.model.as_str(),
|
||||
"to_model_id": self.params.coding_model.as_str(),
|
||||
}),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
self.coding_model_fallback_attempted = true;
|
||||
self.params.model = self.params.coding_model.clone();
|
||||
self.retry(ctx);
|
||||
@@ -730,6 +1049,13 @@ impl ResponseStream {
|
||||
// terminally. (HTTP send failures don't take this path — they arrive as
|
||||
// in-stream error events.)
|
||||
let error = Arc::new(AIApiError::Other(anyhow!(e)));
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_request_error(
|
||||
request_id,
|
||||
error.as_ref(),
|
||||
"stream_creation_failed",
|
||||
ctx,
|
||||
);
|
||||
self.error_event_emitted = true;
|
||||
self.report_request_failure(&error, NetworkStatus::as_ref(ctx).is_online());
|
||||
ctx.emit(ResponseStreamEvent::ReceivedEvent(Consumable::new(Err(
|
||||
@@ -762,6 +1088,42 @@ impl ResponseStream {
|
||||
action.id,
|
||||
action.task_id
|
||||
);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let mut context =
|
||||
self.common_remote_log_context("llm_tool_proposed", request_id);
|
||||
context.insert(
|
||||
"action_id".to_string(),
|
||||
serde_json::json!(action.id.to_string()),
|
||||
);
|
||||
context.insert(
|
||||
"task_id".to_string(),
|
||||
serde_json::json!(action.task_id.to_string()),
|
||||
);
|
||||
context.insert(
|
||||
"tool_name".to_string(),
|
||||
serde_json::json!(action_tool_name(action)),
|
||||
);
|
||||
context.insert(
|
||||
"requires_result".to_string(),
|
||||
serde_json::json!(action.requires_result),
|
||||
);
|
||||
remote_logging::log_model_event(
|
||||
ctx,
|
||||
RemoteLogRecord {
|
||||
level: RemoteLogLevel::Info,
|
||||
message: "LLM proposed tool".to_string(),
|
||||
context: serde_json::Value::Object(context),
|
||||
},
|
||||
);
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_raw_model_response(
|
||||
request_id,
|
||||
"tool_proposed",
|
||||
format!("{action:#?}"),
|
||||
ctx,
|
||||
);
|
||||
ctx.emit(ResponseStreamEvent::ReceivedEvent(Consumable::new(event)));
|
||||
}
|
||||
Ok(api::StreamEvent::Response(response_event)) => {
|
||||
@@ -787,6 +1149,13 @@ impl ResponseStream {
|
||||
None => "None",
|
||||
};
|
||||
log::info!("[bedrock-debug] ResponseStream emitting event type={event_type_name}");
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_raw_model_response(
|
||||
request_id,
|
||||
event_type_name,
|
||||
format!("{response_event:#?}"),
|
||||
ctx,
|
||||
);
|
||||
if let Some(event_type) = &response_event.r#type {
|
||||
match event_type {
|
||||
warp_multi_agent_api::response_event::Type::Init(init_event) => {
|
||||
@@ -796,12 +1165,35 @@ impl ResponseStream {
|
||||
init_event.request_id.clone(),
|
||||
));
|
||||
}
|
||||
warp_multi_agent_api::response_event::Type::ClientActions(_) => {
|
||||
warp_multi_agent_api::response_event::Type::ClientActions(
|
||||
client_actions,
|
||||
) => {
|
||||
// Mark that we've received client actions
|
||||
self.has_received_client_actions = true;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let mut context = self.common_remote_log_context(
|
||||
"llm_client_actions_received",
|
||||
request_id,
|
||||
);
|
||||
context.insert(
|
||||
"action_count".to_string(),
|
||||
serde_json::json!(client_actions.actions.len()),
|
||||
);
|
||||
remote_logging::log_model_event(
|
||||
ctx,
|
||||
RemoteLogRecord {
|
||||
level: RemoteLogLevel::Info,
|
||||
message: "LLM client actions received".to_string(),
|
||||
context: serde_json::Value::Object(context),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
warp_multi_agent_api::response_event::Type::Finished(finished_event) => {
|
||||
self.stream_finished_received = true;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_response_finished(request_id, finished_event, ctx);
|
||||
// Emit retry success telemetry on successful completion
|
||||
if matches!(
|
||||
finished_event.reason,
|
||||
@@ -837,6 +1229,13 @@ impl ResponseStream {
|
||||
log::warn!(
|
||||
"Thinking model rate-limited; retrying with the profile coding model"
|
||||
);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_request_error(
|
||||
request_id,
|
||||
e.as_ref(),
|
||||
"fallback_to_coding_model",
|
||||
ctx,
|
||||
);
|
||||
self.retry_with_coding_model(ctx);
|
||||
return;
|
||||
}
|
||||
@@ -855,6 +1254,8 @@ impl ResponseStream {
|
||||
self.retry_count + 1,
|
||||
MAX_RETRIES
|
||||
);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_request_error(request_id, e.as_ref(), "retry_now", ctx);
|
||||
// Only emit error telemetry here if we're retrying.
|
||||
// Final errors that aren't being retried are emitted elsewhere.
|
||||
self.emit_retryable_agent_mode_error_telemetry(format!("{e:?}"), ctx);
|
||||
@@ -868,6 +1269,13 @@ impl ResponseStream {
|
||||
self.retry_count + 1,
|
||||
MAX_RETRIES
|
||||
);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_request_error(
|
||||
request_id,
|
||||
e.as_ref(),
|
||||
"retry_when_online",
|
||||
ctx,
|
||||
);
|
||||
self.emit_retryable_agent_mode_error_telemetry(format!("{e:?}"), ctx);
|
||||
self.defer_retry_until_online(ctx);
|
||||
return;
|
||||
@@ -880,10 +1288,20 @@ impl ResponseStream {
|
||||
log::warn!(
|
||||
"MultiAgent request failed after client actions; resuming conversation after stream finishes - Error: {e:?}"
|
||||
);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_request_error(
|
||||
request_id,
|
||||
e.as_ref(),
|
||||
"resume_after_stream",
|
||||
ctx,
|
||||
);
|
||||
// The resume spawn itself waits for connectivity.
|
||||
self.should_resume_conversation_after_stream_finished = true;
|
||||
}
|
||||
RecoveryAction::Fail => {}
|
||||
RecoveryAction::Fail => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_request_error(request_id, e.as_ref(), "fail", ctx);
|
||||
}
|
||||
}
|
||||
self.error_event_emitted = true;
|
||||
|
||||
@@ -928,6 +1346,13 @@ impl ResponseStream {
|
||||
self.retry_count + 1,
|
||||
MAX_RETRIES
|
||||
);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_request_error(
|
||||
request_id,
|
||||
unexpected_eof.as_ref(),
|
||||
"retry_now",
|
||||
ctx,
|
||||
);
|
||||
self.emit_retryable_agent_mode_error_telemetry(
|
||||
format!("{unexpected_eof:?}"),
|
||||
ctx,
|
||||
@@ -941,6 +1366,13 @@ impl ResponseStream {
|
||||
self.retry_count + 1,
|
||||
MAX_RETRIES
|
||||
);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_request_error(
|
||||
request_id,
|
||||
unexpected_eof.as_ref(),
|
||||
"retry_when_online",
|
||||
ctx,
|
||||
);
|
||||
self.emit_retryable_agent_mode_error_telemetry(
|
||||
format!("{unexpected_eof:?}"),
|
||||
ctx,
|
||||
@@ -956,6 +1388,13 @@ impl ResponseStream {
|
||||
log::warn!(
|
||||
"MultiAgent request truncated after client actions; resuming conversation after stream finishes - Error: {unexpected_eof:?}"
|
||||
);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_request_error(
|
||||
request_id,
|
||||
unexpected_eof.as_ref(),
|
||||
"resume_after_stream",
|
||||
ctx,
|
||||
);
|
||||
self.should_resume_conversation_after_stream_finished = true;
|
||||
self.error_event_emitted = true;
|
||||
self.report_request_failure(&unexpected_eof, is_online);
|
||||
@@ -964,6 +1403,8 @@ impl ResponseStream {
|
||||
))));
|
||||
}
|
||||
RecoveryAction::Fail => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.log_llm_request_error(request_id, unexpected_eof.as_ref(), "fail", ctx);
|
||||
self.error_event_emitted = true;
|
||||
self.report_request_failure(&unexpected_eof, is_online);
|
||||
ctx.emit(ResponseStreamEvent::ReceivedEvent(Consumable::new(Err(
|
||||
@@ -1031,6 +1472,118 @@ impl ResponseStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn raw_model_request_payload(params: &api::RequestParams) -> String {
|
||||
let payload = serde_json::json!({
|
||||
"model_id": params.model.as_str(),
|
||||
"coding_model_id": params.coding_model.as_str(),
|
||||
"cli_agent_model_id": params.cli_agent_model.as_str(),
|
||||
"computer_use_model_id": params.computer_use_model.as_str(),
|
||||
"input": format!("{:#?}", params.input),
|
||||
"tool_results": format!("{:#?}", params.tool_results),
|
||||
"tasks": format!("{:#?}", params.tasks),
|
||||
"message_history": serde_json::to_value(¶ms.message_history)
|
||||
.unwrap_or_else(|_| serde_json::json!(format!("{:#?}", params.message_history))),
|
||||
"progressive_summary": ¶ms.progressive_summary,
|
||||
"tool_result_archive": serde_json::to_value(¶ms.tool_result_archive)
|
||||
.unwrap_or_else(|_| serde_json::json!(format!("{:#?}", params.tool_result_archive))),
|
||||
"global_rules": ¶ms.global_rules,
|
||||
"mcp_context": format!("{:#?}", params.mcp_context),
|
||||
"session": {
|
||||
"is_remote": params.session_context.is_remote(),
|
||||
},
|
||||
"features": {
|
||||
"memory_enabled": params.is_memory_enabled,
|
||||
"warp_drive_context_enabled": params.warp_drive_context_enabled,
|
||||
"planning_enabled": params.planning_enabled,
|
||||
"web_search_enabled": params.web_search_enabled,
|
||||
"computer_use_enabled": params.computer_use_enabled,
|
||||
"ask_user_question_enabled": params.ask_user_question_enabled,
|
||||
"research_agent_enabled": params.research_agent_enabled,
|
||||
"orchestration_enabled": params.orchestration_enabled,
|
||||
},
|
||||
"autonomy_level": format!("{:?}", params.autonomy_level),
|
||||
"isolation_level": format!("{:?}", params.isolation_level),
|
||||
"supported_tools_override": format!("{:#?}", params.supported_tools_override),
|
||||
"context_window_limit": params.context_window_limit,
|
||||
"omitted_sensitive_fields": [
|
||||
"api_keys",
|
||||
"custom_model_providers",
|
||||
"custom_model_routers",
|
||||
],
|
||||
});
|
||||
serde_json::to_string_pretty(&payload).unwrap_or_else(|_| format!("{payload:#?}"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn action_tool_name(action: &crate::ai::agent::AIAgentAction) -> String {
|
||||
action.tool_name.clone().unwrap_or_else(|| {
|
||||
format!(
|
||||
"{:?}",
|
||||
crate::ai::agent::AIAgentActionTypeDiscriminants::from(&action.action)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn stream_finished_reason_name(
|
||||
reason: &Option<response_event::stream_finished::Reason>,
|
||||
) -> &'static str {
|
||||
match reason {
|
||||
None => "done",
|
||||
Some(response_event::stream_finished::Reason::Done(_)) => "done",
|
||||
Some(response_event::stream_finished::Reason::MaxTokenLimit(_)) => "max_token_limit",
|
||||
Some(response_event::stream_finished::Reason::Other(_)) => "other",
|
||||
Some(response_event::stream_finished::Reason::ContextWindowExceeded(_)) => {
|
||||
"context_window_exceeded"
|
||||
}
|
||||
Some(response_event::stream_finished::Reason::QuotaLimit(_)) => "quota_limit",
|
||||
Some(response_event::stream_finished::Reason::LlmUnavailable(_)) => "llm_unavailable",
|
||||
Some(response_event::stream_finished::Reason::InvalidApiKey(_)) => "invalid_api_key",
|
||||
Some(response_event::stream_finished::Reason::InternalError(_)) => "internal_error",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn token_usage_context(
|
||||
token_usage: &[response_event::stream_finished::TokenUsage],
|
||||
) -> serde_json::Value {
|
||||
let total_input: u64 = token_usage
|
||||
.iter()
|
||||
.map(|usage| u64::from(usage.total_input))
|
||||
.sum();
|
||||
let output: u64 = token_usage
|
||||
.iter()
|
||||
.map(|usage| u64::from(usage.output))
|
||||
.sum();
|
||||
let input_cache_read: u64 = token_usage
|
||||
.iter()
|
||||
.map(|usage| u64::from(usage.input_cache_read))
|
||||
.sum();
|
||||
let input_cache_write: u64 = token_usage
|
||||
.iter()
|
||||
.map(|usage| u64::from(usage.input_cache_write))
|
||||
.sum();
|
||||
let cost_in_cents: f32 = token_usage.iter().map(|usage| usage.cost_in_cents).sum();
|
||||
serde_json::json!({
|
||||
"total_input": total_input,
|
||||
"output": output,
|
||||
"input_cache_read": input_cache_read,
|
||||
"input_cache_write": input_cache_write,
|
||||
"cost_in_cents": cost_in_cents,
|
||||
"models": token_usage.iter().map(|usage| {
|
||||
serde_json::json!({
|
||||
"model_id": usage.model_id,
|
||||
"total_input": usage.total_input,
|
||||
"output": usage.output,
|
||||
"input_cache_read": usage.input_cache_read,
|
||||
"input_cache_write": usage.input_cache_write,
|
||||
"cost_in_cents": usage.cost_in_cents,
|
||||
})
|
||||
}).collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn is_interactive_remote_command(command: &str) -> bool {
|
||||
is_potential_remote_ssh_command(command)
|
||||
|
||||
@@ -64,6 +64,7 @@ fn acp_backend_model_identity_does_not_claim_a_provider_model() {
|
||||
assert_eq!(super::acp_backend_model_id(&AgentBackend::Provider), None);
|
||||
assert_eq!(
|
||||
super::acp_backend_model_id(&AgentBackend::Acp(AcpConversationData {
|
||||
provider_id: String::new(),
|
||||
agent_id: " Codex ".to_owned(),
|
||||
launch_fingerprint: "launch-123".to_owned(),
|
||||
session_id: None,
|
||||
@@ -74,6 +75,19 @@ fn acp_backend_model_identity_does_not_claim_a_provider_model() {
|
||||
})),
|
||||
Some(LLMId::from("acp:codex:model=\"fast\""))
|
||||
);
|
||||
assert_eq!(
|
||||
super::acp_backend_model_id(&AgentBackend::Acp(AcpConversationData {
|
||||
provider_id: "work".to_owned(),
|
||||
agent_id: " Codex ".to_owned(),
|
||||
launch_fingerprint: "launch-123".to_owned(),
|
||||
session_id: None,
|
||||
config_values: std::collections::BTreeMap::from([(
|
||||
"model".to_owned(),
|
||||
serde_json::json!("fast"),
|
||||
)]),
|
||||
})),
|
||||
Some(LLMId::from("acp:work:codex:model=\"fast\""))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1206,33 +1206,29 @@ impl BlocklistAIHistoryModel {
|
||||
return llm_preferences.agent_backend_for_active_model(Some(terminal_surface_id), ctx);
|
||||
}
|
||||
|
||||
let configured_agent_id = settings.acp_agent_id.value().trim();
|
||||
let agent_id = if configured_agent_id.is_empty() {
|
||||
let providers = settings.enabled_acp_providers();
|
||||
let [provider] = providers.as_slice() else {
|
||||
return AgentBackend::Provider;
|
||||
};
|
||||
let agent_id = provider.agent_id.trim();
|
||||
let agent_id = if agent_id.is_empty() {
|
||||
"codex"
|
||||
} else {
|
||||
configured_agent_id
|
||||
agent_id
|
||||
};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let launch_fingerprint = acp_launch_fingerprint(
|
||||
agent_id,
|
||||
settings.acp_agent_command.value(),
|
||||
settings.acp_agent_args.value(),
|
||||
);
|
||||
let launch_fingerprint =
|
||||
acp_launch_fingerprint(agent_id, &provider.command, &provider.args);
|
||||
#[cfg(target_family = "wasm")]
|
||||
let launch_fingerprint = String::new();
|
||||
AgentBackend::Acp(AcpConversationData {
|
||||
provider_id: provider.id.clone(),
|
||||
agent_id: agent_id.to_string(),
|
||||
launch_fingerprint,
|
||||
session_id: None,
|
||||
config_values: settings
|
||||
.acp_agents
|
||||
.value()
|
||||
.iter()
|
||||
.find(|agent| agent.id.eq_ignore_ascii_case(agent_id))
|
||||
.map(|agent| {
|
||||
crate::ai::acp::AcpRuntimeModel::current_config_values(&agent.config_options)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
config_values: crate::ai::acp::AcpRuntimeModel::current_config_values(
|
||||
&provider.config_options,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ fn acp_enabled_with_empty_command_selects_codex_backend() {
|
||||
assert_eq!(
|
||||
conversation.agent_backend(),
|
||||
&AgentBackend::Acp(AcpConversationData {
|
||||
provider_id: "legacy".to_string(),
|
||||
agent_id: "codex".to_string(),
|
||||
launch_fingerprint: crate::ai::acp::acp_launch_fingerprint("codex", "", &[]),
|
||||
session_id: None,
|
||||
|
||||
Reference in New Issue
Block a user