blob: a2c86128b00ff0e3323c2cb7a89c64c66d39a690 [file]
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "src/trace_processor/shell/agent_mode.h"
#include <cstdint>
#include <cstdlib>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include "perfetto/base/status.h"
#include "perfetto/ext/base/string_utils.h"
#include "src/trace_processor/shell/shell_utils.h"
#include "src/trace_processor/util/simple_json_serializer.h"
namespace perfetto::trace_processor::shell {
namespace {
// Environment variables which coding agents set for the commands they run.
// There is no single convention yet: this is the list used by DuckDB's agent
// mode (https://github.com/duckdb/duckdb/pull/26167). AI_AGENT and AGENT are
// generic markers (set by e.g. Claude Code, Goose and Amp), the others are
// specific to one agent.
constexpr const char* kAgentEnvVars[] = {
"AI_AGENT",
"AGENT",
"CLAUDECODE",
"CODEX_CI",
"CODEX_SANDBOX",
"CODEX_THREAD_ID",
"CURSOR_AGENT",
"GEMINI_CLI",
"COPILOT_AGENT",
"COPILOT_CLI",
"COPILOT_AGENT_SESSION_ID",
};
std::optional<std::string> DetectAgentEnvVar() {
for (const char* var : kAgentEnvVars) {
const char* value = getenv(var);
if (value && *value) {
return var;
}
}
return std::nullopt;
}
// Splits an error message into the SQL traceback (if any) and the error
// itself. Tracebacks are laid out like rustc diagnostics (see
// SqlSource::AsTraceback): frames each starting with a `--> ` line, then a
// final line starting with "error: ".
struct SplitError {
std::string_view traceback;
std::string_view error;
std::optional<std::string_view> location;
};
SplitError SplitErrorMessage(std::string_view message) {
constexpr std::string_view kErrorPrefix = "error: ";
SplitError res;
res.error = message;
size_t pos = message.rfind("\nerror: ");
if (pos == std::string_view::npos) {
return res;
}
res.traceback = message.substr(0, pos + 1);
res.error = message.substr(pos + 1 + kErrorPrefix.size());
// The first frame is the SQL the user wrote.
constexpr std::string_view kArrow = "--> ";
size_t arrow = res.traceback.find(kArrow);
if (arrow != std::string_view::npos) {
size_t start = arrow + kArrow.size();
size_t end = res.traceback.find('\n', start);
res.location = res.traceback.substr(start, end - start);
}
return res;
}
// A short next step for the errors agents hit most often.
std::optional<std::string> HintForError(std::string_view error) {
std::string e(error);
if (base::Contains(e, "no such table:")) {
return "list tables and views with `SELECT name FROM perfetto_tables`. "
"Standard library tables need `INCLUDE PERFETTO MODULE` first: "
"`trace_processor help agent` shows how to search for them";
}
if (base::Contains(e, "no such column:")) {
return "`SELECT * FROM <table> LIMIT 0` prints a table's columns";
}
if (base::Contains(e, "no such function:")) {
return "standard library functions need `INCLUDE PERFETTO MODULE` first: "
"`trace_processor help agent` shows how to search for them";
}
return std::nullopt;
}
} // namespace
AgentMode DecideAgentMode(std::optional<bool> force,
bool explicit_format,
bool stdout_is_tty) {
AgentMode mode;
if (force.has_value()) {
mode.enabled = *force;
return mode;
}
if (explicit_format || stdout_is_tty) {
return mode;
}
mode.detected_env_var = DetectAgentEnvVar();
mode.enabled = mode.detected_env_var.has_value();
return mode;
}
void PrintAgentModeNotice(const AgentMode& mode) {
if (!mode.detected_env_var) {
return;
}
PrintStatusLine(
StatusLabelStyle::kHint, "agent mode",
"on as " + *mode.detected_env_var +
" is set: compact tables, capped results, JSON errors. "
"--no-agent turns it off, `trace_processor help agent` explains "
"more");
}
void PrintAgentModeHint() {
PrintStatusLine(StatusLabelStyle::kHint, "hint",
"coding agent? --agent prints compact, capped tables and "
"JSON errors (`trace_processor help agent`)");
}
std::string FormatErrorAsJson(const base::Status& status) {
SplitError split = SplitErrorMessage(status.message());
std::optional<uint32_t> statement;
if (auto payload = status.GetPayload(kFailedStatementPayload); payload) {
statement = base::StringToUInt32(std::string(*payload));
}
std::optional<std::string> hint = HintForError(split.error);
return json::SerializeJson([&](json::JsonValueSerializer&& writer) {
std::move(writer).WriteDict([&](json::JsonDictSerializer& dict) {
dict.AddString("error", split.error);
if (statement) {
dict.AddUint("statement", *statement);
}
if (split.location) {
dict.AddString("location", *split.location);
}
if (!split.traceback.empty()) {
dict.AddString("traceback", split.traceback);
}
if (hint) {
dict.AddString("hint", *hint);
}
});
});
}
} // namespace perfetto::trace_processor::shell