pi-advisor-flow 0.9.2-dev.4 → 0.9.2-dev.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.9.2-dev.4",
3
+ "version": "0.9.2-dev.5",
4
4
  "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
5
5
  "keywords": [
6
6
  "advisor",
@@ -15,6 +15,8 @@ import type { CommandRuntime, ManualAdvisorProgressState } from "./types.ts";
15
15
 
16
16
  interface ManualResultDetails {
17
17
  advisor: string;
18
+ agentRulesBytes?: number;
19
+ preferenceBytes?: number;
18
20
  question?: string;
19
21
  text: string;
20
22
  usage?: AdvisorUsageSnapshot;
@@ -48,34 +50,35 @@ export const startManualConsultation = async (
48
50
  const finishHerdrActivity = runtime.herdrActivity.start();
49
51
  let scoutDetails: ScoutToolDetails | undefined;
50
52
  try {
51
- const { adviceId, markdown, usage } = await runtime.requestAdvisor(
52
- ctx,
53
- question,
54
- controller.signal,
55
- (thinking, text) => {
56
- if (controller.signal.aborted) {
57
- return;
58
- }
59
- progress.phase = "active";
60
- progress.thinking = thinking;
61
- progress.text = text;
62
- runtime.requestManualRender(ctx);
63
- },
64
- (event) => {
65
- if (!controller.signal.aborted) {
66
- runtime.scoutStatus.update(ctx, scoutStatusToken, event);
67
- scoutDetails = appendScoutLifecycleEntry(
68
- runtime.pi,
69
- event,
70
- scoutDetails
71
- );
72
- progress.scout = scoutDetails;
53
+ const { adviceId, agentRulesBytes, markdown, preferenceBytes, usage } =
54
+ await runtime.requestAdvisor(
55
+ ctx,
56
+ question,
57
+ controller.signal,
58
+ (thinking, text) => {
59
+ if (controller.signal.aborted) {
60
+ return;
61
+ }
73
62
  progress.phase = "active";
63
+ progress.thinking = thinking;
64
+ progress.text = text;
74
65
  runtime.requestManualRender(ctx);
75
- }
76
- },
77
- gitContext
78
- );
66
+ },
67
+ (event) => {
68
+ if (!controller.signal.aborted) {
69
+ runtime.scoutStatus.update(ctx, scoutStatusToken, event);
70
+ scoutDetails = appendScoutLifecycleEntry(
71
+ runtime.pi,
72
+ event,
73
+ scoutDetails
74
+ );
75
+ progress.scout = scoutDetails;
76
+ progress.phase = "active";
77
+ runtime.requestManualRender(ctx);
78
+ }
79
+ },
80
+ gitContext
81
+ );
79
82
  if (controller.signal.aborted) {
80
83
  return;
81
84
  }
@@ -103,6 +106,8 @@ export const startManualConsultation = async (
103
106
  runtime.updateAdvisorUsageStatus(ctx);
104
107
  const details: ManualResultDetails = {
105
108
  advisor: advisorRef,
109
+ agentRulesBytes,
110
+ preferenceBytes,
106
111
  question,
107
112
  text: markdown,
108
113
  };
@@ -0,0 +1,98 @@
1
+ import { aggregateOutcomeRecords, readOutcomeLog } from "../outcome-stats.ts";
2
+ import type { OutcomeStats } from "../outcome-stats.ts";
3
+ import { OUTCOME_LOG_MAX_BYTES } from "../outcomes.ts";
4
+ import type { CommandRuntime } from "./types.ts";
5
+
6
+ export interface AdvisorStatsDetails {
7
+ error?: string;
8
+ malformedLines: number;
9
+ stats?: OutcomeStats;
10
+ }
11
+
12
+ const percent = (rate: number | undefined) =>
13
+ rate === undefined ? "n/a" : `${(rate * 100).toFixed(1)}%`;
14
+
15
+ const validatedCount = (value: { failed: number; passed: number }) =>
16
+ value.failed + value.passed;
17
+
18
+ const timestamp = (value: string | undefined) =>
19
+ value ? new Date(value).toLocaleString() : "n/a";
20
+
21
+ export const formatAdvisorStats = ({
22
+ error,
23
+ malformedLines,
24
+ stats,
25
+ }: AdvisorStatsDetails): string => {
26
+ if (error) {
27
+ return `**Could not read Advisor outcomes.**\n\n${error}`;
28
+ }
29
+ if (!stats || stats.total === 0) {
30
+ const ignored = malformedLines
31
+ ? `\n\nIgnored ${malformedLines} malformed ledger line${malformedLines === 1 ? "" : "s"}.`
32
+ : "";
33
+ return `**No Advisor outcomes recorded yet.**\n\nEnable outcome logging and record an outcome after an Advisor response to build this report.${ignored}`;
34
+ }
35
+ const { validationByAdoption } = stats;
36
+ const { followed } = validationByAdoption;
37
+ const notFollowed = validationByAdoption["not-followed"];
38
+ const triggerLines = Object.entries(stats.byTrigger).map(
39
+ ([trigger, count]) => `- ${trigger}: ${count}`
40
+ );
41
+ const adoptionLines = Object.entries(stats.adoption).map(
42
+ ([adoption, count]) => `- ${adoption}: ${count}`
43
+ );
44
+ const validationLines = [
45
+ `- followed: ${percent(followed.passRate)} passed (${followed.passed}/${validatedCount(followed)})`,
46
+ `- not-followed: ${percent(notFollowed.passRate)} passed (${notFollowed.passed}/${validatedCount(notFollowed)})`,
47
+ ];
48
+ const ignored = malformedLines
49
+ ? `\n\nIgnored ${malformedLines} malformed ledger line${malformedLines === 1 ? "" : "s"}.`
50
+ : "";
51
+ return [
52
+ `**${stats.total} Advisor outcome${stats.total === 1 ? "" : "s"}**`,
53
+ "",
54
+ "**By trigger**",
55
+ ...triggerLines,
56
+ "",
57
+ "**Adoption**",
58
+ ...adoptionLines,
59
+ "",
60
+ "**Validation passed rate (passed / passed + failed)**",
61
+ ...validationLines,
62
+ "",
63
+ `Distinct advices seen: ${stats.distinctAdvices}`,
64
+ `Time window: ${timestamp(stats.firstTimestamp)} → ${timestamp(stats.lastTimestamp)}`,
65
+ `Ledger window: capped at ${OUTCOME_LOG_MAX_BYTES / (1024 * 1024)} MiB and rewritten on overflow; this report covers only retained records.${ignored}`,
66
+ ].join("\n");
67
+ };
68
+
69
+ const readAdvisorStats = async (): Promise<AdvisorStatsDetails> => {
70
+ try {
71
+ const log = await readOutcomeLog();
72
+ const stats = aggregateOutcomeRecords(log.records, log.malformedLines);
73
+ return { malformedLines: log.malformedLines, stats };
74
+ } catch (error) {
75
+ return {
76
+ error: error instanceof Error ? error.message : String(error),
77
+ malformedLines: 0,
78
+ };
79
+ }
80
+ };
81
+
82
+ export const registerOutcomeStatsCommand = (runtime: CommandRuntime) => {
83
+ runtime.pi.registerCommand("advisor-stats", {
84
+ description: "Show retained Advisor outcome adoption and validation stats",
85
+ handler: async (_args, _ctx) => {
86
+ const details = await readAdvisorStats();
87
+ runtime.pi.sendMessage(
88
+ {
89
+ content: [],
90
+ customType: "advisor-stats-result",
91
+ details,
92
+ display: true,
93
+ },
94
+ { deliverAs: "steer", triggerTurn: false }
95
+ );
96
+ },
97
+ });
98
+ };
@@ -7,6 +7,7 @@ import { activateAdvisor } from "./activation.ts";
7
7
  import { registerCommandLifecycle } from "./lifecycle.ts";
8
8
  import { registerManualCommand } from "./manual-command.ts";
9
9
  import { registerModelCommands } from "./model-commands.ts";
10
+ import { registerOutcomeStatsCommand } from "./outcome-stats.ts";
10
11
  import { registerCommandRenderers } from "./renderers.ts";
11
12
  import { createCommandRuntime } from "./runtime.ts";
12
13
  import { registerSettingsCommands } from "./settings-commands.ts";
@@ -24,5 +25,6 @@ export const registerCommands = (
24
25
  registerCommandLifecycle(runtime, activate);
25
26
  registerManualCommand(runtime);
26
27
  registerModelCommands(runtime);
28
+ registerOutcomeStatsCommand(runtime);
27
29
  registerSettingsCommands(runtime);
28
30
  };
@@ -15,6 +15,8 @@ import {
15
15
  } from "../tools/render-common.ts";
16
16
  import { formatAdvisorUsage } from "../usage.ts";
17
17
  import { ManualAdvisorProgressComponent } from "./manual-progress.ts";
18
+ import { formatAdvisorStats } from "./outcome-stats.ts";
19
+ import type { AdvisorStatsDetails } from "./outcome-stats.ts";
18
20
  import type { CommandRuntime } from "./types.ts";
19
21
 
20
22
  interface ManualCallEntryDetails {
@@ -24,10 +26,22 @@ interface ManualCallEntryDetails {
24
26
 
25
27
  interface ManualResultDetails {
26
28
  advisor?: string;
29
+ agentRulesBytes?: number;
30
+ preferenceBytes?: number;
27
31
  text?: string;
28
32
  usage?: unknown;
29
33
  }
30
34
 
35
+ const formatByteSize = (bytes: number) => {
36
+ if (bytes >= 1024 * 1024) {
37
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
38
+ }
39
+ if (bytes >= 1024) {
40
+ return `${Math.round(bytes / 1024)} KiB`;
41
+ }
42
+ return `${bytes} B`;
43
+ };
44
+
31
45
  const manualCallRenderer =
32
46
  (runtime: CommandRuntime): EntryRenderer<ManualCallEntryDetails> =>
33
47
  (entry, { expanded }, theme) => {
@@ -70,12 +84,45 @@ const manualResultRenderer: MessageRenderer<ManualResultDetails> = (
70
84
  box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
71
85
  }
72
86
  }
87
+ const attachments = [
88
+ details?.agentRulesBytes
89
+ ? `AGENTS.md rules attached · ${formatByteSize(details.agentRulesBytes)}`
90
+ : undefined,
91
+ details?.preferenceBytes
92
+ ? `Project preferences attached · ${formatByteSize(details.preferenceBytes)}`
93
+ : undefined,
94
+ ].filter((value): value is string => value !== undefined);
95
+ if (attachments.length) {
96
+ box.addChild(
97
+ new Text(theme.fg("dim", ` ${attachments.join(" · ")}`), 0, 0)
98
+ );
99
+ }
73
100
  box.addChild(
74
101
  new Markdown(adviceForDisplay(advice, expanded), 0, 0, getMarkdownTheme())
75
102
  );
76
103
  return box;
77
104
  };
78
105
 
106
+ const outcomeStatsRenderer: MessageRenderer<AdvisorStatsDetails> = (
107
+ message,
108
+ _options,
109
+ theme
110
+ ) => {
111
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
112
+ box.addChild(
113
+ new Text(theme.fg("accent", theme.bold("◆ ADVISOR · OUTCOME STATS")), 0, 0)
114
+ );
115
+ box.addChild(
116
+ new Markdown(
117
+ formatAdvisorStats(message.details ?? { malformedLines: 0 }),
118
+ 0,
119
+ 0,
120
+ getMarkdownTheme()
121
+ )
122
+ );
123
+ return box;
124
+ };
125
+
79
126
  export const registerCommandRenderers = (runtime: CommandRuntime) => {
80
127
  runtime.pi.registerEntryRenderer?.(
81
128
  "advisor-manual-call",
@@ -85,4 +132,8 @@ export const registerCommandRenderers = (runtime: CommandRuntime) => {
85
132
  "advisor-manual-result",
86
133
  manualResultRenderer
87
134
  );
135
+ runtime.pi.registerMessageRenderer?.(
136
+ "advisor-stats-result",
137
+ outcomeStatsRenderer
138
+ );
88
139
  };
@@ -3,6 +3,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import {
4
4
  getAdvisorSettings,
5
5
  getPersistedModelRefs,
6
+ setAdvisorAgentsMdContextRef,
6
7
  setAdvisorAutoLoopGateRef,
7
8
  setAdvisorBlockOnBlockedRef,
8
9
  setAdvisorCollapseResponsesRef,
@@ -126,6 +127,7 @@ const applyDisclosureSettings = (settings: AdvisorSettings) => {
126
127
  setAdvisorGitContextRef(settings.gitContext ?? "summary");
127
128
  setAdvisorGitContextMaxCharsRef(settings.gitContextMaxChars ?? 20_000);
128
129
  setAdvisorToolPoliciesRef(settings.toolPolicies ?? {});
130
+ setAdvisorAgentsMdContextRef(settings.agentsMdContext ?? true);
129
131
  setAdvisorTrackedFileContentRef(settings.trackedFileContent ?? false);
130
132
  setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
131
133
  setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
@@ -19,10 +19,11 @@ export type ManualConsult = (
19
19
  gitContext?: GitContextLevel
20
20
  ) => Promise<{
21
21
  adviceId?: string;
22
- markdown: string;
23
- thinkingText: string;
22
+ agentRulesBytes?: number;
24
23
  draftBytes?: number;
24
+ markdown: string;
25
25
  preferenceBytes?: number;
26
+ thinkingText: string;
26
27
  usage?: unknown;
27
28
  }>;
28
29
 
@@ -1,4 +1,5 @@
1
1
  import {
2
+ setAdvisorAgentsMdContextRef,
2
3
  setAdvisorAutoLoopGateRef,
3
4
  setAdvisorBlockOnBlockedRef,
4
5
  setAdvisorCollapseResponsesRef,
@@ -68,6 +69,7 @@ import {
68
69
  export const resetDefaults = () => {
69
70
  setExecutorRef("");
70
71
  setAdvisorRef("");
72
+ setAdvisorAgentsMdContextRef(true);
71
73
  setPersistedModelRefs(undefined, undefined);
72
74
  setExecutorEffortRef(undefined);
73
75
  setAdvisorEffortRef(undefined);
@@ -139,6 +141,11 @@ export const applyConfig = (config: AdvisorConfig) => {
139
141
  applyNonEmptyStringConfig(config.advisor, setAdvisorRef);
140
142
  applyNonEmptyStringConfig(config.executorEffort, setExecutorEffortRef);
141
143
  applyNonEmptyStringConfig(config.advisorEffort, setAdvisorEffortRef);
144
+ applyOptionalConfig(
145
+ config,
146
+ "advisorAgentsMdContext",
147
+ setAdvisorAgentsMdContextRef
148
+ );
142
149
  applyOptionalConfig(config, "contextMaxChars", setContextMaxCharsRef);
143
150
  applyOptionalConfig(config, "advisorPlanGate", setAdvisorPlanGateRef);
144
151
  applyOptionalConfig(config, "advisorFailureGate", setAdvisorFailureGateRef);
@@ -2,6 +2,7 @@ import { isString } from "../content-utils.ts";
2
2
  import type { JsonValue } from "../content-utils.ts";
3
3
  import { GIT_CONTEXT_LEVELS, isValidGitContextLevel } from "../git.ts";
4
4
  import {
5
+ advisorAgentsMdContextRef,
5
6
  advisorAutoLoopGateRef,
6
7
  advisorBlockOnBlockedRef,
7
8
  advisorCollapseResponsesRef,
@@ -171,6 +172,12 @@ export const CONFIG_SCHEMA = {
171
172
  persisted: true,
172
173
  type: "string",
173
174
  },
175
+ advisorAgentsMdContext: {
176
+ accepted: "true or false",
177
+ current: () => advisorAgentsMdContextRef,
178
+ persisted: true,
179
+ type: "boolean",
180
+ },
174
181
  advisorAutoLoopGate: {
175
182
  accepted: "true or false",
176
183
  current: () => advisorAutoLoopGateRef,
@@ -25,6 +25,7 @@ import type {
25
25
  // An empty ref means no model has been selected yet.
26
26
  export let executorRef = "";
27
27
  export let advisorRef = "";
28
+ export let advisorAgentsMdContextRef = true;
28
29
  let persistedExecutorRef: string | undefined;
29
30
  let persistedAdvisorRef: string | undefined;
30
31
  export let executorEffortRef: string | undefined;
@@ -80,6 +81,9 @@ export const setExecutorRef = (ref: string) => {
80
81
  export const setAdvisorRef = (ref: string) => {
81
82
  advisorRef = ref;
82
83
  };
84
+ export const setAdvisorAgentsMdContextRef = (enabled: boolean) => {
85
+ advisorAgentsMdContextRef = enabled;
86
+ };
83
87
  /** Returns model refs explicitly persisted in the global Advisor config. */
84
88
  export const getPersistedModelRefs = () => ({
85
89
  advisor: persistedAdvisorRef,
@@ -234,6 +238,7 @@ export const setShowUsageFooterRef = (enabled: boolean) => {
234
238
  * imported mutable bindings, which can be snapshotted by extension loaders.
235
239
  */
236
240
  export const getAdvisorSettings = () => ({
241
+ agentsMdContext: advisorAgentsMdContextRef,
237
242
  alwaysOn: alwaysOnRef,
238
243
  autoLoopGate: advisorAutoLoopGateRef,
239
244
  blockOnBlocked: advisorBlockOnBlockedRef,
@@ -60,6 +60,7 @@ export const GATE_FAILURE_MODES: GateFailureMode[] = [
60
60
 
61
61
  export interface AdvisorConfig {
62
62
  advisor?: string;
63
+ advisorAgentsMdContext?: boolean;
63
64
  advisorAutoLoopGate?: boolean;
64
65
  advisorBlockOnBlocked?: boolean;
65
66
  advisorCollapseResponses?: boolean;
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // biome-ignore lint/performance/noBarrelFile: this facade intentionally preserves the public config module contract.
2
2
  export { parseArgs } from "./config/args.ts";
3
3
  export {
4
+ advisorAgentsMdContextRef,
4
5
  advisorAutoLoopGateRef,
5
6
  advisorBlockOnBlockedRef,
6
7
  advisorCollapseResponsesRef,
@@ -34,6 +35,7 @@ export {
34
35
  getAdvisorSettings,
35
36
  getPersistedModelRefs,
36
37
  isSimpleMode,
38
+ setAdvisorAgentsMdContextRef,
37
39
  setAdvisorAutoLoopGateRef,
38
40
  setAdvisorBlockOnBlockedRef,
39
41
  setAdvisorCollapseResponsesRef,
@@ -0,0 +1,154 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { isRecord, isString } from "./content-utils.ts";
4
+ import {
5
+ ADOPTIONS,
6
+ OUTCOME_TRIGGERS,
7
+ VALIDATIONS,
8
+ outcomeLogPath,
9
+ } from "./outcomes.ts";
10
+ import type {
11
+ OutcomeAdoption,
12
+ OutcomeRecord,
13
+ OutcomeTrigger,
14
+ } from "./outcomes.ts";
15
+
16
+ interface OutcomeValidationStats {
17
+ failed: number;
18
+ notRun: number;
19
+ passed: number;
20
+ passRate?: number;
21
+ total: number;
22
+ unknown: number;
23
+ }
24
+ export interface OutcomeStats {
25
+ adoption: Record<OutcomeAdoption, number>;
26
+ byTrigger: Record<OutcomeTrigger, number>;
27
+ distinctAdvices: number;
28
+ firstTimestamp?: string;
29
+ lastTimestamp?: string;
30
+ malformedLines: number;
31
+ total: number;
32
+ validationByAdoption: Record<OutcomeAdoption, OutcomeValidationStats>;
33
+ }
34
+ export interface OutcomeLogRead {
35
+ malformedLines: number;
36
+ missing: boolean;
37
+ records: OutcomeRecord[];
38
+ }
39
+
40
+ const ADOPTION_SET = new Set<string>(ADOPTIONS);
41
+ const VALIDATION_SET = new Set<string>(VALIDATIONS);
42
+ const TRIGGER_SET = new Set<string>(OUTCOME_TRIGGERS);
43
+ const isErrnoException = (error: unknown): error is NodeJS.ErrnoException =>
44
+ error instanceof Error && "code" in error;
45
+
46
+ const isOutcomeRecord = (value: unknown): value is OutcomeRecord =>
47
+ isRecord(value) &&
48
+ value.v === 1 &&
49
+ isString(value.adoption) &&
50
+ ADOPTION_SET.has(value.adoption) &&
51
+ isString(value.adviceHash) &&
52
+ isString(value.timestamp) &&
53
+ Number.isFinite(Date.parse(value.timestamp)) &&
54
+ isString(value.trigger) &&
55
+ TRIGGER_SET.has(value.trigger) &&
56
+ isString(value.validationStatus) &&
57
+ VALIDATION_SET.has(value.validationStatus);
58
+
59
+ export const parseOutcomeLog = (text: string): OutcomeLogRead => {
60
+ const records: OutcomeRecord[] = [];
61
+ let malformedLines = 0;
62
+ for (const line of text.split(/\r?\n/u)) {
63
+ if (!line.trim()) {
64
+ continue;
65
+ }
66
+ try {
67
+ const value: unknown = JSON.parse(line);
68
+ if (isOutcomeRecord(value)) {
69
+ records.push(value);
70
+ } else {
71
+ malformedLines += 1;
72
+ }
73
+ } catch {
74
+ malformedLines += 1;
75
+ }
76
+ }
77
+ return { malformedLines, missing: false, records };
78
+ };
79
+
80
+ export const readOutcomeLog = async (
81
+ path = outcomeLogPath()
82
+ ): Promise<OutcomeLogRead> => {
83
+ try {
84
+ return parseOutcomeLog(await readFile(path, "utf-8"));
85
+ } catch (error) {
86
+ if (isErrnoException(error) && error.code === "ENOENT") {
87
+ return { malformedLines: 0, missing: true, records: [] };
88
+ }
89
+ throw error;
90
+ }
91
+ };
92
+
93
+ const emptyValidationStats = (): OutcomeValidationStats => ({
94
+ failed: 0,
95
+ notRun: 0,
96
+ passed: 0,
97
+ total: 0,
98
+ unknown: 0,
99
+ });
100
+
101
+ export const aggregateOutcomeRecords = (
102
+ records: readonly OutcomeRecord[],
103
+ malformedLines = 0
104
+ ): OutcomeStats => {
105
+ // SAFETY: ADOPTIONS enumerates every key in the returned adoption record.
106
+ const adoption = Object.fromEntries(
107
+ ADOPTIONS.map((value) => [value, 0])
108
+ ) as Record<OutcomeAdoption, number>;
109
+ // SAFETY: OUTCOME_TRIGGERS enumerates every key in the returned trigger record.
110
+ const byTrigger = Object.fromEntries(
111
+ OUTCOME_TRIGGERS.map((value) => [value, 0])
112
+ ) as Record<OutcomeTrigger, number>;
113
+ // SAFETY: ADOPTIONS enumerates every key and each value gets a fresh stats object.
114
+ const validationByAdoption = Object.fromEntries(
115
+ ADOPTIONS.map((value) => [value, emptyValidationStats()])
116
+ ) as Record<OutcomeAdoption, OutcomeValidationStats>;
117
+ const adviceHashes = new Set<string>();
118
+ let first: { timestamp: string; time: number } | undefined;
119
+ let last: { timestamp: string; time: number } | undefined;
120
+
121
+ for (const record of records) {
122
+ adoption[record.adoption] += 1;
123
+ byTrigger[record.trigger] += 1;
124
+ adviceHashes.add(record.adviceHash);
125
+ const validation = validationByAdoption[record.adoption];
126
+ validation.total += 1;
127
+ validation[
128
+ record.validationStatus === "not-run" ? "notRun" : record.validationStatus
129
+ ] += 1;
130
+ const time = Date.parse(record.timestamp);
131
+ if (!first || time < first.time) {
132
+ first = { time, timestamp: record.timestamp };
133
+ }
134
+ if (!last || time > last.time) {
135
+ last = { time, timestamp: record.timestamp };
136
+ }
137
+ }
138
+ for (const value of Object.values(validationByAdoption)) {
139
+ const validated = value.passed + value.failed;
140
+ if (validated > 0) {
141
+ value.passRate = value.passed / validated;
142
+ }
143
+ }
144
+ return {
145
+ adoption,
146
+ byTrigger,
147
+ distinctAdvices: adviceHashes.size,
148
+ firstTimestamp: first?.timestamp,
149
+ lastTimestamp: last?.timestamp,
150
+ malformedLines,
151
+ total: records.length,
152
+ validationByAdoption,
153
+ };
154
+ };
package/src/outcomes.ts CHANGED
@@ -17,7 +17,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
17
17
 
18
18
  export type OutcomeAdoption = "followed" | "not-followed" | "unknown";
19
19
  export type OutcomeValidation = "passed" | "failed" | "not-run" | "unknown";
20
- type OutcomeTrigger =
20
+ export type OutcomeTrigger =
21
21
  | "manual"
22
22
  | "executor-requested"
23
23
  | "turn-gate"
@@ -33,6 +33,12 @@ export const VALIDATIONS: OutcomeValidation[] = [
33
33
  "not-run",
34
34
  "unknown",
35
35
  ];
36
+ export const OUTCOME_TRIGGERS: OutcomeTrigger[] = [
37
+ "manual",
38
+ "executor-requested",
39
+ "turn-gate",
40
+ "repeated-tool-call",
41
+ ];
36
42
  export interface OutcomeRecord {
37
43
  adoption: OutcomeAdoption;
38
44
  adviceHash: string;
@@ -42,6 +48,7 @@ export interface OutcomeRecord {
42
48
  validationStatus: OutcomeValidation;
43
49
  }
44
50
  const MAX_LOG_BYTES = 1024 * 1024;
51
+ export const OUTCOME_LOG_MAX_BYTES = MAX_LOG_BYTES;
45
52
  const statePath = () => join(getAgentDir(), "advisor-outcomes-salt");
46
53
  export const outcomeLogPath = () =>
47
54
  join(getAgentDir(), "advisor-outcomes.jsonl");