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
2 changes: 2 additions & 0 deletions migrations/0018_recommendation_outcome_surface_snapshot.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE agent_recommendation_outcomes ADD COLUMN surface TEXT;
ALTER TABLE agent_recommendation_outcomes ADD COLUMN snapshot_id TEXT;
15 changes: 12 additions & 3 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1482,7 +1482,7 @@ function maxIso(left: string | null | undefined, right: string | null | undefine
}

function outcomeStateBuckets(outcomes: AgentRecommendationOutcomeRecord[]): AgentRecommendationOutcomeSummary["states"] {
const states: AgentRecommendationOutcomeState[] = ["accepted", "merged", "improved", "closed", "stale", "ignored"];
const states: AgentRecommendationOutcomeState[] = ["accepted", "merged", "improved", "closed", "rejected", "stale", "ignored"];
return states.flatMap((state) => {
const count = outcomes.filter((outcome) => outcome.outcomeState === state).length;
return count > 0 ? [{ state, count }] : [];
Expand All @@ -1494,6 +1494,7 @@ function recommendationOutcomeTotals(
maintainerLaneTotal: number,
): AgentRecommendationOutcomeSummary["totals"] {
const accepted = outcomes.filter((outcome) => outcome.outcomeState === "accepted").length;
const rejected = outcomes.filter((outcome) => outcome.outcomeState === "rejected").length;
const merged = outcomes.filter((outcome) => outcome.outcomeState === "merged").length;
const improved = outcomes.filter((outcome) => outcome.outcomeState === "improved").length;
const closed = outcomes.filter((outcome) => outcome.outcomeState === "closed").length;
Expand All @@ -1502,13 +1503,14 @@ function recommendationOutcomeTotals(
return {
total: outcomes.length,
accepted,
rejected,
ignored,
stale,
merged,
closed,
improved,
positive: accepted + merged + improved,
negative: closed + stale + ignored,
negative: closed + rejected + stale + ignored,
maintainerLaneTotal,
};
}
Expand All @@ -1532,6 +1534,7 @@ function summarizeRecommendationOutcomeRepos(outcomes: AgentRecommendationOutcom
repoFullName: firstRepo.outcomeRepoFullName ?? firstRepo.targetRepoFullName ?? "unknown/repo",
total: totals.total,
accepted: totals.accepted,
rejected: totals.rejected,
ignored: totals.ignored,
stale: totals.stale,
merged: totals.merged,
Expand Down Expand Up @@ -2427,6 +2430,8 @@ export async function upsertAgentRecommendationOutcome(env: Env, outcome: AgentR
runId: outcome.runId,
actorLogin: boundedString(outcome.actorLogin, 100),
actionType: outcome.actionType,
surface: outcome.surface ?? null,
snapshotId: outcome.snapshotId ?? null,
targetRepoFullName: outcome.targetRepoFullName ? boundedString(outcome.targetRepoFullName, 200) : null,
targetPullNumber: outcome.targetPullNumber ?? null,
targetIssueNumber: outcome.targetIssueNumber ?? null,
Expand All @@ -2452,6 +2457,8 @@ export async function upsertAgentRecommendationOutcome(env: Env, outcome: AgentR
set: {
actorLogin: values.actorLogin,
actionType: values.actionType,
surface: values.surface,
snapshotId: values.snapshotId,
targetRepoFullName: values.targetRepoFullName,
targetPullNumber: values.targetPullNumber,
targetIssueNumber: values.targetIssueNumber,
Expand Down Expand Up @@ -3202,6 +3209,8 @@ function toAgentRecommendationOutcomeRecord(row: typeof agentRecommendationOutco
runId: row.runId,
actorLogin: row.actorLogin,
actionType: parseAgentActionType(row.actionType),
surface: row.surface ? parseAgentSurface(row.surface) : null,
snapshotId: row.snapshotId ?? null,
targetRepoFullName: row.targetRepoFullName,
targetPullNumber: row.targetPullNumber,
targetIssueNumber: row.targetIssueNumber,
Expand Down Expand Up @@ -3996,7 +4005,7 @@ function parseAgentSafetyClass(value: string): AgentSafetyClass {
}

function parseAgentRecommendationOutcomeState(value: string): AgentRecommendationOutcomeState {
if (value === "accepted" || value === "ignored" || value === "stale" || value === "merged" || value === "closed" || value === "improved") return value;
if (value === "accepted" || value === "rejected" || value === "ignored" || value === "stale" || value === "merged" || value === "closed" || value === "improved") return value;
return "ignored";
}

Expand Down
2 changes: 2 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,8 @@ export const agentRecommendationOutcomes = sqliteTable(
targetRepoFullName: text("target_repo_full_name"),
targetPullNumber: integer("target_pull_number"),
targetIssueNumber: integer("target_issue_number"),
surface: text("surface"),
snapshotId: text("snapshot_id"),
outcomeState: text("outcome_state").notNull(),
outcomeTargetType: text("outcome_target_type").notNull(),
outcomeRepoFullName: text("outcome_repo_full_name"),
Expand Down
1 change: 1 addition & 0 deletions src/services/decision-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,7 @@ function emptyRecommendationOutcomeFeedback(login: string): AgentRecommendationO
totals: {
total: 0,
accepted: 0,
rejected: 0,
ignored: 0,
stale: 0,
merged: 0,
Expand Down
27 changes: 24 additions & 3 deletions src/services/recommendation-outcomes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
listAgentActions,
listAgentContextSnapshots,
listAgentRunsForActor,
listContributorIssues,
listContributorPullRequests,
Expand Down Expand Up @@ -40,9 +41,14 @@ export async function evaluateRecommendationOutcomes(
listContributorIssues(env, login),
]);
const completedRuns = runs.filter((run) => run.status === "completed");
const actionGroups = await Promise.all(completedRuns.map(async (run) => ({ run, actions: await listAgentActions(env, run.id) })));
const classifications = actionGroups.flatMap(({ run, actions }) =>
actions.map((action) => classifyRecommendationOutcome({ run, action, pullRequests, issues, evaluatedAt, staleAfterMs, ignoredAfterMs })),
const actionGroups = await Promise.all(
completedRuns.map(async (run) => {
const [actions, snapshots] = await Promise.all([listAgentActions(env, run.id), listAgentContextSnapshots(env, run.id)]);
return { run, actions, snapshotId: snapshots[0]?.id ?? null };
}),
);
const classifications = actionGroups.flatMap(({ run, actions, snapshotId }) =>
actions.map((action) => classifyRecommendationOutcome({ run, action, pullRequests, issues, evaluatedAt, staleAfterMs, ignoredAfterMs, snapshotId })),
);
const classified = classifications.filter((outcome): outcome is AgentRecommendationOutcomeRecord => outcome !== null);
const outcomes = [];
Expand All @@ -63,6 +69,7 @@ export function classifyRecommendationOutcome(args: {
evaluatedAt: string;
staleAfterMs: number;
ignoredAfterMs: number;
snapshotId?: string | null | undefined;
}): AgentRecommendationOutcomeRecord | null {
const actionAt = timestamp(args.action.createdAt ?? args.run.updatedAt ?? args.run.createdAt);
const evaluatedAt = timestamp(args.evaluatedAt);
Expand All @@ -82,6 +89,7 @@ export function classifyRecommendationOutcome(args: {
actionAt,
actionAgeMs,
staleAfterMs: args.staleAfterMs,
snapshotId: args.snapshotId ?? null,
});
}

Expand All @@ -98,6 +106,7 @@ export function classifyRecommendationOutcome(args: {
actionAt,
actionAgeMs,
staleAfterMs: args.staleAfterMs,
snapshotId: args.snapshotId ?? null,
});
}

Expand All @@ -112,6 +121,7 @@ export function classifyRecommendationOutcome(args: {
actionAt,
actionAgeMs,
staleAfterMs: args.staleAfterMs,
snapshotId: args.snapshotId ?? null,
});
}

Expand All @@ -126,11 +136,13 @@ export function classifyRecommendationOutcome(args: {
actionAt,
actionAgeMs,
staleAfterMs: args.staleAfterMs,
snapshotId: args.snapshotId ?? null,
});
}

if (actionAgeMs < args.ignoredAfterMs) return null;
return baseOutcome(args.run, args.action, {
snapshotId: args.snapshotId ?? null,
outcomeState: "ignored",
outcomeTargetType: targetRepoFullName ? "repository" : "none",
outcomeRepoFullName: targetRepoFullName ?? null,
Expand All @@ -153,10 +165,12 @@ function outcomeFromPullRequest(args: {
actionAt: number;
actionAgeMs: number;
staleAfterMs: number;
snapshotId?: string | null | undefined;
}): AgentRecommendationOutcomeRecord {
const state = pullRequestOutcomeState(args.pr, args.action, args.actionAt, args.actionAgeMs, args.staleAfterMs);
const maintainerLane = isMaintainerLane(args.run.actorLogin, args.pr.repoFullName, args.pr.authorAssociation);
return baseOutcome(args.run, args.action, {
snapshotId: args.snapshotId ?? null,
outcomeState: state,
outcomeTargetType: "pull_request",
outcomeRepoFullName: args.pr.repoFullName,
Expand Down Expand Up @@ -184,10 +198,12 @@ function outcomeFromIssue(args: {
actionAt: number;
actionAgeMs: number;
staleAfterMs: number;
snapshotId?: string | null | undefined;
}): AgentRecommendationOutcomeRecord {
const state = issueOutcomeState(args.issue, args.actionAt, args.actionAgeMs, args.staleAfterMs);
const maintainerLane = isMaintainerLane(args.run.actorLogin, args.issue.repoFullName, args.issue.authorAssociation);
return baseOutcome(args.run, args.action, {
snapshotId: args.snapshotId ?? null,
outcomeState: state,
outcomeTargetType: "issue",
outcomeRepoFullName: args.issue.repoFullName,
Expand All @@ -208,6 +224,7 @@ function baseOutcome(
run: AgentRunRecord,
action: AgentActionRecord,
outcome: {
snapshotId?: string | null | undefined;
outcomeState: AgentRecommendationOutcomeState;
outcomeTargetType: AgentRecommendationOutcomeTargetType;
outcomeRepoFullName?: string | null | undefined;
Expand All @@ -226,6 +243,8 @@ function baseOutcome(
runId: run.id,
actorLogin: run.actorLogin,
actionType: action.actionType,
surface: run.surface,
snapshotId: outcome.snapshotId ?? null,
targetRepoFullName: action.targetRepoFullName,
targetPullNumber: action.targetPullNumber,
targetIssueNumber: action.targetIssueNumber,
Expand Down Expand Up @@ -262,6 +281,7 @@ function pullRequestOutcomeState(
if (pr.state === "closed" && updatedAt >= actionAt) return "closed";
const positiveOpenSignal = pr.reviewDecision === "APPROVED" || pr.mergeableState === "clean";
if (action.targetPullNumber && positiveOpenSignal && updatedAt >= actionAt) return "improved";
if (action.targetPullNumber && pr.reviewDecision === "CHANGES_REQUESTED" && updatedAt >= actionAt) return "rejected";
if (createdAt >= actionAt || updatedAt > actionAt) return "accepted";
if (actionAgeMs >= staleAfterMs) return "stale";
return "ignored";
Expand All @@ -280,6 +300,7 @@ function pullRequestOutcomeReason(state: AgentRecommendationOutcomeState, pr: Pu
if (state === "merged") return `${pr.repoFullName}#${pr.number} merged after the recommendation snapshot.`;
if (state === "closed") return `${pr.repoFullName}#${pr.number} closed without a merge after the recommendation snapshot.`;
if (state === "improved") return `${pr.repoFullName}#${pr.number} remains open but now has approval or clean mergeability evidence.`;
if (state === "rejected") return `${pr.repoFullName}#${pr.number} received a changes-requested review decision after the recommendation snapshot.`;
if (state === "accepted") return `${pr.repoFullName}#${pr.number} shows later cached activity matching the recommendation.`;
if (state === "stale") return `${pr.repoFullName}#${pr.number} remains open with no later activity past the stale-outcome window.`;
return `${pr.repoFullName}#${pr.number} is visible but has no later positive or terminal outcome yet.`;
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ export type AgentContextSnapshotRecord = {
createdAt?: string | null | undefined;
};

export type AgentRecommendationOutcomeState = "accepted" | "ignored" | "stale" | "merged" | "closed" | "improved";
export type AgentRecommendationOutcomeState = "accepted" | "rejected" | "ignored" | "stale" | "merged" | "closed" | "improved";
export type AgentRecommendationOutcomeTargetType = "pull_request" | "issue" | "repository" | "none";
export type AgentRecommendationOutcomeConfidence = "high" | "medium" | "low";

Expand All @@ -694,6 +694,8 @@ export type AgentRecommendationOutcomeRecord = {
runId: string;
actorLogin: string;
actionType: AgentActionType;
surface?: AgentSurface | null | undefined;
snapshotId?: string | null | undefined;
targetRepoFullName?: string | null | undefined;
targetPullNumber?: number | null | undefined;
targetIssueNumber?: number | null | undefined;
Expand Down Expand Up @@ -721,6 +723,7 @@ export type AgentRecommendationOutcomeRepoSummary = {
repoFullName: string;
total: number;
accepted: number;
rejected: number;
ignored: number;
stale: number;
merged: number;
Expand All @@ -740,6 +743,7 @@ export type AgentRecommendationOutcomeSummary = {
totals: {
total: number;
accepted: number;
rejected: number;
ignored: number;
stale: number;
merged: number;
Expand Down
6 changes: 6 additions & 0 deletions test/unit/decision-pack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ describe("decision-pack service", () => {
repoFullName: "owner/direct",
total: 4,
accepted: 1,
rejected: 0,
ignored: 1,
stale: 0,
merged: 1,
Expand Down Expand Up @@ -193,6 +194,7 @@ describe("decision-pack service", () => {
repoFullName: "owner/direct",
total: 6,
accepted: 0,
rejected: 0,
ignored: 2,
stale: 2,
merged: 0,
Expand All @@ -213,6 +215,7 @@ describe("decision-pack service", () => {
repoFullName: "owner/direct",
total: 4,
accepted: 1,
rejected: 0,
ignored: 1,
stale: 0,
merged: 1,
Expand Down Expand Up @@ -249,6 +252,7 @@ describe("decision-pack service", () => {
repoFullName: "owner/direct",
total: 0,
accepted: 0,
rejected: 0,
ignored: 0,
stale: 0,
merged: 0,
Expand All @@ -269,6 +273,7 @@ describe("decision-pack service", () => {
repoFullName: "owner/direct",
total: 2,
accepted: 0,
rejected: 0,
ignored: 1,
stale: 0,
merged: 0,
Expand Down Expand Up @@ -315,6 +320,7 @@ describe("decision-pack service", () => {
totals: {
total: 1,
accepted: 1,
rejected: 0,
ignored: 0,
stale: 0,
merged: 0,
Expand Down
Loading