use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use argonauts_core::Paths;
use argonauts_core::behaviors::Behavior;
use argonauts_core::crew_stats::StatsRole;
use serde_json::Value;
use time::OffsetDateTime;
use crate::behaviors::actions::Action;
pub(crate) use crate::behaviors::actions::Outcome;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Recorded {
pub key: String,
pub run: Option<String>,
pub cli: String,
pub model: Option<String>,
pub role: String,
pub agent: String,
pub workflow: String,
pub project: String,
pub at: OffsetDateTime,
pub nth: usize,
pub of: usize,
pub tool: String,
pub what: String,
pub behavior: Behavior,
pub in_task: bool,
pub outcome: Option<Outcome>,
pub place: Option<String>,
pub run_shows: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct Records {
pub actions: Vec<Recorded>,
pub runs: usize,
pub conversations: usize,
}
pub(crate) fn project_name(state: &Path) -> String {
let name = state
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
crate::behaviors::records::project_name(&name).to_owned()
}
pub(crate) fn records(
paths: &Paths,
only: Option<&Path>,
since: Option<OffsetDateTime>,
) -> Records {
let only = only
.and_then(Path::file_name)
.map(|name| name.to_string_lossy().into_owned());
let actions = crate::behaviors::records::Records::of(paths)
.actions(since)
.into_iter()
.filter(|action| only.as_ref().is_none_or(|repo| action.repo == *repo))
.collect();
of_actions(actions)
}
pub(crate) fn of_actions(actions: Vec<Action>) -> Records {
let sources = |conversation: bool| {
actions
.iter()
.filter(|action| action.conversation == conversation)
.map(|action| (action.repo.as_str(), action.run.as_str()))
.collect::<BTreeSet<_>>()
.len()
};
let (runs, conversations) = (sources(false), sources(true));
let mut actions: Vec<Recorded> = actions.into_iter().map(recorded).collect();
number_within_sessions(&mut actions);
Records {
actions,
runs,
conversations,
}
}
const NO_WORKFLOW: &str = "no workflow";
fn recorded(action: Action) -> Recorded {
let project = action.project().to_owned();
let (run, workflow, place) = if action.conversation {
(None, "conversation".to_owned(), Some("in your folder"))
} else {
let place = action.task.is_some().then_some("in the run's worktree");
(
Some(action.run.clone()),
action.workflow.unwrap_or_else(|| NO_WORKFLOW.to_owned()),
place,
)
};
let run_shows = action.note.or_else(|| {
Some(
match action.outcome? {
Outcome::Merged => "Its task merged.",
Outcome::SentBack => "Review sent its task back.",
Outcome::Dropped => "Its task was dropped.",
}
.to_owned(),
)
});
Recorded {
key: format!(
"{}/{}/{}/{}",
action.repo, action.run, action.session, action.call
),
run,
cli: action.cli,
model: action.model,
role: role_words(action.role).to_owned(),
agent: action.agent,
workflow,
project,
at: action.at,
nth: 0,
of: 0,
tool: action.tool,
what: match &action.input {
Value::Null => action.what,
input => input_words(input),
},
behavior: action.behavior,
in_task: action.task.is_some(),
outcome: action.outcome,
place: place.map(str::to_owned),
run_shows,
}
}
fn role_words(role: StatsRole) -> &'static str {
match role {
StatsRole::Main => "main agent",
StatsRole::Worker => "builder",
other => other.as_str(),
}
}
fn number_within_sessions(actions: &mut [Recorded]) {
let session = |key: &str| key.rsplit_once('/').map_or(key, |(s, _)| s).to_owned();
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for action in actions.iter_mut() {
let seen = counts.entry(session(&action.key)).or_default();
*seen += 1;
action.nth = *seen;
}
for action in actions.iter_mut() {
action.of = counts[&session(&action.key)];
}
}
pub(crate) fn input_words(input: &Value) -> String {
let field = |name: &str| input.get(name).and_then(Value::as_str);
if let Some(command) = input.get("command").or_else(|| input.get("cmd")) {
let line = match command {
Value::Array(parts) => parts
.iter()
.filter_map(Value::as_str)
.next_back()
.unwrap_or_default()
.to_owned(),
Value::String(line) => line.clone(),
other => other.to_string(),
};
return without_shell(&line);
}
for name in [
"file_path",
"filePath",
"path",
"pattern",
"query",
"url",
"AbsolutePath",
] {
if let Some(value) = field(name) {
return value.to_owned();
}
}
match input {
Value::String(text) => text.clone(),
Value::Null => String::new(),
other => other.to_string(),
}
}
fn without_shell(line: &str) -> String {
for shell in [
"/bin/zsh -lc ",
"/bin/bash -lc ",
"bash -lc ",
"zsh -lc ",
"sh -c ",
] {
if let Some(rest) = line.strip_prefix(shell) {
return rest.trim_matches('\'').to_owned();
}
}
line.to_owned()
}