Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,7 @@ codex_notifications = []
cloud_mode_setup_v2 = ["cloud_mode"]
cloud_mode_input_v2 = ["cloud_mode"]
configurable_context_window = []
handoff_cloud_cloud = ["cloud_mode_setup_v2"]

[package.metadata.bundle.bin.warp-oss]
category = "public.app-category.developer-tools"
Expand Down
49 changes: 27 additions & 22 deletions app/src/ai/agent_conversations_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,13 +528,15 @@ impl ConversationOrTask<'_> {
/// Returns the session ID for tasks, if we have one.
pub fn session_id(&self) -> Option<SessionId> {
match self {
ConversationOrTask::Task(task) => task.session_id.as_ref().and_then(|s| {
let session_id = s.parse::<SessionId>();
if let Err(ref e) = session_id {
log::warn!("Failed to parse shared session ID: {e}");
}
session_id.ok()
}),
ConversationOrTask::Task(task) => {
task.active_run_execution().session_id.and_then(|s| {
let session_id = s.parse::<SessionId>();
if let Err(ref e) = session_id {
log::warn!("Failed to parse shared session ID: {e}");
}
session_id.ok()
})
}
ConversationOrTask::Conversation(_) => None,
}
}
Expand Down Expand Up @@ -625,11 +627,12 @@ impl ConversationOrTask<'_> {
fn link_preference(&self) -> LinkPreference {
match self {
ConversationOrTask::Task(task) => {
let run_execution = task.active_run_execution();
// Always open session link if there's a live session.
// Without cloud conversations, also open session link as long as it's not expired.
// With cloud conversations, even if the link is not expired, we load conversation
// data from graphql as long as the session isn't live.
if task.is_sandbox_running
if run_execution.is_sandbox_running
|| (!FeatureFlag::CloudConversations.is_enabled()
&& self.get_session_status() != Some(SessionStatus::Expired))
{
Expand All @@ -648,14 +651,16 @@ impl ConversationOrTask<'_> {
pub fn session_or_conversation_link(&self, app: &AppContext) -> Option<String> {
match self.link_preference() {
LinkPreference::Session => match self {
ConversationOrTask::Task(task) => task.session_link.clone(),
ConversationOrTask::Task(task) => task
.active_run_execution()
.session_link
.map(ToString::to_string),
ConversationOrTask::Conversation(_) => None,
},
LinkPreference::Conversation => match self {
ConversationOrTask::Task(task) => task
.conversation_id
.as_ref()
.map(|id| ServerConversationToken::new(id.clone()).conversation_link()),
.conversation_id()
.map(|id| ServerConversationToken::new(id.to_string()).conversation_link()),
ConversationOrTask::Conversation(conversation) => {
let history_model = BlocklistAIHistoryModel::as_ref(app);
history_model
Expand All @@ -680,7 +685,7 @@ impl ConversationOrTask<'_> {
if FeatureFlag::CloudConversations.is_enabled() {
return match self {
ConversationOrTask::Task(task) => {
if task.session_link.is_some() {
if task.active_run_execution().session_link.is_some() {
Some(SessionStatus::Available)
} else {
Some(SessionStatus::Unavailable)
Expand All @@ -691,7 +696,7 @@ impl ConversationOrTask<'_> {
}
match self {
ConversationOrTask::Task(task) => {
if task.session_id.is_some() {
if task.active_run_execution().session_id.is_some() {
Some(SessionStatus::Available)
} else if (Utc::now() - task.created_at) > SESSION_EXPIRATION_TIME {
Some(SessionStatus::Expired)
Expand Down Expand Up @@ -778,16 +783,16 @@ impl ConversationOrTask<'_> {
self.session_id()
.map(|session_id| WorkspaceAction::OpenAmbientAgentSession {
session_id,
task_id: task.task_id,
task_id: task.run_id(),
})
}
ConversationOrTask::Conversation(_) => None,
},
LinkPreference::Conversation => match self {
ConversationOrTask::Task(task) => task.conversation_id.as_ref().map(|id| {
ConversationOrTask::Task(task) => task.conversation_id().map(|id| {
WorkspaceAction::OpenConversationTranscriptViewer {
conversation_id: ServerConversationToken::new(id.clone()),
ambient_agent_task_id: Some(task.task_id),
conversation_id: ServerConversationToken::new(id.to_string()),
ambient_agent_task_id: Some(task.run_id()),
}
}),
ConversationOrTask::Conversation(metadata) => {
Expand Down Expand Up @@ -1124,7 +1129,7 @@ impl AgentConversationsModel {
// Collect all conversation IDs from tasks
let task_conversation_ids: HashSet<String> = tasks
.iter()
.filter_map(|task| task.conversation_id.clone())
.filter_map(|task| task.conversation_id().map(str::to_string))
.collect();

// Build a set of conversation IDs we already have
Expand Down Expand Up @@ -1381,11 +1386,11 @@ impl AgentConversationsModel {
history_model: &BlocklistAIHistoryModel,
) -> Option<AIConversationId> {
history_model
.conversation_id_for_agent_id(&task.task_id.to_string())
.conversation_id_for_agent_id(&task.run_id().to_string())
.or_else(|| {
task.conversation_id.as_ref().and_then(|conversation_id| {
task.conversation_id().and_then(|conversation_id| {
history_model.find_conversation_id_by_server_token(
&ServerConversationToken::new(conversation_id.clone()),
&ServerConversationToken::new(conversation_id.to_string()),
)
})
})
Expand Down
12 changes: 6 additions & 6 deletions app/src/ai/ambient_agents/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,22 @@ pub struct SessionJoinInfo {

impl SessionJoinInfo {
pub fn from_task(task: &AmbientAgentTask) -> Option<Self> {
let run_execution = task.active_run_execution();
// Prefer the server-provided session_link when available; it is a better signal
// that a session-sharing link is ready to be shown to the user.
if let Some(link) = task.session_link.as_ref().filter(|l| !l.is_empty()) {
let session_id = task
if let Some(link) = run_execution.session_link {
let session_id = run_execution
.session_id
.as_deref()
.and_then(|s| SessionId::from_str(s).ok());
.and_then(|session_id| SessionId::from_str(session_id).ok());
return Some(Self {
session_id,
session_link: link.to_string(),
});
}

// Fallback to constructing a link from the session_id.
if let Some(session_id_str) = task.session_id.as_deref() {
if let Ok(session_id) = SessionId::from_str(session_id_str) {
if let Some(session_id) = run_execution.session_id {
if let Ok(session_id) = SessionId::from_str(session_id) {
return Some(Self {
session_id: Some(session_id),
session_link: shared_session::join_link(&session_id),
Expand Down
31 changes: 31 additions & 0 deletions app/src/ai/ambient_agents/spawn_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ use std::sync::{
};

use chrono::Utc;
use session_sharing_protocol::common::SessionId;

use crate::ai::ambient_agents::{AmbientAgentTask, AmbientAgentTaskState};
use crate::server::server_api::ai::{MockAIClient, SpawnAgentResponse};
use crate::terminal::shared_session;

use super::{spawn_task, AmbientAgentEvent, SessionJoinInfo};

Expand Down Expand Up @@ -109,6 +111,35 @@ fn session_join_info_prefers_session_link_and_tolerates_missing_session_id() {
assert!(join_info.session_id.is_none());
}

#[test]
fn session_join_info_falls_back_to_session_id() {
let session_id = SessionId::new();
let task = task_with(
AmbientAgentTaskState::InProgress,
Some(session_id.to_string()),
None,
);

let join_info = SessionJoinInfo::from_task(&task).expect("expected join info");

assert_eq!(join_info.session_id, Some(session_id));
assert_eq!(
join_info.session_link,
shared_session::join_link(&session_id)
);
}

#[test]
fn session_join_info_ignores_empty_link_and_invalid_session_id() {
let task = task_with(
AmbientAgentTaskState::InProgress,
Some("not-a-session-id".to_string()),
Some(String::new()),
);

assert_eq!(SessionJoinInfo::from_task(&task), None);
}

#[tokio::test]
async fn poll_for_session_join_info_waits_until_link_is_available() {
use futures::StreamExt;
Expand Down
31 changes: 28 additions & 3 deletions app/src/ai/ambient_agents/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,14 @@ pub struct AmbientAgentTask {
pub children: Vec<String>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RunExecution<'a> {
pub session_id: Option<&'a str>,
pub session_link: Option<&'a str>,
pub request_usage: Option<&'a RequestUsage>,
pub is_sandbox_running: bool,
}

/// Represents a single attachment input from the client (e.g., file upload)
#[derive(Clone, Debug, Serialize)]
pub struct AttachmentInput {
Expand All @@ -296,10 +304,27 @@ pub struct TaskAttachment {
}

impl AmbientAgentTask {
pub fn run_id(&self) -> AmbientAgentTaskId {
self.task_id
}

pub fn conversation_id(&self) -> Option<&str> {
self.conversation_id.as_deref()
}

pub fn active_run_execution(&self) -> RunExecution<'_> {
RunExecution {
session_id: self.session_id.as_deref(),
session_link: self.session_link.as_deref().filter(|link| !link.is_empty()),
request_usage: self.request_usage.as_ref(),
is_sandbox_running: self.is_sandbox_running,
}
}

/// Total credits used (inference + compute).
pub fn credits_used(&self) -> Option<f32> {
self.request_usage
.as_ref()
self.active_run_execution()
.request_usage
.map(|u| (u.inference_cost.unwrap_or(0.0) + u.compute_cost.unwrap_or(0.0)) as f32)
}

Expand All @@ -317,7 +342,7 @@ impl AmbientAgentTask {

/// Returns true if the underlying session for the ambient agent is no longer running.
pub fn is_no_longer_running(&self) -> bool {
!self.is_sandbox_running && !self.state.is_working()
!self.active_run_execution().is_sandbox_running && !self.state.is_working()
}
}

Expand Down
12 changes: 6 additions & 6 deletions app/src/ai/conversation_details_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,11 @@ impl ConversationDetailsData {
fn directory_for_task(task: &AmbientAgentTask, app: &AppContext) -> Option<String> {
let history_model = BlocklistAIHistoryModel::as_ref(app);
let conversation_id = history_model
.conversation_id_for_agent_id(&task.task_id.to_string())
.conversation_id_for_agent_id(&task.run_id().to_string())
.or_else(|| {
task.conversation_id.as_ref().and_then(|conversation_id| {
task.conversation_id().and_then(|conversation_id| {
history_model.find_conversation_id_by_server_token(
&ServerConversationToken::new(conversation_id.clone()),
&ServerConversationToken::new(conversation_id.to_string()),
)
})
})?;
Expand Down Expand Up @@ -325,7 +325,7 @@ impl ConversationDetailsData {
.as_ref()
.and_then(|config| config.environment_id.clone());

let credits = task.request_usage.as_ref().and_then(|u| {
let credits = task.active_run_execution().request_usage.and_then(|u| {
Some(CreditsInfo::AmbientConversation {
inference: u.inference_cost? as f32,
compute: u.compute_cost? as f32,
Expand All @@ -348,12 +348,12 @@ impl ConversationDetailsData {

ConversationDetailsData {
mode: PanelMode::Task {
task_id: Some(task.task_id),
task_id: Some(task.run_id()),
directory: Self::directory_for_task(task, app),
display_status: Some(AgentRunDisplayStatus::from_task(task, app)),
error_message,
environment_id,
conversation_id: task.conversation_id.clone(),
conversation_id: task.conversation_id().map(str::to_string),
},
title: task.title.clone(),
created_at: Some(task.created_at.with_timezone(&Local)),
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2871,6 +2871,8 @@ pub fn enabled_features() -> HashSet<FeatureFlag> {
FeatureFlag::CloudModeInputV2,
#[cfg(feature = "configurable_context_window")]
FeatureFlag::ConfigurableContextWindow,
#[cfg(feature = "handoff_cloud_cloud")]
FeatureFlag::HandoffCloudCloud,
]);

flags
Expand Down
24 changes: 24 additions & 0 deletions app/src/server/server_api/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ pub struct SpawnAgentRequest {
pub referenced_attachments: Vec<String>,
}

#[derive(Debug, Clone, serde::Serialize)]
pub struct RunFollowupRequest {
pub message: String,
}

// --- Orchestrations V2 messaging types ---

#[derive(Debug, Clone, serde::Serialize)]
Expand Down Expand Up @@ -657,6 +662,10 @@ pub(crate) fn build_list_agent_runs_url(limit: i32, filter: &TaskListFilter) ->
url
}

pub(crate) fn build_run_followup_url(run_id: &AmbientAgentTaskId) -> String {
format!("agent/runs/{run_id}/followups")
}

struct ListRunsResponse {
runs: Vec<AmbientAgentTask>,
}
Expand Down Expand Up @@ -825,6 +834,12 @@ pub trait AIClient: 'static + Send + Sync {
task_id: &AmbientAgentTaskId,
) -> anyhow::Result<serde_json::Value, anyhow::Error>;

async fn submit_run_followup(
&self,
run_id: &AmbientAgentTaskId,
request: RunFollowupRequest,
) -> anyhow::Result<(), anyhow::Error>;

async fn get_scheduled_agent_history(
&self,
schedule_id: &str,
Expand Down Expand Up @@ -1454,6 +1469,15 @@ impl AIClient for ServerApi {
Ok(response)
}

async fn submit_run_followup(
&self,
run_id: &AmbientAgentTaskId,
request: RunFollowupRequest,
) -> anyhow::Result<(), anyhow::Error> {
self.post_public_api_unit(&build_run_followup_url(run_id), &request)
.await
}

async fn get_scheduled_agent_history(
&self,
schedule_id: &str,
Expand Down
Loading
Loading