blob: 8ee9aa2eda699d2d15a4d0e6a9ddbe44e707de9b [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/actor/actor_task.h"
#include <memory>
#include <ostream>
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/state_transitions.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/actor/actor_keyed_service.h"
#include "chrome/browser/actor/actor_metrics.h"
#include "chrome/browser/actor/execution_engine.h"
#include "chrome/browser/actor/ui/event_dispatcher.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/actor.mojom-data-view.h"
#include "chrome/common/actor.mojom-forward.h"
#include "chrome/common/actor/action_result.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/page.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "ui/gfx/geometry/size.h"
namespace actor {
namespace {
bool IsStateActive(ActorTask::State state) {
return (state == ActorTask::State::kCreated ||
state == ActorTask::State::kActing ||
state == ActorTask::State::kReflecting);
}
void SetFocusState(content::WebContents* contents,
std::optional<bool> focus_state) {
if (content::RenderWidgetHostView* view =
contents->GetRenderWidgetHostView()) {
if (content::RenderWidgetHost* host = view->GetRenderWidgetHost()) {
// If a new state was provided, use that. Otherwise us the state from
// the view.
bool new_state = focus_state.value_or(view->HasFocus());
if (new_state) {
host->Focus();
} else {
host->Blur();
}
}
}
}
} // namespace
ActorTask::ActingTabState::ActingTabState(ActorTask* task) : task(task) {}
ActorTask::ActingTabState::~ActingTabState() = default;
void ActorTask::ActingTabState::SetContents(content::WebContents* contents) {
Observe(contents);
}
void ActorTask::ActingTabState::PrimaryPageChanged(content::Page& page) {
content::WebContents* contents =
content::WebContents::FromRenderFrameHost(&page.GetMainDocument());
if (task->IsActive()) {
task->DidContentsBecomeActive(this, contents);
} else {
task->DidContentsBecomeInactive(this, contents);
}
}
ActorTask::ActorTask(Profile* profile,
std::unique_ptr<ExecutionEngine> execution_engine,
std::unique_ptr<ui::UiEventDispatcher> ui_event_dispatcher,
webui::mojom::TaskOptionsPtr options)
: profile_(profile),
execution_engine_(std::move(execution_engine)),
ui_event_dispatcher_(std::move(ui_event_dispatcher)),
title_(options && options->title.has_value() ? options->title.value()
: ""),
ui_weak_ptr_factory_(ui_event_dispatcher_.get()) {}
ActorTask::~ActorTask() = default;
void ActorTask::SetId(base::PassKey<ActorKeyedService>, TaskId id) {
id_ = id;
}
void ActorTask::SetIdForTesting(int id) {
id_ = TaskId(id);
}
ExecutionEngine* ActorTask::GetExecutionEngine() const {
return execution_engine_.get();
}
ActorTask::State ActorTask::GetState() const {
return state_;
}
void ActorTask::SetState(State new_state) {
using enum State;
VLOG(1) << "ActorTask state change: " << state_ << " -> " << new_state;
#if DCHECK_IS_ON()
static const base::NoDestructor<base::StateTransitions<State>>
allowed_transitions(base::StateTransitions<State>(
{{kCreated,
{kActing, kReflecting, kPausedByActor, kPausedByUser, kCancelled,
kFinished}},
{kActing,
{kReflecting, kPausedByActor, kPausedByUser, kCancelled,
kFinished}},
{kReflecting,
{kActing, kPausedByActor, kPausedByUser, kCancelled, kFinished}},
{kPausedByActor, {kReflecting, kCancelled, kFinished}},
{kPausedByUser, {kReflecting, kCancelled, kFinished}},
{kCancelled, {}},
{kFinished, {}}}));
if (new_state != state_) {
DCHECK_STATE_TRANSITION(allowed_transitions,
/*old_state=*/state_,
/*new_state=*/new_state);
}
#endif // DCHECK_IS_ON()
State old_state = state_;
const base::TimeDelta old_state_duration = current_state_timer_.Elapsed();
// If the old state was active, add its duration to the total active time for
// the task.
if (IsActive()) {
total_active_time_ += old_state_duration;
}
// Record granular state transition histograms.
RecordActorTaskStateTransitionDuration(old_state_duration, old_state);
RecordActorTaskStateTransitionActionCount(actions_in_current_state_,
old_state, new_state);
state_ = new_state;
current_state_timer_ = base::ElapsedTimer();
actions_in_current_state_ = 0;
if (IsStateActive(new_state) && !IsStateActive(old_state)) {
for (const auto& [tab, _] : acting_tabs_) {
DidTabBecomeActive(tab);
}
} else if (!IsStateActive(new_state) && IsStateActive(old_state)) {
for (const auto& [tab, _] : acting_tabs_) {
DidTabBecomeInactive(tab);
}
}
ui_event_dispatcher_->OnActorTaskSyncChange(
ui::UiEventDispatcher::ChangeTaskState{
.task_id = id_, .old_state = old_state, .new_state = new_state});
actor::ActorKeyedService::Get(profile_)->NotifyTaskStateChanged(*this);
// If the state is to be finished/cancelled record a histogram.
if (state_ == kFinished) {
base::UmaHistogramCounts1000("Actor.Task.Count.Completed",
total_number_of_actions_);
base::UmaHistogramLongTimes100("Actor.Task.Duration.Completed",
total_active_time_);
} else if (state_ == kCancelled) {
base::UmaHistogramCounts1000("Actor.Task.Count.Cancelled",
total_number_of_actions_);
base::UmaHistogramLongTimes100("Actor.Task.Duration.Cancelled",
total_active_time_);
}
}
void ActorTask::Act(std::vector<std::unique_ptr<ToolRequest>>&& actions,
ActCallback callback) {
if (state_ == State::kPausedByActor) {
std::move(callback).Run(MakeResult(mojom::ActionResultCode::kTaskPaused),
std::nullopt, {});
return;
}
if (IsStopped()) {
std::move(callback).Run(MakeResult(mojom::ActionResultCode::kTaskWentAway),
std::nullopt, {});
return;
}
SetState(State::kActing);
actions_in_current_state_ += actions.size();
total_number_of_actions_ += actions.size();
execution_engine_->Act(
std::move(actions),
base::BindOnce(&ActorTask::OnFinishedAct, weak_ptr_factory_.GetWeakPtr(),
std::move(callback)));
}
void ActorTask::OnFinishedAct(
ActCallback callback,
mojom::ActionResultPtr result,
std::optional<size_t> index_of_failed_action,
std::vector<ActionResultWithLatencyInfo> action_results) {
if (state_ != State::kActing) {
std::move(callback).Run(MakeErrorResult(), std::nullopt, {});
return;
}
SetState(State::kReflecting);
std::move(callback).Run(std::move(result), index_of_failed_action,
std::move(action_results));
}
void ActorTask::Stop(bool success) {
if (execution_engine_) {
execution_engine_->CancelOngoingActions(
mojom::ActionResultCode::kTaskWentAway);
}
end_time_ = base::Time::Now();
// Remove all the tabs from the task.
while (!acting_tabs_.empty()) {
RemoveTab(acting_tabs_.begin()->first);
}
if (success) {
SetState(State::kFinished);
} else {
SetState(State::kCancelled);
}
}
void ActorTask::Pause(bool from_actor) {
if (GetState() == State::kFinished) {
return;
}
if (execution_engine_) {
execution_engine_->CancelOngoingActions(
mojom::ActionResultCode::kTaskPaused);
}
if (from_actor) {
SetState(State::kPausedByActor);
} else {
SetState(State::kPausedByUser);
}
}
void ActorTask::Resume() {
// Only resume from a paused state.
if (!IsPaused()) {
return;
}
SetState(State::kReflecting);
}
bool ActorTask::IsPaused() const {
return (GetState() == State::kPausedByActor) ||
(GetState() == State::kPausedByUser);
}
bool ActorTask::IsStopped() const {
return (GetState() == State::kFinished) || (GetState() == State::kCancelled);
}
bool ActorTask::IsActive() const {
return IsStateActive(state_);
}
base::Time ActorTask::GetEndTime() const {
return end_time_;
}
void ActorTask::AddTab(tabs::TabHandle tab_handle, AddTabCallback callback) {
if (!IsActive()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
std::move(callback),
MakeResult(IsPaused() ? mojom::ActionResultCode::kTaskPaused
: mojom::ActionResultCode::kTaskWentAway)));
return;
}
if (acting_tabs_.contains(tab_handle)) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), MakeOkResult()));
return;
}
acting_tabs_.emplace(tab_handle, std::make_unique<ActingTabState>(this));
DidTabBecomeActive(tab_handle);
// Notify the UI of the new tab.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&ui::UiEventDispatcher::OnActorTaskAsyncChange,
ui_weak_ptr_factory_.GetWeakPtr(),
ui::UiEventDispatcher::AddTab{
.task_id = id_, .handle = tab_handle},
std::move(callback)));
}
// TODO(crbug.com/450524344): Add a test for this. Note that at this point the
// tab is not yet associated with the new_contents.
void ActorTask::HandleDiscardContents(tabs::TabInterface* tab,
content::WebContents* old_contents,
content::WebContents* new_contents) {
CHECK(acting_tabs_.contains(tab->GetHandle()));
if (!IsActive()) {
// The observer should only be attached when we're active.
NOTREACHED(base::NotFatalUntil::M145);
return;
}
ActingTabState* state = acting_tabs_[tab->GetHandle()].get();
DidContentsBecomeActive(state, new_contents);
}
void ActorTask::RemoveTab(tabs::TabHandle tab_handle) {
if (IsActingOnTab(tab_handle)) {
DidTabBecomeInactive(tab_handle);
}
auto num_removed = acting_tabs_.erase(tab_handle);
if (num_removed > 0) {
// Notify the UI of the tab removal.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&ui::UiEventDispatcher::OnActorTaskSyncChange,
ui_weak_ptr_factory_.GetWeakPtr(),
ui::UiEventDispatcher::RemoveTab{
.task_id = id_, .handle = tab_handle}));
}
}
void ActorTask::OnTabWillDetach(tabs::TabInterface* tab,
tabs::TabInterface::DetachReason reason) {
if (reason != tabs::TabInterface::DetachReason::kDelete) {
return;
}
if (!HasTab(tab->GetHandle())) {
return;
}
// TODO(mcnee): This will also stop a task that's paused. Should we leave
// paused tasks as is?
actor::ActorKeyedService::Get(profile_)->StopTask(id(), /*success=*/false);
}
bool ActorTask::HasTab(tabs::TabHandle tab) const {
return acting_tabs_.contains(tab);
}
bool ActorTask::IsActingOnTab(tabs::TabHandle tab) const {
if (!IsActive()) {
return false;
}
return HasTab(tab);
}
absl::flat_hash_set<tabs::TabHandle> ActorTask::GetLastActedTabs() const {
// TODO(crbug.com/420669167): Currently the client only acts on a single tab
// so we can return the full set but with multi-tab this will need to be
// smarter about which tabs are relevant to the last/current action.
return GetTabs();
}
absl::flat_hash_set<tabs::TabHandle> ActorTask::GetTabs() const {
absl::flat_hash_set<tabs::TabHandle> handles;
for (const auto& [handle, _] : acting_tabs_) {
handles.insert(handle);
}
return handles;
}
void ActorTask::DidTabBecomeActive(tabs::TabHandle handle) {
DCHECK(IsActingOnTab(handle));
tabs::TabInterface* tab = handle.Get();
if (!tab) {
// This happens in unitttests.
return;
}
ActingTabState* state = acting_tabs_[handle].get();
content::WebContents* contents = tab->GetContents();
if (!contents) {
return;
}
state->will_detach_subscription = tab->RegisterWillDetach(base::BindRepeating(
&ActorTask::OnTabWillDetach, weak_ptr_factory_.GetWeakPtr()));
// TODO(crbug.com/450524344)): Add a test for discarded content.
state->content_discarded_subscription =
tab->RegisterWillDiscardContents(base::BindRepeating(
&ActorTask::HandleDiscardContents, weak_ptr_factory_.GetWeakPtr()));
DidContentsBecomeActive(state, contents);
}
void ActorTask::DidContentsBecomeActive(ActorTask::ActingTabState* state,
content::WebContents* contents) {
SetFocusState(contents, true);
state->SetContents(contents);
state->actuation_runner =
contents->IncrementCapturerCount(gfx::Size(),
/*stay_hidden=*/false,
/*stay_awake=*/true,
/*is_activity=*/true);
}
void ActorTask::DidTabBecomeInactive(tabs::TabHandle handle) {
// Note that the state_ may be kActive if we are just removing this tab.
DCHECK(acting_tabs_.contains(handle));
tabs::TabInterface* tab = handle.Get();
if (!tab) {
// This happens in unitttests.
return;
}
ActingTabState* state = acting_tabs_[handle].get();
content::WebContents* contents = tab->GetContents();
if (!contents) {
return;
}
// Reset focus and remove observers.
SetFocusState(contents, std::nullopt);
state->will_detach_subscription = {};
state->SetContents(nullptr);
state->content_discarded_subscription = {};
DidContentsBecomeInactive(state, contents);
}
void ActorTask::DidContentsBecomeInactive(ActorTask::ActingTabState* state,
content::WebContents* contents) {
SetFocusState(contents, std::nullopt);
state->SetContents(nullptr);
// Triggers the ScopedClosureRunner's destructor (via std::optional's
// destructor), which automatically calls DecrementCapturerCount on the
// WebContents.
state->actuation_runner = {};
}
std::string ToString(const ActorTask::State& state) {
using enum ActorTask::State;
switch (state) {
case kCreated:
return "Created";
case kActing:
return "Acting";
case kReflecting:
return "Reflecting";
case kPausedByActor:
return "PausedByActor";
case kPausedByUser:
return "PausedByUser";
case kCancelled:
return "Cancelled";
case kFinished:
return "Finished";
}
}
std::ostream& operator<<(std::ostream& os, const ActorTask::State& state) {
return os << ToString(state);
}
} // namespace actor