Skip to content
Open
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
9 changes: 8 additions & 1 deletion app/src/ai/agent_sdk/ambient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ use crate::server::server_api::ai::{
ListAgentMessagesRequest, ReadAgentMessageResponse, RunSortBy, RunSortOrder,
SendAgentMessageRequest, SendAgentMessageResponse, SpawnAgentRequest, TaskListFilter,
};
use crate::server::team_scope::RequestTeamScope;
use crate::terminal::shared_session;
use crate::util::time_format::format_approx_duration_from_now_utc;
use crate::workspaces::user_workspaces::UserWorkspaces;
Expand Down Expand Up @@ -393,6 +394,7 @@ impl AmbientAgentRunner {
return;
}
};
let request_team_scope = RequestTeamScope::from_scope(&team_scope);

let mut environment_args = args.environment;
if environment_args.environment.is_none() && !environment_args.no_environment
Expand Down Expand Up @@ -559,7 +561,12 @@ impl AmbientAgentRunner {
let oz_root_url = ChannelState::oz_root_url();
let ai_client_clone = ai_client.clone();
let spawn_future = async move {
let mut stream = Box::pin(spawn_task(request, ai_client_clone, Some(TASK_STATUS_POLLING_DURATION)));
let mut stream = Box::pin(spawn_task(
request,
request_team_scope,
ai_client_clone,
Some(TASK_STATUS_POLLING_DURATION),
));
let mut session_join_info = None;
let mut spawned_task_id = None;

Expand Down
4 changes: 3 additions & 1 deletion app/src/ai/ambient_agents/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::server::retry_strategies::with_bounded_retry;
use crate::server::server_api::ai::{
AIClient, RunFollowupRequest, SpawnAgentRequest, TaskStatusMessage,
};
use crate::server::team_scope::RequestTeamScope;
use crate::terminal::shared_session;

/// How long to poll for the agent to be ready.
Expand Down Expand Up @@ -101,14 +102,15 @@ enum RunPollMode {
/// If `timeout` is `None`, there is no timeout.
pub fn spawn_task(
request: SpawnAgentRequest,
team_scope: RequestTeamScope,
ai_client: Arc<dyn AIClient>,
timeout: Option<Duration>,
) -> impl Stream<Item = Result<AmbientAgentEvent, anyhow::Error>> {
// We can't use try_stream! because of the select! macro invocation.
// See https://github.com/tokio-rs/async-stream/issues/63.
async_stream::stream! {
// First, spawn the ambient agent task.
let (task_id, run_id, at_capacity) = match ai_client.spawn_agent(request).await {
let (task_id, run_id, at_capacity) = match ai_client.spawn_agent(request, team_scope).await {
Ok(response) => (response.task_id, response.run_id, response.at_capacity),
Err(err) => {
yield Err(err);
Expand Down
30 changes: 20 additions & 10 deletions app/src/ai/ambient_agents/spawn_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@ use super::{
};
use crate::ai::agent::UserQueryMode;
use crate::ai::ambient_agents::{AmbientAgentTask, AmbientAgentTaskState};
use crate::server::ids::ServerId;
use crate::server::server_api::ai::{MockAIClient, SpawnAgentResponse, TaskStatusMessage};
use crate::server::team_scope::RequestTeamScope;
use crate::terminal::shared_session;
use crate::workspaces::user_workspaces::{TeamContextForOperation, TeamlessScopeForTest};

fn request_team_scope() -> RequestTeamScope {
RequestTeamScope::from_scope(&TeamlessScopeForTest)
}

fn task_with(
state: AmbientAgentTaskState,
Expand Down Expand Up @@ -717,8 +724,11 @@ async fn poll_retries_transient_429_errors() {

let mut mock = MockAIClient::new();
let call_count = Arc::new(AtomicUsize::new(0));
let expected_scope =
RequestTeamScope::from_scope(&TeamContextForOperation::new_for_test(ServerId::from(43)));

mock.expect_spawn_agent().returning(|_| {
mock.expect_spawn_agent().returning(move |_, team_scope| {
assert_eq!(team_scope, expected_scope);
Ok(SpawnAgentResponse {
task_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(),
run_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
Expand Down Expand Up @@ -759,7 +769,7 @@ async fn poll_retries_transient_429_errors() {
orchestration_handoff: None,
};

let mut stream = Box::pin(spawn_task(request, ai_client, None));
let mut stream = Box::pin(spawn_task(request, expected_scope, ai_client, None));

// First event: TaskSpawned
let event = stream
Expand Down Expand Up @@ -795,7 +805,7 @@ async fn poll_fails_on_permanent_http_error() {

let mut mock = MockAIClient::new();

mock.expect_spawn_agent().returning(|_| {
mock.expect_spawn_agent().returning(|_, _| {
Ok(SpawnAgentResponse {
task_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(),
run_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
Expand Down Expand Up @@ -828,7 +838,7 @@ async fn poll_fails_on_permanent_http_error() {
orchestration_handoff: None,
};

let mut stream = Box::pin(spawn_task(request, ai_client, None));
let mut stream = Box::pin(spawn_task(request, request_team_scope(), ai_client, None));

// First event: TaskSpawned
let event = stream
Expand Down Expand Up @@ -861,7 +871,7 @@ async fn poll_gives_up_after_max_transient_retries() {
let mut mock = MockAIClient::new();
let call_count = Arc::new(AtomicUsize::new(0));

mock.expect_spawn_agent().returning(|_| {
mock.expect_spawn_agent().returning(|_, _| {
Ok(SpawnAgentResponse {
task_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(),
run_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
Expand Down Expand Up @@ -898,7 +908,7 @@ async fn poll_gives_up_after_max_transient_retries() {
orchestration_handoff: None,
};

let mut stream = Box::pin(spawn_task(request, ai_client, None));
let mut stream = Box::pin(spawn_task(request, request_team_scope(), ai_client, None));

// First event: TaskSpawned
let event = stream
Expand Down Expand Up @@ -930,7 +940,7 @@ async fn poll_stops_on_terminal_failure_like_state() {

let mut mock = MockAIClient::new();

mock.expect_spawn_agent().returning(|_| {
mock.expect_spawn_agent().returning(|_, _| {
Ok(SpawnAgentResponse {
task_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(),
run_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
Expand Down Expand Up @@ -962,7 +972,7 @@ async fn poll_stops_on_terminal_failure_like_state() {
orchestration_handoff: None,
};

let mut stream = Box::pin(spawn_task(request, ai_client, None));
let mut stream = Box::pin(spawn_task(request, request_team_scope(), ai_client, None));

let event = stream
.next()
Expand Down Expand Up @@ -1063,7 +1073,7 @@ async fn poll_for_session_join_info_waits_until_link_is_available() {

let call_count = Arc::new(AtomicUsize::new(0));

mock.expect_spawn_agent().returning(|_| {
mock.expect_spawn_agent().returning(|_, _| {
Ok(SpawnAgentResponse {
task_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(),
run_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
Expand Down Expand Up @@ -1109,7 +1119,7 @@ async fn poll_for_session_join_info_waits_until_link_is_available() {
orchestration_handoff: None,
};

let mut stream = Box::pin(spawn_task(request, ai_client, None));
let mut stream = Box::pin(spawn_task(request, request_team_scope(), ai_client, None));

// First event should be TaskSpawned
let event = stream
Expand Down
6 changes: 5 additions & 1 deletion app/src/ai/blocklist/handoff/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ use crate::server::ids::{ServerId, SyncId};
use crate::server::server_api::ai::{
AIClient, AgentConfigSnapshot, AttachmentInput, InitialSnapshotToken, SpawnAgentRequest,
};
use crate::server::team_scope::RequestTeamScope;
use crate::settings::AISettings;
use crate::workspaces::user_workspaces::ResolvedTeamScope;

Expand Down Expand Up @@ -685,6 +686,7 @@ struct SnapshotSettledHandoff {
/// exclusive.
pub fn execute_handoff(
mut pending: PendingHandoff,
team_scope: RequestTeamScope,
ai_client: Arc<dyn AIClient>,
caller_cancellation: Option<oneshot::Receiver<()>>,
materialize_handoff_target: Option<MaterializeHandoffTarget>,
Expand Down Expand Up @@ -715,6 +717,7 @@ pub fn execute_handoff(

Box::pin(execute_validated_handoff(
pending,
team_scope,
ai_client,
caller_cancellation,
materialize_handoff_target,
Expand All @@ -723,6 +726,7 @@ pub fn execute_handoff(

async fn execute_validated_handoff(
pending: PendingHandoff,
team_scope: RequestTeamScope,
ai_client: Arc<dyn AIClient>,
caller_cancellation: Option<oneshot::Receiver<()>>,
materialize_handoff_target: Option<MaterializeHandoffTarget>,
Expand Down Expand Up @@ -799,7 +803,7 @@ async fn execute_validated_handoff(
settled.forked_conversation_id,
settled.initial_snapshot_token,
);
let response = ai_client.spawn_agent(request.clone()).await;
let response = ai_client.spawn_agent(request.clone(), team_scope).await;
if cancellation
.as_mut()
.is_some_and(handoff_cancellation_requested)
Expand Down
37 changes: 29 additions & 8 deletions app/src/ai/blocklist/handoff/pipeline_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ use crate::features::FeatureFlag;
use crate::server::ids::{ServerId, SyncId};
use crate::server::server_api::ServerApiProvider;
use crate::server::server_api::ai::{ForkConversationResponse, MockAIClient, SpawnAgentResponse};
use crate::server::team_scope::RequestTeamScope;
use crate::test_util::add_window_with_terminal;
use crate::test_util::terminal::initialize_app_for_terminal_view;
use crate::workspaces::user_workspaces::TeamlessScopeForTest;

fn request_team_scope() -> RequestTeamScope {
RequestTeamScope::from_scope(&TeamlessScopeForTest)
}

fn task_id() -> AmbientAgentTaskId {
"550e8400-e29b-41d4-a716-446655440000"
Expand Down Expand Up @@ -185,7 +191,8 @@ fn execute_revalidates_current_model_before_returning_future() {
pending.selected_model_id = "custom-router:local:byok".to_owned();
pending.model_is_cloud_runnable = true;

let future = app.update(|ctx| execute_handoff(pending, client, None, None, ctx));
let future = app
.update(|ctx| execute_handoff(pending, request_team_scope(), client, None, None, ctx));
let HandoffCommitOutcome::Rejected { error, .. } = future.await else {
panic!("current invalid model must reject before external work");
};
Expand All @@ -208,7 +215,8 @@ fn execute_revalidates_current_environment_catalog_before_returning_future() {
pending.valid_environment_ids.insert(environment_id);
pending.config.environment_id = Some(environment_id.to_string());

let future = app.update(|ctx| execute_handoff(pending, client, None, None, ctx));
let future = app
.update(|ctx| execute_handoff(pending, request_team_scope(), client, None, None, ctx));
let HandoffCommitOutcome::Rejected { error, .. } = future.await else {
panic!("deleted environment must reject before external work");
};
Expand All @@ -227,7 +235,8 @@ fn execute_revalidates_current_handoff_enablement_before_returning_future() {
let client: Arc<dyn AIClient> = Arc::new(mock);
let pending = pending(client.clone(), None, false, "continue");

let future = app.update(|ctx| execute_handoff(pending, client, None, None, ctx));
let future = app
.update(|ctx| execute_handoff(pending, request_team_scope(), client, None, None, ctx));
let HandoffCommitOutcome::Rejected { error, .. } = future.await else {
panic!("disabled handoff must reject before external work");
};
Expand Down Expand Up @@ -764,6 +773,11 @@ async fn fork_materialization_precedes_exactly_one_spawn() {
let materialized = Arc::new(AtomicBool::new(false));
let spawn_count = Arc::new(AtomicUsize::new(0));
let observed_request = Arc::new(Mutex::new(None));
let expected_scope = RequestTeamScope::from_scope(
&crate::workspaces::user_workspaces::TeamContextForOperation::new_for_test(ServerId::from(
42,
)),
);
let mut mock = MockAIClient::new();
mock.expect_fork_conversation()
.times(1)
Expand All @@ -779,8 +793,9 @@ async fn fork_materialization_precedes_exactly_one_spawn() {
let materialized = materialized.clone();
let spawn_count = spawn_count.clone();
let observed_request = observed_request.clone();
move |request| {
move |request, team_scope| {
assert!(materialized.load(Ordering::SeqCst));
assert_eq!(team_scope, expected_scope);
spawn_count.fetch_add(1, Ordering::SeqCst);
*observed_request.lock().expect("request lock") = Some(request);
Ok(SpawnAgentResponse {
Expand Down Expand Up @@ -814,6 +829,7 @@ async fn fork_materialization_precedes_exactly_one_spawn() {
true,
"",
),
expected_scope,
client,
None,
Some(materialize),
Expand Down Expand Up @@ -856,7 +872,7 @@ async fn fresh_launch_skips_fork_and_materializes_before_spawn() {
mock.expect_fork_conversation().times(0);
mock.expect_spawn_agent().times(1).returning({
let materialized = materialized.clone();
move |request| {
move |request, _| {
assert!(materialized.load(Ordering::SeqCst));
assert!(request.conversation_id.is_none());
Ok(SpawnAgentResponse {
Expand All @@ -882,6 +898,7 @@ async fn fresh_launch_skips_fork_and_materializes_before_spawn() {

let outcome = execute_validated_handoff(
pending(client.clone(), None, false, "new task"),
request_team_scope(),
client,
None,
Some(materialize),
Expand Down Expand Up @@ -912,6 +929,7 @@ async fn cancellation_after_materialization_stops_before_spawn() {

let outcome = execute_validated_handoff(
pending(client.clone(), None, false, "new task"),
request_team_scope(),
client,
None,
Some(materialize),
Expand All @@ -928,7 +946,7 @@ async fn cancellation_during_spawn_cancels_the_created_task() {
mock.expect_fork_conversation().times(0);
mock.expect_spawn_agent().times(1).returning({
let cancel = cancel.clone();
move |_| {
move |_, _| {
cancel
.lock()
.expect("cancel sender lock")
Expand Down Expand Up @@ -958,6 +976,7 @@ async fn cancellation_during_spawn_cancels_the_created_task() {

let outcome = execute_validated_handoff(
pending(client.clone(), None, false, "new task"),
request_team_scope(),
client,
None,
Some(materialize),
Expand All @@ -984,7 +1003,7 @@ async fn snapshot_failure_degrades_to_spawn_without_token() {
mock.expect_upload_local_handoff_snapshot()
.times(1)
.returning(|_| Err(anyhow::anyhow!("snapshot unavailable")));
mock.expect_spawn_agent().times(1).returning(|request| {
mock.expect_spawn_agent().times(1).returning(|request, _| {
assert!(request.initial_snapshot_token.is_none());
Ok(SpawnAgentResponse {
task_id: task_id(),
Expand All @@ -996,7 +1015,8 @@ async fn snapshot_failure_degrades_to_spawn_without_token() {
let mut pending = pending(client.clone(), None, false, "continue");
pending.source_paths = vec![path];

let outcome = execute_validated_handoff(pending, client, None, None).await;
let outcome =
execute_validated_handoff(pending, request_team_scope(), client, None, None).await;
let HandoffCommitOutcome::Created(created) = outcome else {
panic!("snapshot failure should not fail the handoff");
};
Expand All @@ -1016,6 +1036,7 @@ async fn caller_cancellation_stops_before_spawn() {

let outcome = execute_validated_handoff(
pending(client.clone(), None, false, "new task"),
request_team_scope(),
client,
Some(cancellation),
None,
Expand Down
8 changes: 6 additions & 2 deletions app/src/pane_group/pane/terminal_pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ use crate::pane_group::{self, Direction, PaneGroup};
use crate::persistence::{BlockCompleted, ModelEvent};
#[cfg(not(target_family = "wasm"))]
use crate::server::server_api::ServerApiProvider;
use crate::server::team_scope::RequestTeamScope;
use crate::session_management::SessionNavigationData;
use crate::terminal::cli_agent_sessions::CLIAgentSessionsModel;
use crate::terminal::general_settings::GeneralSettings;
Expand All @@ -68,7 +69,8 @@ use crate::view_components::ToastFlavor;
use crate::workspace::sync_inputs::SyncedInputState;
use crate::workspace::{PaneViewLocator, WorkspaceRegistry};
#[cfg(not(target_family = "wasm"))]
use crate::workspaces::user_workspaces::{ResolvedTeamScope, UserWorkspaces};
use crate::workspaces::user_workspaces::ResolvedTeamScope;
use crate::workspaces::user_workspaces::UserWorkspaces;
#[cfg(not(target_family = "wasm"))]
use crate::{
pane_group::child_agent::{
Expand Down Expand Up @@ -1986,6 +1988,8 @@ fn launch_remote_child(
return None;
}
};
let team_scope =
RequestTeamScope::from_scope(&UserWorkspaces::as_ref(ctx).team_context_for_operation(ctx));

new_terminal_view.update(ctx, |terminal_view, ctx| {
terminal_view.enter_agent_view(
Expand All @@ -1997,7 +2001,7 @@ fn launch_remote_child(
if let Some(ambient_agent_view_model) = terminal_view.ambient_agent_view_model() {
ambient_agent_view_model.update(ctx, |model, ctx| {
model.set_conversation_id(Some(conversation_id));
model.spawn_agent_with_request(prepared.spawn_request, ctx);
model.spawn_agent_with_request(prepared.spawn_request, team_scope, ctx);
});
} else {
report_error!("Remote StartAgent child pane missing ambient agent view model");
Expand Down
Loading
Loading