pi-advisor-flow 0.9.2-dev.4 → 0.10.0
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 +11 -0
- package/README.md +16 -7
- package/dist/index.js +938 -287
- package/package.json +2 -2
- package/src/commands/activation-preparation.ts +4 -0
- package/src/commands/manual-consultation.ts +20 -3
- package/src/commands/model-commands.ts +5 -1
- package/src/commands/model-picker.ts +44 -2
- 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/runtime.ts +9 -1
- package/src/commands/settings-persistence.ts +4 -0
- package/src/commands/types.ts +5 -2
- package/src/config/defaults.ts +13 -0
- package/src/config/schema.ts +14 -0
- package/src/config/state.ts +10 -0
- package/src/config/types.ts +2 -0
- package/src/config.ts +4 -0
- package/src/follow-up.ts +85 -0
- package/src/outcome-stats.ts +154 -0
- package/src/outcomes.ts +8 -1
- package/src/preferences.ts +90 -12
- package/src/session-state.ts +33 -0
- package/src/tools/consult-context.ts +12 -1
- package/src/tools/consultation.ts +344 -47
- package/src/tools/jev-turn-gate.ts +2 -0
- package/src/tools/loop-gate.ts +2 -0
- package/src/tools/model-access.ts +7 -3
- package/src/tools/prompts.ts +28 -2
- package/src/tools/register-ask-advisor.ts +110 -23
- package/src/tools/register-lifecycle.ts +11 -0
- package/src/tools/register-renderers.ts +40 -6
- package/src/tools/render-advisor-result.ts +6 -0
- package/src/tools/render-common.ts +6 -2
- package/src/tools/types.ts +5 -0
- package/src/ui/settings-items.ts +43 -0
- package/src/ui/settings-mutations.ts +9 -0
- package/src/ui/settings-selector.ts +9 -0
- package/src/ui/types.ts +4 -0
package/src/follow-up.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
|
|
3
|
+
export const FOLLOW_UP_TTL_MS = 5 * 60 * 1000;
|
|
4
|
+
export const FOLLOW_UP_MAX_DEPTH = 3;
|
|
5
|
+
export const FOLLOW_UP_MAX_TOOL_CALLS = 3;
|
|
6
|
+
|
|
7
|
+
export interface AdvisorFollowUpPayload {
|
|
8
|
+
depth: number;
|
|
9
|
+
messages: Message[];
|
|
10
|
+
model: string;
|
|
11
|
+
privacyKey: string;
|
|
12
|
+
redacted: boolean;
|
|
13
|
+
systemPrompt: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AdvisorFollowUpLookup {
|
|
17
|
+
payload?: AdvisorFollowUpPayload;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface StoredFollowUpPayload extends AdvisorFollowUpPayload {
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
toolCallsSince: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const copy = <Value>(value: Value): Value => structuredClone(value);
|
|
27
|
+
|
|
28
|
+
export class AdvisorFollowUpCache {
|
|
29
|
+
readonly #entries = new Map<string, StoredFollowUpPayload>();
|
|
30
|
+
readonly #now: () => number;
|
|
31
|
+
|
|
32
|
+
constructor(now: () => number = Date.now) {
|
|
33
|
+
this.#now = now;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
clear() {
|
|
37
|
+
this.#entries.clear();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
advanceToolCall() {
|
|
41
|
+
const now = this.#now();
|
|
42
|
+
for (const [adviceId, entry] of this.#entries) {
|
|
43
|
+
if (
|
|
44
|
+
now >= entry.expiresAt ||
|
|
45
|
+
entry.toolCallsSince + 1 >= FOLLOW_UP_MAX_TOOL_CALLS
|
|
46
|
+
) {
|
|
47
|
+
this.#entries.delete(adviceId);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
entry.toolCallsSince += 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
capture(
|
|
55
|
+
adviceId: string,
|
|
56
|
+
payload: AdvisorFollowUpPayload,
|
|
57
|
+
replacesAdviceId?: string
|
|
58
|
+
) {
|
|
59
|
+
if (replacesAdviceId) {
|
|
60
|
+
this.#entries.delete(replacesAdviceId);
|
|
61
|
+
}
|
|
62
|
+
this.#entries.set(adviceId, {
|
|
63
|
+
...copy(payload),
|
|
64
|
+
expiresAt: this.#now() + FOLLOW_UP_TTL_MS,
|
|
65
|
+
toolCallsSince: 0,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get(adviceId: string): AdvisorFollowUpLookup {
|
|
70
|
+
const entry = this.#entries.get(adviceId);
|
|
71
|
+
if (!entry) {
|
|
72
|
+
return { reason: "the adviceId is unknown or no longer cached" };
|
|
73
|
+
}
|
|
74
|
+
if (this.#now() >= entry.expiresAt) {
|
|
75
|
+
this.#entries.delete(adviceId);
|
|
76
|
+
return { reason: "the cached payload has expired" };
|
|
77
|
+
}
|
|
78
|
+
if (entry.depth >= FOLLOW_UP_MAX_DEPTH) {
|
|
79
|
+
return {
|
|
80
|
+
reason: `the follow-up chain is limited to ${FOLLOW_UP_MAX_DEPTH} follow-ups`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { payload: copy(entry) };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -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");
|
package/src/preferences.ts
CHANGED
|
@@ -1,36 +1,67 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { lstat, open, realpath } from "node:fs/promises";
|
|
3
|
-
import { join, relative } from "node:path";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
|
|
5
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
|
|
7
8
|
import { redactAndCapText } from "./redaction.ts";
|
|
8
9
|
|
|
9
10
|
const PREFERENCES_MAX_BYTES = 8 * 1024;
|
|
10
|
-
// Keep
|
|
11
|
+
// Keep local filename components separate from Socket's URL-string heuristic.
|
|
11
12
|
const PREFERENCES_FILENAME = ["advisor-preferences", "md"].join(".");
|
|
13
|
+
const AGENTS_FILENAME = "AGENTS.md";
|
|
12
14
|
export interface TextAttachment {
|
|
13
15
|
bytes: number;
|
|
14
16
|
text: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
export interface ProjectRulesAttachment {
|
|
20
|
+
bytes: number;
|
|
21
|
+
global?: TextAttachment;
|
|
22
|
+
project?: TextAttachment;
|
|
23
|
+
withheldReason?: "untrusted";
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
const inside = (root: string, candidate: string) => {
|
|
18
27
|
const path = relative(root, candidate);
|
|
19
|
-
return
|
|
28
|
+
return (
|
|
29
|
+
path === "" ||
|
|
30
|
+
(path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path))
|
|
31
|
+
);
|
|
20
32
|
};
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
const hasSymlinkBelow = async (rootCandidate: string, candidate: string) => {
|
|
35
|
+
const root = resolve(rootCandidate);
|
|
36
|
+
const target = resolve(candidate);
|
|
37
|
+
if (!inside(root, target)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
let current = root;
|
|
41
|
+
for (const part of relative(root, target).split(sep).filter(Boolean)) {
|
|
42
|
+
current = join(current, part);
|
|
43
|
+
const stats = await lstat(current);
|
|
44
|
+
if (stats.isSymbolicLink()) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const readTrustedText = async (
|
|
52
|
+
candidate: string,
|
|
53
|
+
rootCandidate: string,
|
|
54
|
+
maxBytes: number,
|
|
55
|
+
redact: boolean
|
|
27
56
|
): Promise<TextAttachment | undefined> => {
|
|
28
|
-
if (
|
|
57
|
+
if (maxBytes <= 0) {
|
|
29
58
|
return;
|
|
30
59
|
}
|
|
31
60
|
try {
|
|
32
|
-
const root = await realpath(
|
|
33
|
-
|
|
61
|
+
const root = await realpath(rootCandidate);
|
|
62
|
+
if (await hasSymlinkBelow(rootCandidate, candidate)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
34
65
|
const stats = await lstat(candidate);
|
|
35
66
|
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
36
67
|
return;
|
|
@@ -39,6 +70,9 @@ export const readProjectPreferences = async (
|
|
|
39
70
|
if (!inside(root, resolved)) {
|
|
40
71
|
return;
|
|
41
72
|
}
|
|
73
|
+
if (await hasSymlinkBelow(rootCandidate, candidate)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
42
76
|
const flags = constants.O_NOFOLLOW
|
|
43
77
|
? constants.O_RDONLY + constants.O_NOFOLLOW
|
|
44
78
|
: constants.O_RDONLY;
|
|
@@ -61,6 +95,50 @@ export const readProjectPreferences = async (
|
|
|
61
95
|
await file.close();
|
|
62
96
|
}
|
|
63
97
|
} catch {
|
|
64
|
-
// Missing, unreadable, or unsafe
|
|
98
|
+
// Missing, unreadable, or unsafe files are intentionally withheld.
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const readProjectPreferences = (
|
|
103
|
+
ctx: ExtensionContext,
|
|
104
|
+
maxBytes = PREFERENCES_MAX_BYTES,
|
|
105
|
+
redact = true
|
|
106
|
+
): Promise<TextAttachment | undefined> => {
|
|
107
|
+
if (!ctx.isProjectTrusted()) {
|
|
108
|
+
return Promise.resolve(undefined);
|
|
109
|
+
}
|
|
110
|
+
return readTrustedText(
|
|
111
|
+
join(ctx.cwd, ".pi", PREFERENCES_FILENAME),
|
|
112
|
+
ctx.cwd,
|
|
113
|
+
maxBytes,
|
|
114
|
+
redact
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const readProjectRules = async (
|
|
119
|
+
ctx: ExtensionContext,
|
|
120
|
+
maxBytes = PREFERENCES_MAX_BYTES,
|
|
121
|
+
redact = true
|
|
122
|
+
): Promise<ProjectRulesAttachment> => {
|
|
123
|
+
if (!ctx.isProjectTrusted()) {
|
|
124
|
+
return { bytes: 0, withheldReason: "untrusted" };
|
|
65
125
|
}
|
|
126
|
+
const project = await readTrustedText(
|
|
127
|
+
join(ctx.cwd, AGENTS_FILENAME),
|
|
128
|
+
ctx.cwd,
|
|
129
|
+
maxBytes,
|
|
130
|
+
redact
|
|
131
|
+
);
|
|
132
|
+
const remaining = Math.max(0, maxBytes * 2 - (project?.bytes ?? 0));
|
|
133
|
+
const global = await readTrustedText(
|
|
134
|
+
join(getAgentDir(), AGENTS_FILENAME),
|
|
135
|
+
getAgentDir(),
|
|
136
|
+
Math.min(maxBytes, remaining),
|
|
137
|
+
redact
|
|
138
|
+
);
|
|
139
|
+
return {
|
|
140
|
+
bytes: (project?.bytes ?? 0) + (global?.bytes ?? 0),
|
|
141
|
+
global,
|
|
142
|
+
project,
|
|
143
|
+
};
|
|
66
144
|
};
|
package/src/session-state.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { isRecordOf, isString } from "./content-utils.ts";
|
|
2
2
|
import type { JsonValue } from "./content-utils.ts";
|
|
3
|
+
import { AdvisorFollowUpCache } from "./follow-up.ts";
|
|
4
|
+
import type {
|
|
5
|
+
AdvisorFollowUpLookup,
|
|
6
|
+
AdvisorFollowUpPayload,
|
|
7
|
+
} from "./follow-up.ts";
|
|
3
8
|
import { AdvisorJevLedgerState } from "./jev/ledger.ts";
|
|
4
9
|
import type { AdvisorJevUsage } from "./jev/ledger.ts";
|
|
5
10
|
import {
|
|
@@ -22,6 +27,7 @@ type ExecutionEffect = "continued" | "tool-blocked" | "session-blocked";
|
|
|
22
27
|
export interface AdvisorInvocationRecord {
|
|
23
28
|
cost?: number;
|
|
24
29
|
decision?: GateDecision;
|
|
30
|
+
followUp?: boolean;
|
|
25
31
|
executionEffect: ExecutionEffect;
|
|
26
32
|
failure?: string;
|
|
27
33
|
kind: "markdown" | "gate";
|
|
@@ -179,9 +185,14 @@ export class AdvisorSessionState {
|
|
|
179
185
|
#consumedCalls = 0;
|
|
180
186
|
#callReservations = new Set<string>();
|
|
181
187
|
readonly #jev = new AdvisorJevLedgerState();
|
|
188
|
+
readonly #followUps: AdvisorFollowUpCache;
|
|
182
189
|
#sessionTurnOrdinal = 0;
|
|
183
190
|
#turnsSinceConsultation = 0;
|
|
184
191
|
|
|
192
|
+
constructor(now?: () => number) {
|
|
193
|
+
this.#followUps = new AdvisorFollowUpCache(now);
|
|
194
|
+
}
|
|
195
|
+
|
|
185
196
|
resetTask() {
|
|
186
197
|
this.#repetition = freshRepetition();
|
|
187
198
|
this.#blockedReason = undefined;
|
|
@@ -190,6 +201,7 @@ export class AdvisorSessionState {
|
|
|
190
201
|
this.#consumedCalls = 0;
|
|
191
202
|
this.#callReservations.clear();
|
|
192
203
|
this.#jev.reset();
|
|
204
|
+
this.#followUps.clear();
|
|
193
205
|
this.#sessionTurnOrdinal = 0;
|
|
194
206
|
this.#turnsSinceConsultation = 0;
|
|
195
207
|
}
|
|
@@ -268,6 +280,26 @@ export class AdvisorSessionState {
|
|
|
268
280
|
return this.#consumedCalls;
|
|
269
281
|
}
|
|
270
282
|
|
|
283
|
+
advanceFollowUpToolCall() {
|
|
284
|
+
this.#followUps.advanceToolCall();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
clearFollowUps() {
|
|
288
|
+
this.#followUps.clear();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
followUpFor(adviceId: string): AdvisorFollowUpLookup {
|
|
292
|
+
return this.#followUps.get(adviceId);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
captureFollowUp(
|
|
296
|
+
adviceId: string,
|
|
297
|
+
payload: AdvisorFollowUpPayload,
|
|
298
|
+
replacesAdviceId?: string
|
|
299
|
+
) {
|
|
300
|
+
this.#followUps.capture(adviceId, payload, replacesAdviceId);
|
|
301
|
+
}
|
|
302
|
+
|
|
271
303
|
/** Counts one completed executor turn on both turn counters. The ordinal
|
|
272
304
|
* feeds the filter override window and summary positions and is never reset
|
|
273
305
|
* by anything; turnsSinceConsultation resets on every consultation. */
|
|
@@ -495,6 +527,7 @@ export class AdvisorSessionState {
|
|
|
495
527
|
`Budget: ${budget}`,
|
|
496
528
|
`Usage: ${formatAdvisorUsageTotals(totals)}`,
|
|
497
529
|
`Markdown advice: ${markdown.length} responses (${this.#ledger.draftConsultations} with drafts)`,
|
|
530
|
+
`Follow-ups: ${markdown.filter((item) => item.followUp).length}`,
|
|
498
531
|
`Outcome reports: ${this.#ledger.outcomes}`,
|
|
499
532
|
`Gate decisions: ${this.#decisionsLine()}`,
|
|
500
533
|
`Loop matching: normalized tool signatures; ${this.#repetition.interventions} gate intervention${this.#repetition.interventions === 1 ? "" : "s"}`,
|
|
@@ -6,6 +6,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
import { readTrackedFiles, readUntrackedFiles } from "../attachments.ts";
|
|
7
7
|
import type { UntrackedAttachment } from "../attachments.ts";
|
|
8
8
|
import {
|
|
9
|
+
advisorAgentsMdContextRef,
|
|
9
10
|
advisorGitContextMaxCharsRef,
|
|
10
11
|
advisorGitContextRef,
|
|
11
12
|
advisorRedactSecretsRef,
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
ADVISOR_IMAGES_TOTAL_MAX_BYTES,
|
|
24
25
|
selectedConversationImages,
|
|
25
26
|
} from "../images.ts";
|
|
26
|
-
import { readProjectPreferences } from "../preferences.ts";
|
|
27
|
+
import { readProjectPreferences, readProjectRules } from "../preferences.ts";
|
|
27
28
|
import { redactAndCapText, redactSecrets } from "../redaction.ts";
|
|
28
29
|
import { curateAdvisorConversation } from "../scout-curation.ts";
|
|
29
30
|
import { runAdvisorScout } from "../scout.ts";
|
|
@@ -64,6 +65,8 @@ export interface ConsultationContext {
|
|
|
64
65
|
supportsImages: boolean;
|
|
65
66
|
/** Redacted project preferences, if present. */
|
|
66
67
|
preferences?: { bytes: number; text: string };
|
|
68
|
+
/** Redacted trusted AGENTS.md files, or a withheld-project note. */
|
|
69
|
+
projectRules?: Awaited<ReturnType<typeof readProjectRules>>;
|
|
67
70
|
/** Non-cancelling Scout outcome, when Scout ran. */
|
|
68
71
|
scout?: Exclude<ScoutOutcome, { cancelled: true }>;
|
|
69
72
|
/** Redacted tracked-file attachments. */
|
|
@@ -130,6 +133,13 @@ export const assembleConsultationContext = async (
|
|
|
130
133
|
ATTACHMENT_TEXT_MAX_BYTES,
|
|
131
134
|
advisorRedactSecretsRef
|
|
132
135
|
);
|
|
136
|
+
const projectRules = advisorAgentsMdContextRef
|
|
137
|
+
? await readProjectRules(
|
|
138
|
+
ctx,
|
|
139
|
+
ATTACHMENT_TEXT_MAX_BYTES,
|
|
140
|
+
advisorRedactSecretsRef
|
|
141
|
+
)
|
|
142
|
+
: undefined;
|
|
133
143
|
const draftText = options.draft
|
|
134
144
|
? redactAndCapText(
|
|
135
145
|
options.draft,
|
|
@@ -215,6 +225,7 @@ export const assembleConsultationContext = async (
|
|
|
215
225
|
imagePartsSeen: census.imagePartsSeen,
|
|
216
226
|
images,
|
|
217
227
|
preferences,
|
|
228
|
+
projectRules,
|
|
218
229
|
scout: curated.scout,
|
|
219
230
|
supportsImages,
|
|
220
231
|
tracked,
|