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/CHANGELOG.md +5 -0
- package/README.md +7 -2
- package/dist/index.js +479 -167
- package/package.json +1 -1
- package/src/commands/manual-consultation.ts +31 -26
- package/src/commands/outcome-stats.ts +98 -0
- package/src/commands/registration.ts +2 -0
- package/src/commands/renderers.ts +51 -0
- package/src/commands/settings-persistence.ts +2 -0
- package/src/commands/types.ts +3 -2
- package/src/config/defaults.ts +7 -0
- package/src/config/schema.ts +7 -0
- package/src/config/state.ts +5 -0
- package/src/config/types.ts +1 -0
- package/src/config.ts +2 -0
- package/src/outcome-stats.ts +154 -0
- package/src/outcomes.ts +8 -1
- package/src/preferences.ts +90 -12
- package/src/tools/consult-context.ts +12 -1
- package/src/tools/consultation.ts +34 -7
- package/src/tools/jev-turn-gate.ts +2 -0
- package/src/tools/loop-gate.ts +2 -0
- package/src/tools/prompts.ts +28 -2
- package/src/tools/register-ask-advisor.ts +1 -0
- package/src/tools/register-renderers.ts +40 -6
- package/src/tools/render-advisor-result.ts +3 -0
- package/src/tools/types.ts +3 -0
- package/src/ui/settings-items.ts +7 -0
- package/src/ui/settings-mutations.ts +1 -0
- package/src/ui/types.ts +1 -0
package/package.json
CHANGED
|
@@ -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 } =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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);
|
package/src/commands/types.ts
CHANGED
|
@@ -19,10 +19,11 @@ export type ManualConsult = (
|
|
|
19
19
|
gitContext?: GitContextLevel
|
|
20
20
|
) => Promise<{
|
|
21
21
|
adviceId?: string;
|
|
22
|
-
|
|
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
|
|
package/src/config/defaults.ts
CHANGED
|
@@ -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);
|
package/src/config/schema.ts
CHANGED
|
@@ -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,
|
package/src/config/state.ts
CHANGED
|
@@ -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,
|
package/src/config/types.ts
CHANGED
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");
|