pi-antiloop 1.0.0 → 1.0.1

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/src/types.ts ADDED
@@ -0,0 +1,51 @@
1
+ // antiloop — shared types. Zero runtime cost.
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+
4
+ export interface AntiloopConfig {
5
+ enabled: boolean;
6
+ warningThreshold: number;
7
+ forceBreakThreshold: number;
8
+ abortThreshold: number;
9
+ similarityThreshold: number;
10
+ detectToolLoops: boolean;
11
+ detectThinkingLoops: boolean;
12
+ detectTextLoops: boolean;
13
+ notifyOnDetection: boolean;
14
+ maxHistoryEntries: number;
15
+ detectionWindow: number;
16
+ }
17
+
18
+ export type LoopKind = "text" | "tool" | "thinking" | "structural";
19
+
20
+ export interface LoopDetection {
21
+ type: LoopKind;
22
+ similarity: number;
23
+ messageIndices: number[];
24
+ description: string;
25
+ timestamp: number;
26
+ }
27
+
28
+ export interface TrackedMessage {
29
+ content: string;
30
+ thinking?: string;
31
+ toolCalls?: Array<{ name: string; args: string }>;
32
+ timestamp: number;
33
+ turnIndex: number;
34
+ }
35
+
36
+ export interface AntiloopState {
37
+ recentMessages: TrackedMessage[];
38
+ detections: LoopDetection[];
39
+ currentLevel: 0 | 1 | 2 | 3;
40
+ consecutiveDetections: number;
41
+ inForcedBreak: boolean;
42
+ totalDetections: number;
43
+ lastUserMessageTime: number;
44
+ }
45
+
46
+ export interface Runtime {
47
+ config: AntiloopConfig;
48
+ state: AntiloopState;
49
+ pendingIntervention: string | null;
50
+ updateStatus(ctx: ExtensionContext): void;
51
+ }
package/src/ui.ts ADDED
@@ -0,0 +1,23 @@
1
+ // antiloop — UI helpers. Lazy-loaded with command handler.
2
+
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+
5
+ export function formatDuration(ms: number): string {
6
+ if (ms < 1000) return `${ms}ms`;
7
+ if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
8
+ if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`;
9
+ return `${Math.round(ms / 3_600_000)}h`;
10
+ }
11
+
12
+ // Labeled select: returns the matched `value` from `items`, or undefined if cancelled.
13
+ export async function selectFrom<T>(
14
+ ctx: ExtensionContext,
15
+ title: string,
16
+ items: Array<{ value: T; label: string; description?: string }>,
17
+ ): Promise<T | undefined> {
18
+ const strings = items.map((it) => (it.description ? `${it.label} — ${it.description}` : it.label));
19
+ const picked = await ctx.ui.select(title, strings);
20
+ if (picked === undefined) return undefined;
21
+ const idx = strings.indexOf(picked);
22
+ return idx >= 0 ? items[idx].value : undefined;
23
+ }