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
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ members = [
]

[workspace.package]
version = "0.8.7"
version = "0.8.8"
edition = "2024"
publish = false

Expand Down
37 changes: 34 additions & 3 deletions crates/agency-tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ pub mod message;
pub mod project;
#[path = "../../../apps/gui/src/db/schema/project_item.rs"]
pub mod project_item;
pub mod ps_usage;
#[path = "../../../apps/gui/src/db/schema/study_event.rs"]
pub mod study_event;
#[path = "../../../apps/gui/src/db/schema/task_log.rs"]
pub mod task_log;
#[path = "../../../apps/gui/src/db/schema/usage_cache.rs"]
Expand All @@ -69,6 +72,7 @@ use kv::KvWorkTable;
use message::{MessageRow, MessageWorkTable};
use project::{ProjectRow, ProjectWorkTable};
use project_item::{ProjectItemRow, ProjectItemWorkTable};
use study_event::StudyEventWorkTable;
use usage_cache::UsageCacheWorkTable;
use usage_ledger::UsageLedgerWorkTable;

Expand All @@ -95,13 +99,31 @@ const RETRY_BASE_MS: u64 = 50;
/// Only when the platform reports no home directory at all, which means there
/// is no default and no pointer file location to check.
pub fn data_location() -> eyre::Result<location::DataLocation> {
const IDENTIFIER: &str = "com.pathscale.agencyzero";
data_location_for(IDENTIFIER_STABLE)
}

/// The bundle identifier of the standard build.
pub const IDENTIFIER_STABLE: &str = "com.pathscale.agencyzero";

/// The bundle identifier of the experimental build.
///
/// A separate identifier means a separate config directory, a separate pointer
/// file and a separate store. Reading the stable store while the experimental
/// window is the one running reports another profile's data as if it were this
/// one's, which is worse than reporting nothing.
pub const IDENTIFIER_EXPERIMENTAL: &str = "com.pathscale.agencyzero.experimental";

/// The store directory for one bundle identifier.
///
/// # Errors
/// Only when the platform reports no home directory at all.
pub fn data_location_for(identifier: &str) -> eyre::Result<location::DataLocation> {
let config_dir = dirs::config_dir()
.ok_or_else(|| eyre::eyre!("no config directory on this platform"))?
.join(IDENTIFIER);
.join(identifier);
let data_dir = dirs::data_dir()
.ok_or_else(|| eyre::eyre!("no data directory on this platform"))?
.join(IDENTIFIER);
.join(identifier);
Ok(location::resolve(&config_dir, &data_dir))
}

Expand Down Expand Up @@ -184,6 +206,15 @@ open_read_only!(
open_messages,
MessageWorkTable
);
open_read_only!(
/// The content-free directive-usage table, read-only.
///
/// Written only while the opt-in setting is on, and holding no prompt text,
/// titles, paths or URLs by construction. `ps_usage_report` reads it to
/// count how the declared operations were used.
open_study_events,
StudyEventWorkTable
);

/// A project row as the CLI prints it: one JSON object, one line.
///
Expand Down
79 changes: 78 additions & 1 deletion crates/agency-tools/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ enum Command {
Usage {
project: Option<String>,
},
PsUsageReport {
blinded: bool,
start: Option<String>,
end: Option<String>,
},
}

const USAGE: &str = "\
Expand All @@ -57,6 +62,18 @@ commands:
with project names and exact recovery candidates
usage [--project ID] token/cost rollup: whole-store totals, the single
largest turn, and per-model / per-day breakdowns
ps-usage-report [--blinded] [--start RFC3339] [--end RFC3339]
directive-usage statistics over a closed window:
incidence, per-surface and per-verb counts,
outcomes and per-day activity. Prints a table on
stdout and the same numbers as JSON on the last
line. --blinded substitutes every verb through a
fixed mapping and drops identifying strings.
The window has no default and must be given.

Set AZ_PROFILE=experimental to read the experimental build's store; it keeps
its own config directory, pointer file and tables, so the default resolves
the standard build only.

The store location honours the same overrides as the GUI: $AZ_DATA_DIR,
then the data-location.json pointer next to the app's config, then the
Expand Down Expand Up @@ -123,6 +140,28 @@ fn parse_args(args: &[String]) -> Result<Command, String> {
let query = args.next().ok_or("search-items needs a query")?.to_string();
Command::SearchItems { query }
}
"ps-usage-report" => {
let mut blinded = false;
let mut start = None;
let mut end = None;
while let Some(flag) = args.next() {
match flag.as_str() {
"--blinded" => blinded = true,
"--start" => {
start = Some(args.next().ok_or("--start needs a value")?.to_string());
}
"--end" => {
end = Some(args.next().ok_or("--end needs a value")?.to_string());
}
other => return Err(format!("unexpected argument: {other}")),
}
}
return Ok(Command::PsUsageReport {
blinded,
start,
end,
});
}
"usage" => {
return Ok(Command::Usage {
project: project_filter(args)?,
Expand Down Expand Up @@ -163,7 +202,19 @@ fn main() -> ExitCode {
}

fn run(command: Command) -> eyre::Result<()> {
let location = agency_tools::data_location()?;
// `AZ_DATA_DIR` still wins, as it does for the GUI. Without it, an
// experimental window's store is only reachable through its own bundle
// identifier: a different identifier means a different config directory,
// a different pointer file and a different store, so resolving the stable
// one reports another profile's numbers as if they were this profile's.
let identifier = if std::env::var_os("AZ_PROFILE")
.is_some_and(|value| value.eq_ignore_ascii_case("experimental"))
{
agency_tools::IDENTIFIER_EXPERIMENTAL
} else {
agency_tools::IDENTIFIER_STABLE
};
let location = agency_tools::data_location_for(identifier)?;
let dir = location.path;
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
Expand Down Expand Up @@ -234,6 +285,32 @@ fn run(command: Command) -> eyre::Result<()> {
println!("{}", serde_json::to_string_pretty(&summary)?);
Ok(())
}
Command::PsUsageReport {
blinded,
start,
end,
} => {
use agency_tools::ps_usage;
use worktable::prelude::SelectQueryExecutor;

// A flag beats editing the constants for a one-off window, but
// the constants stay the declared default so an unset window is
// still a refusal rather than a silent choice.
let start = start.unwrap_or_else(|| ps_usage::WINDOW_START.to_owned());
let end = end.unwrap_or_else(|| ps_usage::WINDOW_END.to_owned());

let table = agency_tools::open_study_events(&dir).await?;
let rows = table.select_all().execute()?;
let report = ps_usage::build(&rows, &start, &end)?;
let report = if blinded {
ps_usage::blind(&report)
} else {
report
};
print!("{}", ps_usage::render(&report));
println!("{}", serde_json::to_string(&report)?);
Ok(())
}
}
})
}
Expand Down
Loading
Loading