Skip to content
Draft
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
11 changes: 11 additions & 0 deletions .changeset/services-series.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@bounded-systems/prx": minor
---

Add `prx services series` — effort/token series showing completion rate by
model × spend tier.

Reveals the non-monotonic relationship between spend and outcome (higher spend
does not monotonically improve completion rate past a model-specific peak).
Refactors the shared work-unit loading into a private `buildWuidOutcomes`
helper used by both the diamond and series projectors.
41 changes: 31 additions & 10 deletions packages/prx/src/pr-state/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ import { runDedupeBd as doctorRunDedupeBd } from "../doctor/dedupe-bd.ts";
// GH-1823: audit actor — read-only adherence metrics over the artifact graph.
import { runAuditIngest, runAuditUow, runAuditSystem } from "../audit/cli.ts";
// GH-1407: services actor — Anthropic prompt-cache hit-rate projector.
import { runServicesDiamond, runServicesStatus } from "../services/cli.ts";
import { runServicesDiamond, runServicesSeries, runServicesStatus } from "../services/cli.ts";
import { taskRoleMachine, taskRoles, type TaskRole } from "../machine/machines/task.ts";
import { workflowMachine } from "../machine/machines/workflow.ts";
import { resolveWorktreePath } from "../tools/worktree_path.ts";
Expand Down Expand Up @@ -1286,6 +1286,12 @@ type ParsedCommand =
window?: string | undefined;
format: "plain" | "json";
}
| {
// prx-b2n — `prx services series` shows completion rate by model × spend tier.
command: "services-series";
window?: string | undefined;
format: "plain" | "json";
}
| {
// GH-1194: per-actor dispatch envelope. The leading source flag is
// injected by normalizeNamespaceArgv (or by intake/implement passthrough);
Expand Down Expand Up @@ -5790,7 +5796,7 @@ function parseTestGateCommand(rest: string[]): ParsedCommand {
}

// GH-1407 — `prx services <verb>` read-only external-plane status verb.
const SERVICES_VERBS = ["status", "diamond"] as const;
const SERVICES_VERBS = ["status", "diamond", "series"] as const;

function printServicesHelp(): string {
return [
Expand All @@ -5803,11 +5809,16 @@ function printServicesHelp(): string {
" diamond [--window=Nd] [--format=plain|json]",
" Cost-vs-outcome diamond: avg spend and completion rate per model.",
"",
" series [--window=Nd] [--format=plain|json]",
" Effort/token series: completion rate by model × spend tier.",
" Reveals the non-monotonic relationship between spend and outcome.",
"",
"Examples:",
" prx services status --anthropic --by=model",
" prx services status --anthropic --window=7d --format=json",
" prx services diamond --window=30d",
" prx services diamond --format=json",
" prx services series --window=30d",
" prx services series --format=json",
].join("\n");
}

Expand All @@ -5831,7 +5842,7 @@ function parseServicesCommand(rest: string[]): ParsedCommand {
printServicesHelpAndExit();
}

if (verbArg === "diamond") {
if (verbArg === "diamond" || verbArg === "series") {
const { values, positionals } = parseArgs({
args: subRest,
options: {
Expand All @@ -5842,13 +5853,12 @@ function parseServicesCommand(rest: string[]): ParsedCommand {
allowPositionals: true,
});
if (positionals.length > 0) {
throw new CliError("prx services diamond takes no positional arguments");
throw new CliError(`prx services ${verbArg} takes no positional arguments`);
}
return {
command: "services-diamond",
...(values.window !== undefined ? { window: values.window } : {}),
format: ensureChoice(values.format, ["plain", "json"], "--format"),
};
const fmt = ensureChoice(values.format, ["plain", "json"], "--format");
const win = values.window !== undefined ? { window: values.window } : {};
if (verbArg === "series") return { command: "services-series", ...win, format: fmt };
return { command: "services-diamond", ...win, format: fmt };
}

// verb === "status"
Expand Down Expand Up @@ -18786,6 +18796,17 @@ export function runCli(
);
}

// prx-b2n: effort/token series.
if (parsed.command === "services-series") {
return runServicesSeries(
{
...(parsed.window !== undefined ? { window: parsed.window } : {}),
format: parsed.format,
},
output,
);
}

if (parsed.command === "intake-mirror") {
const handler = deps.runIntakeMirror ?? runIntakeMirror;
const validated: IntakeMirrorOptions = intakeMirrorOptionsSchema.parse({
Expand Down
159 changes: 135 additions & 24 deletions packages/prx/src/services/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,23 @@ const CLOSED_STATES = new Set(["closed"]);

type TransitionRow = { issue: string; state_to: string; ts: string };

export function projectAnthropicDiamond(
db: Database,
opts: AnthropicProjectorOptions = {},
): DiamondPoint[] {
const since = opts.since;
type WuidOutcome = {
dominantModel: string;
totalCost: number;
totalCacheRead: number;
totalInput: number;
outcome: "completed" | "closed" | "in_progress";
};

/**
* Shared loader used by both the diamond and series projectors.
*
* For each work unit that has at least one attributed usage event, resolves
* the dominant model (highest cost share) and the outcome from the latest
* transition. Work units with no `workUnitId` are skipped — there is no
* outcome to measure for unattached sessions.
*/
function buildWuidOutcomes(db: Database, since?: string): WuidOutcome[] {
const rows = since
? db
.query<EventRow, [string]>(
Expand All @@ -108,9 +119,6 @@ export function projectAnthropicDiamond(
.query<EventRow, []>(`SELECT raw_json FROM events WHERE action = 'non-interactive-agent'`)
.all();

// Accumulate cost per (workUnitId, model). Each work unit is later assigned
// to its dominant model (highest cost share) so it contributes exactly one
// point to the diamond — no double-counting across models.
type ModelAccum = { cost: number; cache_read: number; input: number };
const byWuid = new Map<string, Map<string, ModelAccum>>();

Expand All @@ -123,7 +131,7 @@ export function projectAnthropicDiamond(
}
if (payload.subkind !== "usage") continue;
const wuid = payload.workUnitId;
if (!wuid) continue; // unattached — no outcome to measure
if (!wuid) continue;
const model = payload.model ?? "(unknown model)";
const inner = byWuid.get(wuid) ?? new Map<string, ModelAccum>();
const entry = inner.get(model) ?? { cost: 0, cache_read: 0, input: 0 };
Expand All @@ -136,8 +144,6 @@ export function projectAnthropicDiamond(

if (byWuid.size === 0) return [];

// Get latest state_to per issue from the transitions log (ascending order so
// each later row overwrites the previous — last write wins = most recent).
const allTransitions = db
.query<TransitionRow, []>(
`SELECT issue, state_to, ts FROM transitions ORDER BY ts ASC`,
Expand All @@ -148,19 +154,8 @@ export function projectAnthropicDiamond(
if (t.issue) latestState.set(t.issue, t.state_to);
}

type ModelAgg = {
work_units: number;
total_cost: number;
completed: number;
closed: number;
in_progress: number;
total_cache_read: number;
total_input: number;
};
const modelAgg = new Map<string, ModelAgg>();

const outcomes: WuidOutcome[] = [];
for (const [wuid, modelMap] of byWuid) {
// Dominant model = highest cost share for this work unit.
let dominantModel = "(unknown model)";
let maxCost = -1;
let totalCost = 0;
Expand All @@ -175,7 +170,6 @@ export function projectAnthropicDiamond(
dominantModel = model;
}
}

const state = latestState.get(wuid);
const outcome = state
? COMPLETED_STATES.has(state)
Expand All @@ -184,7 +178,30 @@ export function projectAnthropicDiamond(
? "closed"
: "in_progress"
: "in_progress";
outcomes.push({ dominantModel, totalCost, totalCacheRead, totalInput, outcome });
}
return outcomes;
}

export function projectAnthropicDiamond(
db: Database,
opts: AnthropicProjectorOptions = {},
): DiamondPoint[] {
const outcomes = buildWuidOutcomes(db, opts.since);
if (outcomes.length === 0) return [];

type ModelAgg = {
work_units: number;
total_cost: number;
completed: number;
closed: number;
in_progress: number;
total_cache_read: number;
total_input: number;
};
const modelAgg = new Map<string, ModelAgg>();

for (const { dominantModel, totalCost, totalCacheRead, totalInput, outcome } of outcomes) {
const agg = modelAgg.get(dominantModel) ?? {
work_units: 0,
total_cost: 0,
Expand Down Expand Up @@ -222,6 +239,100 @@ export function projectAnthropicDiamond(
.sort((a, b) => b.completion_rate - a.completion_rate || b.total_cost_usd - a.total_cost_usd);
}

/**
* One point on the effort/token series — a (model, cost_tier) pair showing
* completion rate within a specific spend band.
*
* The series exposes the non-monotonic relationship between spend and outcome:
* more tokens ≠ better results past a model-specific peak. Use it to pick the
* cost-efficient tier for each task rather than defaulting to max effort.
*/
export type SeriesPoint = {
model: string;
tier: string;
tier_min: number;
tier_max: number;
work_units: number;
avg_cost_usd: number;
completion_rate: number;
hit_rate: number;
};

// Cost bands chosen to capture the ~$8 non-monotonic peak observed in
// Opus 4.8 (peaks around $8, dips past $10.5 — higher spend ≠ better outcome).
const COST_TIERS: ReadonlyArray<{ label: string; min: number; max: number }> = [
{ label: "<$2", min: 0, max: 2 },
{ label: "$2–$5", min: 2, max: 5 },
{ label: "$5–$10", min: 5, max: 10 },
{ label: "$10–$20", min: 10, max: 20 },
{ label: "$20–$40", min: 20, max: 40 },
{ label: "$40+", min: 40, max: Infinity },
];

function costTier(cost: number): (typeof COST_TIERS)[number] {
for (const tier of COST_TIERS) {
if (cost < tier.max) return tier;
}
return COST_TIERS[COST_TIERS.length - 1]!;
}

export function projectAnthropicSeries(
db: Database,
opts: AnthropicProjectorOptions = {},
): SeriesPoint[] {
const outcomes = buildWuidOutcomes(db, opts.since);
if (outcomes.length === 0) return [];

type TierAgg = {
work_units: number;
total_cost: number;
completed: number;
total_cache_read: number;
total_input: number;
};
// key = `${model}\x00${tierLabel}`
const tierAgg = new Map<string, TierAgg & { tier: (typeof COST_TIERS)[number] }>();

for (const { dominantModel, totalCost, totalCacheRead, totalInput, outcome } of outcomes) {
const tier = costTier(totalCost);
const key = `${dominantModel}\x00${tier.label}`;
const agg = tierAgg.get(key) ?? {
work_units: 0,
total_cost: 0,
completed: 0,
total_cache_read: 0,
total_input: 0,
tier,
};
agg.work_units += 1;
agg.total_cost += totalCost;
agg.total_cache_read += totalCacheRead;
agg.total_input += totalInput;
if (outcome === "completed") agg.completed += 1;
tierAgg.set(key, agg);
}

return [...tierAgg.entries()]
.map(([key, agg]) => {
const model = key.split("\x00")[0]!;
const denom = agg.total_input + agg.total_cache_read;
return {
model,
tier: agg.tier.label,
tier_min: agg.tier.min,
tier_max: agg.tier.max === Infinity ? -1 : agg.tier.max,
work_units: agg.work_units,
avg_cost_usd: agg.work_units > 0 ? agg.total_cost / agg.work_units : 0,
completion_rate: agg.work_units > 0 ? agg.completed / agg.work_units : 0,
hit_rate: denom === 0 ? 0 : agg.total_cache_read / denom,
};
})
.sort((a, b) => {
const modelCmp = a.model.localeCompare(b.model);
return modelCmp !== 0 ? modelCmp : a.tier_min - b.tier_min;
});
}

export function projectAnthropicUsage(
db: Database,
opts: AnthropicProjectorOptions = {},
Expand Down
Loading
Loading