@fiale-plus/pi-rogue-bundle 0.1.8 → 0.1.10
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/README.md +24 -13
- package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +257 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1334 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +48 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +301 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +78 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +516 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +96 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
- package/package.json +10 -2
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { appendText, featureFile, readText, sessionFile, sessionKey, truncate } from "./internal.js";
|
|
3
|
+
import { clearResearchState, hasActiveResearch } from "./autoresearch-state.js";
|
|
4
|
+
import { setAdvisorCheckinsEnabled } from "./advisor-checkins.js";
|
|
5
|
+
import { buildGoalCheckPrompt, beginGoalCheck, hasGoalCheckPending } from "./goal-resolution.js";
|
|
6
|
+
import { readSessionJson, writeSessionJson } from "./state.js";
|
|
7
|
+
import { loopArgumentCompletions } from "./completions.js";
|
|
8
|
+
|
|
9
|
+
const FEATURE = "orchestration";
|
|
10
|
+
const LOOP_FILE = "loop.json";
|
|
11
|
+
const GOAL_FILE = "goal.md";
|
|
12
|
+
const LOOP_HISTORY_FILE = featureFile(FEATURE, "loop-history.jsonl");
|
|
13
|
+
const MIN_INTERVAL_MS = 60_000;
|
|
14
|
+
const loopTimers = new Map<string, NodeJS.Timeout>();
|
|
15
|
+
|
|
16
|
+
type LoopState = {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
interval: string;
|
|
19
|
+
instruction: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function defaultLoopState(): LoopState {
|
|
24
|
+
return {
|
|
25
|
+
enabled: false,
|
|
26
|
+
interval: "",
|
|
27
|
+
instruction: "",
|
|
28
|
+
updatedAt: "",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function activeGoal(ctx: any): string {
|
|
33
|
+
return readText(sessionFile(FEATURE, ctx, GOAL_FILE)).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readLoopState(ctx: any): LoopState {
|
|
37
|
+
return readSessionJson(FEATURE, ctx, LOOP_FILE, defaultLoopState());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeLoopState(ctx: any, state: LoopState): LoopState {
|
|
41
|
+
const next: LoopState = { ...state, updatedAt: new Date().toISOString() };
|
|
42
|
+
writeSessionJson(FEATURE, ctx, LOOP_FILE, next);
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clearLoopState(ctx: any): LoopState {
|
|
47
|
+
return writeLoopState(ctx, defaultLoopState());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function archiveLoopState(ctx: any, previous: LoopState): void {
|
|
51
|
+
if (!previous.enabled && !previous.instruction && !previous.interval) return;
|
|
52
|
+
appendText(LOOP_HISTORY_FILE, `${JSON.stringify({
|
|
53
|
+
at: new Date().toISOString(),
|
|
54
|
+
session: sessionKey(ctx),
|
|
55
|
+
previous,
|
|
56
|
+
})}\n`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function clearLoop(ctx: any, options: { clearResearch?: boolean; preserveCheckins?: boolean } = {}): LoopState {
|
|
60
|
+
const current = readLoopState(ctx);
|
|
61
|
+
archiveLoopState(ctx, current);
|
|
62
|
+
const next = clearLoopState(ctx);
|
|
63
|
+
stopLoopTimer(sessionKey(ctx));
|
|
64
|
+
setLoopStatus(ctx, next);
|
|
65
|
+
if (!options.preserveCheckins) {
|
|
66
|
+
setAdvisorCheckinsEnabled(false);
|
|
67
|
+
}
|
|
68
|
+
if (options.clearResearch) {
|
|
69
|
+
clearResearchState(ctx);
|
|
70
|
+
}
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseIntervalMs(interval: string): number | null {
|
|
75
|
+
const raw = interval.trim().toLowerCase();
|
|
76
|
+
const match = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
|
|
79
|
+
const value = Number(match[1]);
|
|
80
|
+
if (!Number.isFinite(value) || value <= 0) return null;
|
|
81
|
+
|
|
82
|
+
const unit = match[2] ?? "s";
|
|
83
|
+
const multiplier = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60_000 : 3_600_000;
|
|
84
|
+
const ms = Math.round(value * multiplier);
|
|
85
|
+
return ms >= MIN_INTERVAL_MS ? ms : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatLoopState(state: LoopState): string {
|
|
89
|
+
if (!state.enabled) {
|
|
90
|
+
return "No active loop.";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const target = state.instruction ? ` — ${truncate(state.instruction, 160)}` : "";
|
|
94
|
+
return `↻ Loop active: every ${state.interval}${target}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setLoopStatus(ctx: any, state: LoopState): void {
|
|
98
|
+
ctx.ui.setStatus(
|
|
99
|
+
"orchestration-loop",
|
|
100
|
+
state.enabled ? `↻ ${state.interval}${state.instruction ? ` · ${truncate(state.instruction, 40)}` : ""}` : undefined,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stopLoopTimer(key: string): void {
|
|
105
|
+
const timer = loopTimers.get(key);
|
|
106
|
+
if (timer) {
|
|
107
|
+
clearInterval(timer);
|
|
108
|
+
loopTimers.delete(key);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let advisorLoopCheckinFn: ((pi: ExtensionAPI, ctx: any, source?: string) => Promise<boolean>) | null | undefined;
|
|
113
|
+
|
|
114
|
+
async function runAdvisorCheckinTick(pi: ExtensionAPI, ctx: any): Promise<void> {
|
|
115
|
+
if (advisorLoopCheckinFn === undefined) {
|
|
116
|
+
try {
|
|
117
|
+
const advisor = await import("@fiale-plus/pi-rogue-advisor");
|
|
118
|
+
const candidate = (advisor as { requestAdvisorLoopCheckin?: (pi: ExtensionAPI, ctx: any, source?: string) => Promise<boolean> }).requestAdvisorLoopCheckin;
|
|
119
|
+
advisorLoopCheckinFn = typeof candidate === "function" ? candidate : null;
|
|
120
|
+
} catch {
|
|
121
|
+
advisorLoopCheckinFn = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!advisorLoopCheckinFn) return;
|
|
126
|
+
await advisorLoopCheckinFn(pi, ctx, "loop_tick");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function runLoopTick(pi: ExtensionAPI, ctx: any): boolean {
|
|
130
|
+
const key = sessionKey(ctx);
|
|
131
|
+
const current = readLoopState(ctx);
|
|
132
|
+
if (!current.enabled || !current.instruction) {
|
|
133
|
+
stopLoopTimer(key);
|
|
134
|
+
setLoopStatus(ctx, current);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const currentIntervalMs = parseIntervalMs(current.interval);
|
|
139
|
+
if (currentIntervalMs === null) {
|
|
140
|
+
stopLoopTimer(key);
|
|
141
|
+
setLoopStatus(ctx, current);
|
|
142
|
+
ctx.ui.notify("Loop interval must be at least 1m (e.g. 1m, 5m, 1h).", "warning");
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const goal = activeGoal(ctx);
|
|
147
|
+
if (goal && hasGoalCheckPending(ctx)) {
|
|
148
|
+
void runAdvisorCheckinTick(pi, ctx);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const prompt = goal ? buildGoalCheckPrompt(goal, current.instruction) : current.instruction;
|
|
153
|
+
ctx.ui.notify(goal ? `🎯 Goal check: ${truncate(goal, 80)}` : `↻ Loop tick: ${truncate(current.instruction, 80)}`, "info");
|
|
154
|
+
if (goal) {
|
|
155
|
+
beginGoalCheck(ctx);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (ctx.isIdle()) {
|
|
159
|
+
pi.sendUserMessage(prompt);
|
|
160
|
+
} else {
|
|
161
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
void runAdvisorCheckinTick(pi, ctx);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function syncLoopTimer(pi: ExtensionAPI, ctx: any): void {
|
|
169
|
+
const key = sessionKey(ctx);
|
|
170
|
+
stopLoopTimer(key);
|
|
171
|
+
|
|
172
|
+
const state = readLoopState(ctx);
|
|
173
|
+
setLoopStatus(ctx, state);
|
|
174
|
+
if (!state.enabled || !state.instruction) {
|
|
175
|
+
setAdvisorCheckinsEnabled(false);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const intervalMs = parseIntervalMs(state.interval);
|
|
180
|
+
if (intervalMs === null) {
|
|
181
|
+
ctx.ui.notify("Loop interval must be at least 1m (e.g. 1m, 5m, 1h).", "warning");
|
|
182
|
+
setAdvisorCheckinsEnabled(false);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
setAdvisorCheckinsEnabled(true);
|
|
187
|
+
const tick = () => {
|
|
188
|
+
const currentIntervalMs = parseIntervalMs(readLoopState(ctx).interval);
|
|
189
|
+
if (currentIntervalMs === null || currentIntervalMs !== intervalMs) {
|
|
190
|
+
syncLoopTimer(pi, ctx);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
runLoopTick(pi, ctx);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
loopTimers.set(key, setInterval(tick, intervalMs));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function startLoop(pi: ExtensionAPI, ctx: any, interval: string, instruction: string, options: { triggerNow?: boolean } = {}): LoopState | null {
|
|
201
|
+
if (!interval || !instruction || parseIntervalMs(interval) === null) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const next = writeLoopState(ctx, {
|
|
206
|
+
enabled: true,
|
|
207
|
+
interval,
|
|
208
|
+
instruction,
|
|
209
|
+
updatedAt: "",
|
|
210
|
+
});
|
|
211
|
+
setAdvisorCheckinsEnabled(true);
|
|
212
|
+
setLoopStatus(ctx, next);
|
|
213
|
+
syncLoopTimer(pi, ctx);
|
|
214
|
+
if (options.triggerNow) {
|
|
215
|
+
triggerLoopTick(pi, ctx);
|
|
216
|
+
}
|
|
217
|
+
return next;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function triggerLoopTick(pi: ExtensionAPI, ctx: any): boolean {
|
|
221
|
+
return runLoopTick(pi, ctx);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function registerLoop(pi: ExtensionAPI): void {
|
|
225
|
+
const p = pi as any;
|
|
226
|
+
if (p.__piRogueLoopRegistered) return;
|
|
227
|
+
p.__piRogueLoopRegistered = true;
|
|
228
|
+
|
|
229
|
+
pi.on("session_start", (_event, ctx) => {
|
|
230
|
+
syncLoopTimer(pi, ctx);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
234
|
+
stopLoopTimer(sessionKey(ctx));
|
|
235
|
+
setLoopStatus(ctx, defaultLoopState());
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
pi.registerCommand("loop", {
|
|
239
|
+
description: "Record, show, or clear the current session loop cadence",
|
|
240
|
+
getArgumentCompletions: (prefix: string) => loopArgumentCompletions(prefix),
|
|
241
|
+
handler: async (args, ctx) => {
|
|
242
|
+
const input = String(args ?? "").trim();
|
|
243
|
+
const [cmd, ...rest] = input.split(/\s+/);
|
|
244
|
+
const resolved = !input ? "status" : ["status", "show", "off", "clear", "stop"].includes(cmd) ? cmd : "set";
|
|
245
|
+
|
|
246
|
+
if (resolved === "status" || resolved === "show") {
|
|
247
|
+
ctx.ui.notify(formatLoopState(readLoopState(ctx)), "info");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (resolved === "off" || resolved === "clear" || resolved === "stop") {
|
|
252
|
+
const clearedResearch = hasActiveResearch(ctx);
|
|
253
|
+
const next = clearLoop(ctx, { clearResearch: true });
|
|
254
|
+
ctx.ui.notify(next.enabled ? formatLoopState(next) : `Loop cleared${clearedResearch ? "; autoresearch status cleared" : ""}.`, "info");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const interval = cmd;
|
|
259
|
+
const instruction = rest.join(" ").trim();
|
|
260
|
+
if (!interval || !instruction || parseIntervalMs(interval) === null) {
|
|
261
|
+
ctx.ui.notify("Usage: /loop <interval> <instruction> (e.g. 1m, 5m, 1h)", "error");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
clearLoop(ctx, { clearResearch: true });
|
|
266
|
+
const next = startLoop(pi, ctx, interval, instruction);
|
|
267
|
+
if (!next) {
|
|
268
|
+
ctx.ui.notify("Usage: /loop <interval> <instruction> (e.g. 1m, 5m, 1h)", "error");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
ctx.ui.notify(formatLoopState(next), "info");
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
detectAssistantRepetition,
|
|
4
|
+
normalizeTurn,
|
|
5
|
+
recordAssistantTurn,
|
|
6
|
+
turnSimilarity,
|
|
7
|
+
type RepetitionGuardState,
|
|
8
|
+
} from "./novelty-guard.js";
|
|
9
|
+
|
|
10
|
+
describe("repetition guard", () => {
|
|
11
|
+
it("normalizes noisy assistant text", () => {
|
|
12
|
+
expect(normalizeTurn("Run `npm test` now. https://example.test")).toBe("run now url");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("keeps similarity high for close paraphrases", () => {
|
|
16
|
+
const similarity = turnSimilarity(
|
|
17
|
+
"Inspect current state and apply the smallest missing delta before retrying.",
|
|
18
|
+
"Inspect the current state, then apply only the smallest missing change before retrying.",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(similarity).toBeGreaterThan(0.8);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("detects repeated assistant output", () => {
|
|
25
|
+
const base: RepetitionGuardState = { recentAssistantTurns: [] };
|
|
26
|
+
const first = recordAssistantTurn(base, "Now let me build the session-flow analyzer and workflow clustering pipeline.");
|
|
27
|
+
const second = recordAssistantTurn(first, "Now let me build the session-flow analyzer and workflow clustering pipeline.");
|
|
28
|
+
const third = recordAssistantTurn(second, "Now let me build the session-flow analyzer and workflow clustering pipeline.");
|
|
29
|
+
|
|
30
|
+
const repeat = detectAssistantRepetition(third);
|
|
31
|
+
|
|
32
|
+
expect(repeat?.count).toBe(3);
|
|
33
|
+
expect(third.assistantRepeat?.text).toContain("session-flow analyzer");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { contentText, readText, sessionFile, truncate, writeText } from "./internal.js";
|
|
3
|
+
|
|
4
|
+
const FEATURE = "orchestration";
|
|
5
|
+
const STATE_FILE = "repetition-guard.json";
|
|
6
|
+
const MAX_ASSISTANT_TURNS = 6;
|
|
7
|
+
const REPEAT_COUNT = 3;
|
|
8
|
+
const REPEAT_THRESHOLD = 0.8;
|
|
9
|
+
|
|
10
|
+
export interface RepetitionGuardTurn {
|
|
11
|
+
at: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RepetitionGuardRepeat {
|
|
16
|
+
at: string;
|
|
17
|
+
count: number;
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RepetitionGuardState {
|
|
22
|
+
recentAssistantTurns: RepetitionGuardTurn[];
|
|
23
|
+
assistantRepeat?: RepetitionGuardRepeat;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function defaultRepetitionGuardState(): RepetitionGuardState {
|
|
27
|
+
return { recentAssistantTurns: [] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeTurn(text: string): string {
|
|
31
|
+
return String(text ?? "")
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/`[^`]*`/g, " ")
|
|
34
|
+
.replace(/https?:\/\/\S+/g, " url ")
|
|
35
|
+
.replace(/[^a-z0-9\s']/g, " ")
|
|
36
|
+
.replace(/\s+/g, " ")
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function turnSimilarity(a: string, b: string): number {
|
|
41
|
+
const left = new Set(normalizeTurn(a).split(" ").filter((token) => token.length > 2));
|
|
42
|
+
const right = new Set(normalizeTurn(b).split(" ").filter((token) => token.length > 2));
|
|
43
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
44
|
+
|
|
45
|
+
let overlap = 0;
|
|
46
|
+
for (const token of left) {
|
|
47
|
+
if (right.has(token)) overlap++;
|
|
48
|
+
}
|
|
49
|
+
return overlap / Math.min(left.size, right.size);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function detectAssistantRepetition(state: RepetitionGuardState, minCount = REPEAT_COUNT): RepetitionGuardRepeat | null {
|
|
53
|
+
const recent = state.recentAssistantTurns.slice(-MAX_ASSISTANT_TURNS);
|
|
54
|
+
const last = recent.at(-1);
|
|
55
|
+
if (!last || normalizeTurn(last.text).length < 16) return null;
|
|
56
|
+
|
|
57
|
+
let count = 1;
|
|
58
|
+
for (let index = recent.length - 2; index >= 0; index--) {
|
|
59
|
+
const candidate = recent[index];
|
|
60
|
+
if (!candidate || normalizeTurn(candidate.text).length < 16) break;
|
|
61
|
+
if (normalizeTurn(candidate.text) !== normalizeTurn(last.text) && turnSimilarity(last.text, candidate.text) < REPEAT_THRESHOLD) break;
|
|
62
|
+
count++;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (count < minCount) return null;
|
|
66
|
+
return {
|
|
67
|
+
at: new Date().toISOString(),
|
|
68
|
+
count,
|
|
69
|
+
text: truncate(last.text, 240),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function recordAssistantTurn(state: RepetitionGuardState, text: string): RepetitionGuardState {
|
|
74
|
+
const trimmed = String(text ?? "").trim();
|
|
75
|
+
if (!trimmed) return state;
|
|
76
|
+
const next: RepetitionGuardState = {
|
|
77
|
+
...state,
|
|
78
|
+
recentAssistantTurns: [...state.recentAssistantTurns, { at: new Date().toISOString(), text: truncate(trimmed, 1200) }].slice(-MAX_ASSISTANT_TURNS),
|
|
79
|
+
};
|
|
80
|
+
const repeat = detectAssistantRepetition(next);
|
|
81
|
+
return repeat ? { ...next, assistantRepeat: repeat } : { ...next, assistantRepeat: undefined };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseState(raw: string): RepetitionGuardState {
|
|
85
|
+
if (!raw.trim()) return defaultRepetitionGuardState();
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(raw) as Partial<RepetitionGuardState>;
|
|
88
|
+
return {
|
|
89
|
+
recentAssistantTurns: Array.isArray(parsed.recentAssistantTurns)
|
|
90
|
+
? parsed.recentAssistantTurns.filter((turn) => typeof turn?.text === "string").slice(-MAX_ASSISTANT_TURNS)
|
|
91
|
+
: [],
|
|
92
|
+
assistantRepeat: parsed.assistantRepeat && typeof parsed.assistantRepeat.text === "string"
|
|
93
|
+
? {
|
|
94
|
+
at: String(parsed.assistantRepeat.at ?? new Date().toISOString()),
|
|
95
|
+
count: Number(parsed.assistantRepeat.count) || REPEAT_COUNT,
|
|
96
|
+
text: parsed.assistantRepeat.text,
|
|
97
|
+
}
|
|
98
|
+
: undefined,
|
|
99
|
+
};
|
|
100
|
+
} catch {
|
|
101
|
+
return defaultRepetitionGuardState();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readGuardState(ctx: any): RepetitionGuardState {
|
|
106
|
+
return parseState(readText(sessionFile(FEATURE, ctx, STATE_FILE)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeGuardState(ctx: any, state: RepetitionGuardState): void {
|
|
110
|
+
writeText(sessionFile(FEATURE, ctx, STATE_FILE), `${JSON.stringify(state, null, 2)}\n`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function registerNoveltyGuard(pi: ExtensionAPI): void {
|
|
114
|
+
const p = pi as any;
|
|
115
|
+
if (p.__piRogueNoveltyGuardRegistered) return;
|
|
116
|
+
p.__piRogueNoveltyGuardRegistered = true;
|
|
117
|
+
|
|
118
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
119
|
+
const state = readGuardState(ctx);
|
|
120
|
+
const repeat = detectAssistantRepetition(state) ?? state.assistantRepeat;
|
|
121
|
+
if (!repeat) return { systemPrompt: event.systemPrompt };
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
systemPrompt: [
|
|
125
|
+
event.systemPrompt,
|
|
126
|
+
"Pi-Rogue repetition guard:",
|
|
127
|
+
`The previous assistant output repeated ${repeat.count} times: ${truncate(repeat.text, 180)}`,
|
|
128
|
+
"Inspect current state before continuing, then apply only the smallest missing delta. Do not repeat the same response.",
|
|
129
|
+
].join("\n\n"),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
pi.on("message_end", async (event, ctx) => {
|
|
134
|
+
if (event?.message?.role !== "assistant") return;
|
|
135
|
+
const text = contentText(event.message.content);
|
|
136
|
+
if (!text) return;
|
|
137
|
+
|
|
138
|
+
const previous = readGuardState(ctx);
|
|
139
|
+
const next = recordAssistantTurn(previous, text);
|
|
140
|
+
writeGuardState(ctx, next);
|
|
141
|
+
if (next.assistantRepeat && (!previous.assistantRepeat || next.assistantRepeat.count > previous.assistantRepeat.count)) {
|
|
142
|
+
ctx.ui.notify("Repetition guard detected repeated assistant output; the next turn will inspect current state before retrying.", "warning");
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readText, sessionFile, writeText } from "./internal.js";
|
|
2
|
+
|
|
3
|
+
export function readSessionText(feature: string, ctx: any, file: string): string {
|
|
4
|
+
return readText(sessionFile(feature, ctx, file)).trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function writeSessionText(feature: string, ctx: any, file: string, text: string): void {
|
|
8
|
+
writeText(sessionFile(feature, ctx, file), text ? `${text}\n` : "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function readSessionJson<T>(feature: string, ctx: any, file: string, fallback: T): T {
|
|
12
|
+
const raw = readSessionText(feature, ctx, file);
|
|
13
|
+
if (!raw) return fallback;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(raw) as T;
|
|
17
|
+
} catch {
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function writeSessionJson(feature: string, ctx: any, file: string, value: unknown): void {
|
|
23
|
+
writeText(sessionFile(feature, ctx, file), `${JSON.stringify(value, null, 2)}\n`);
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiale-plus/pi-rogue-bundle",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Public Pi-Rogue bundle for advisor and orchestration.",
|
|
3
|
+
"version": "0.1.10",
|
|
4
|
+
"description": "Public Pi-Rogue bundle for advisor and orchestration. Single consolidated artefact (advisor and orchestration releases paused; their packages are private and bundled here).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"pi": {
|
|
19
19
|
"extensions": [
|
|
20
20
|
"./src/extension.ts"
|
|
21
|
+
],
|
|
22
|
+
"skills": [
|
|
23
|
+
"./node_modules/@fiale-plus/pi-rogue-advisor/skills",
|
|
24
|
+
"./node_modules/@fiale-plus/pi-rogue-orchestration/skills"
|
|
21
25
|
]
|
|
22
26
|
},
|
|
23
27
|
"peerDependencies": {
|
|
@@ -27,6 +31,10 @@
|
|
|
27
31
|
"@fiale-plus/pi-rogue-advisor": "^0.1.0",
|
|
28
32
|
"@fiale-plus/pi-rogue-orchestration": "^0.1.0"
|
|
29
33
|
},
|
|
34
|
+
"bundledDependencies": [
|
|
35
|
+
"@fiale-plus/pi-rogue-advisor",
|
|
36
|
+
"@fiale-plus/pi-rogue-orchestration"
|
|
37
|
+
],
|
|
30
38
|
"publishConfig": {
|
|
31
39
|
"access": "public"
|
|
32
40
|
}
|