@monotykamary/pi-supervisor 0.5.9
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 +120 -0
- package/LICENSE +21 -0
- package/README.md +341 -0
- package/media/demo.mp4 +0 -0
- package/media/screenshot.png +0 -0
- package/package.json +87 -0
- package/src/compaction/brief.ts +841 -0
- package/src/compaction/build-sections.ts +340 -0
- package/src/compaction/causal-keys.ts +138 -0
- package/src/compaction/content.ts +68 -0
- package/src/compaction/extract/commits.ts +78 -0
- package/src/compaction/extract/goals.ts +79 -0
- package/src/compaction/extract/preferences.ts +52 -0
- package/src/compaction/extract/shared-symbols.ts +376 -0
- package/src/compaction/filter-noise.ts +47 -0
- package/src/compaction/format.ts +89 -0
- package/src/compaction/index.ts +38 -0
- package/src/compaction/normalize.ts +73 -0
- package/src/compaction/sanitize.ts +5 -0
- package/src/compaction/sections.ts +19 -0
- package/src/compaction/skill-collapse.ts +35 -0
- package/src/compaction/tool-args.ts +14 -0
- package/src/compaction/types.ts +26 -0
- package/src/core/analyzer.ts +58 -0
- package/src/core/index.ts +8 -0
- package/src/core/inference.ts +77 -0
- package/src/core/prompt-builder.ts +137 -0
- package/src/core/prompt-loader.ts +125 -0
- package/src/core/reframe.ts +27 -0
- package/src/fabric-provider.ts +115 -0
- package/src/global-config.ts +65 -0
- package/src/index.ts +514 -0
- package/src/session/client.ts +46 -0
- package/src/session/response-parser.ts +37 -0
- package/src/session/supervisor-session.ts +102 -0
- package/src/state/manager.ts +133 -0
- package/src/state/mid-run-signals.ts +103 -0
- package/src/state/patterns.ts +82 -0
- package/src/state/reframe.ts +27 -0
- package/src/subagent-detector.ts +94 -0
- package/src/types.ts +42 -0
- package/src/ui/animations.ts +95 -0
- package/src/ui/model-picker.ts +72 -0
- package/src/ui/model-settings-selector.ts +440 -0
- package/src/ui/model-sort.ts +101 -0
- package/src/ui/renderer.ts +314 -0
- package/src/ui/types.ts +48 -0
- package/tests/compaction.test.ts +507 -0
- package/tests/engine.test.ts +622 -0
- package/tests/ephemeral-supervision.test.ts +347 -0
- package/tests/fabric-provider.test.ts +55 -0
- package/tests/full-fidelity-snapshot.test.ts +250 -0
- package/tests/global-config.test.ts +74 -0
- package/tests/model-sort.test.ts +157 -0
- package/tests/parsing.test.ts +303 -0
- package/tests/state.test.ts +474 -0
- package/tests/status-widget.test.ts +539 -0
- package/tests/subagent-detector.test.ts +191 -0
- package/tests/supervise-command.test.ts +363 -0
- package/tests/supervise-model-command.test.ts +184 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Message } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { NormalizedBlock } from './types';
|
|
3
|
+
import { textOf } from './content';
|
|
4
|
+
import { sanitize } from './sanitize';
|
|
5
|
+
|
|
6
|
+
const normalizeOne = (msg: Message, msgIndex: number): NormalizedBlock[] => {
|
|
7
|
+
if (msg.role === 'user') {
|
|
8
|
+
const blocks: NormalizedBlock[] = [];
|
|
9
|
+
const text = sanitize(textOf(msg.content));
|
|
10
|
+
if (text) blocks.push({ kind: 'user', text, sourceIndex: msgIndex });
|
|
11
|
+
if (msg.content && typeof msg.content !== 'string') {
|
|
12
|
+
for (const part of msg.content) {
|
|
13
|
+
if (part.type === 'image') {
|
|
14
|
+
blocks.push({ kind: 'user', text: `[image: ${part.mimeType}]`, sourceIndex: msgIndex });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return blocks.length > 0 ? blocks : [{ kind: 'user', text: '', sourceIndex: msgIndex }];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if ((msg as any).role === 'bashExecution') {
|
|
22
|
+
const cmd = (msg as any).command ?? '';
|
|
23
|
+
const out = (msg as any).output ?? '';
|
|
24
|
+
const exit = (msg as any).exitCode;
|
|
25
|
+
return [{ kind: 'bash', command: cmd, output: out, exitCode: exit, sourceIndex: msgIndex }];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (msg.role === 'toolResult') {
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
kind: 'tool_result',
|
|
32
|
+
name: msg.toolName,
|
|
33
|
+
text: sanitize(textOf(msg.content)),
|
|
34
|
+
isError: msg.isError,
|
|
35
|
+
sourceIndex: msgIndex,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (msg.role === 'assistant') {
|
|
41
|
+
if (!msg.content) return [];
|
|
42
|
+
if (typeof msg.content === 'string') {
|
|
43
|
+
return [{ kind: 'assistant', text: sanitize(msg.content), sourceIndex: msgIndex }];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const blocks: NormalizedBlock[] = [];
|
|
47
|
+
for (const part of msg.content) {
|
|
48
|
+
if (part.type === 'text') {
|
|
49
|
+
blocks.push({ kind: 'assistant', text: sanitize(part.text), sourceIndex: msgIndex });
|
|
50
|
+
} else if (part.type === 'thinking') {
|
|
51
|
+
blocks.push({
|
|
52
|
+
kind: 'thinking',
|
|
53
|
+
text: sanitize(part.thinking),
|
|
54
|
+
redacted: part.redacted ?? false,
|
|
55
|
+
sourceIndex: msgIndex,
|
|
56
|
+
});
|
|
57
|
+
} else if (part.type === 'toolCall') {
|
|
58
|
+
blocks.push({
|
|
59
|
+
kind: 'tool_call',
|
|
60
|
+
name: part.name,
|
|
61
|
+
args: part.arguments,
|
|
62
|
+
sourceIndex: msgIndex,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return blocks;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const normalize = (messages: Message[]): NormalizedBlock[] =>
|
|
73
|
+
messages.flatMap((msg, i) => normalizeOne(msg, i));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TranscriptEntry } from './brief';
|
|
2
|
+
import type { SymbolRef } from './extract/shared-symbols';
|
|
3
|
+
|
|
4
|
+
export interface SectionData {
|
|
5
|
+
sessionGoal: string[];
|
|
6
|
+
outstandingContext: string[];
|
|
7
|
+
filesAndChanges: string[];
|
|
8
|
+
commits: string[];
|
|
9
|
+
userPreferences: string[];
|
|
10
|
+
/** Exported signatures from modified/read files */
|
|
11
|
+
typeCatalog: string[];
|
|
12
|
+
/** Symbol-level changes (function/type/class names per file) */
|
|
13
|
+
symbolChanges: SymbolRef[];
|
|
14
|
+
/** Per-turn one-liner summaries for the HCA zone (heaviest compression, oldest turns) */
|
|
15
|
+
turnSummaries: string[];
|
|
16
|
+
briefTranscript: string;
|
|
17
|
+
/** Structured transcript entries (verbose object format) */
|
|
18
|
+
transcriptEntries: TranscriptEntry[];
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Shared skill-tag collapse utilities */
|
|
2
|
+
|
|
3
|
+
const SKILL_TAG_RE = /^-?\s*<skill\s+name="([^"]+)"/;
|
|
4
|
+
const SKILL_CLOSE_RE = /^-?\s*<\/skill>/;
|
|
5
|
+
|
|
6
|
+
/** Collapse skill tags in an array of lines — dedup by name, drop all content inside block */
|
|
7
|
+
export const collapseSkillLines = (lines: string[]): string[] => {
|
|
8
|
+
const result: string[] = [];
|
|
9
|
+
const seenSkills = new Set<string>();
|
|
10
|
+
let insideSkill = false;
|
|
11
|
+
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
const skillMatch = line.match(SKILL_TAG_RE);
|
|
14
|
+
if (skillMatch) {
|
|
15
|
+
insideSkill = true;
|
|
16
|
+
const name = skillMatch[1];
|
|
17
|
+
if (!seenSkills.has(name)) {
|
|
18
|
+
seenSkills.add(name);
|
|
19
|
+
result.push(`[skill: ${name}]`);
|
|
20
|
+
}
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (insideSkill) {
|
|
24
|
+
if (SKILL_CLOSE_RE.test(line)) insideSkill = false;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
result.push(line);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Collapse <skill name="X" ...>...</skill> blocks in raw text */
|
|
33
|
+
const SKILL_BLOCK_RE = /<skill\s+name="([^"]+)"[^>]*>[\s\S]*?(?:<\/skill>|$)/g;
|
|
34
|
+
export const collapseSkillText = (text: string): string =>
|
|
35
|
+
text.replace(SKILL_BLOCK_RE, (_, name) => `[skill: ${name}]`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const extractPath = (args: Record<string, unknown>): string | null => {
|
|
2
|
+
for (const key of ['path', 'file_path', 'filePath', 'file']) {
|
|
3
|
+
if (typeof args[key] === 'string') return args[key] as string;
|
|
4
|
+
}
|
|
5
|
+
return null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const summarizeToolArgs = (args: Record<string, unknown>): string => {
|
|
9
|
+
const path = extractPath(args);
|
|
10
|
+
if (path) return `path=${path}`;
|
|
11
|
+
if (typeof args.command === 'string') return `command=${args.command}`;
|
|
12
|
+
if (typeof args.query === 'string') return `query=${args.query}`;
|
|
13
|
+
return Object.keys(args).join(', ');
|
|
14
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Message } from '@earendil-works/pi-ai';
|
|
2
|
+
|
|
3
|
+
export interface FileOps {
|
|
4
|
+
readFiles?: string[];
|
|
5
|
+
modifiedFiles?: string[];
|
|
6
|
+
createdFiles?: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Pre-computed look-ahead index: maps tool_call index → nearest tool_result block. */
|
|
10
|
+
export interface ToolResultIndex {
|
|
11
|
+
get(callIndex: number): Extract<NormalizedBlock, { kind: 'tool_result' }> | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type NormalizedBlock =
|
|
15
|
+
| { kind: 'user'; text: string; sourceIndex?: number }
|
|
16
|
+
| { kind: 'assistant'; text: string; sourceIndex?: number }
|
|
17
|
+
| { kind: 'tool_call'; name: string; args: Record<string, unknown>; sourceIndex?: number }
|
|
18
|
+
| { kind: 'tool_result'; name: string; text: string; isError: boolean; sourceIndex?: number }
|
|
19
|
+
| {
|
|
20
|
+
kind: 'bash';
|
|
21
|
+
command: string;
|
|
22
|
+
output: string;
|
|
23
|
+
exitCode: number | undefined;
|
|
24
|
+
sourceIndex?: number;
|
|
25
|
+
}
|
|
26
|
+
| { kind: 'thinking'; text: string; redacted: boolean; sourceIndex?: number };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main analyzer - orchestrates supervisor analysis using compaction-based context.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import type { SteeringDecision, SupervisorState } from '../types.js';
|
|
7
|
+
import { callSupervisorModel } from '../session/client.js';
|
|
8
|
+
import { loadSystemPrompt } from './prompt-loader.js';
|
|
9
|
+
import {
|
|
10
|
+
buildCompactionSummary,
|
|
11
|
+
extractMessages,
|
|
12
|
+
formatForSupervisor,
|
|
13
|
+
} from '../compaction/index.js';
|
|
14
|
+
import { buildUserPrompt } from './prompt-builder.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Analyze the current conversation and return a steering decision.
|
|
18
|
+
* Falls back to { action: "steer" } when the agent is idle to prevent it from staying stuck.
|
|
19
|
+
*/
|
|
20
|
+
export async function analyze(
|
|
21
|
+
ctx: ExtensionContext,
|
|
22
|
+
state: SupervisorState,
|
|
23
|
+
agentIsIdle: boolean,
|
|
24
|
+
ineffectivePattern?: { detected: boolean; similarCount: number; secondsSinceLastSteer: number },
|
|
25
|
+
signal?: AbortSignal,
|
|
26
|
+
onDelta?: (accumulated: string) => void
|
|
27
|
+
): Promise<SteeringDecision> {
|
|
28
|
+
const { prompt: systemPrompt } = loadSystemPrompt(ctx.cwd);
|
|
29
|
+
|
|
30
|
+
// Build structured compaction summary from current branch
|
|
31
|
+
const messages = extractMessages(ctx);
|
|
32
|
+
const summary = buildCompactionSummary(messages);
|
|
33
|
+
const contextText = formatForSupervisor(summary);
|
|
34
|
+
|
|
35
|
+
const userPrompt = buildUserPrompt(state, contextText, agentIsIdle, ineffectivePattern);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return await callSupervisorModel(
|
|
39
|
+
ctx,
|
|
40
|
+
state.provider,
|
|
41
|
+
state.modelId,
|
|
42
|
+
systemPrompt,
|
|
43
|
+
userPrompt,
|
|
44
|
+
signal,
|
|
45
|
+
onDelta
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
// When idle and analysis fails, nudge rather than silently do nothing
|
|
49
|
+
return agentIsIdle
|
|
50
|
+
? {
|
|
51
|
+
action: 'steer',
|
|
52
|
+
message: 'Please continue working toward the goal.',
|
|
53
|
+
reasoning: 'Analysis error',
|
|
54
|
+
confidence: 0,
|
|
55
|
+
}
|
|
56
|
+
: { action: 'continue', reasoning: 'Analysis error', confidence: 0 };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcome inference from conversation history.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { SupervisorSession } from '../session/supervisor-session.js';
|
|
7
|
+
import {
|
|
8
|
+
extractMessages,
|
|
9
|
+
buildCompactionSummary,
|
|
10
|
+
formatForSupervisor,
|
|
11
|
+
} from '../compaction/index.js';
|
|
12
|
+
|
|
13
|
+
/** System prompt for inferring an outcome from conversation history. */
|
|
14
|
+
const INFER_OUTCOME_SYSTEM_PROMPT = `You are a goal extraction assistant. Your task is to analyze a conversation between a user and a coding AI assistant, and extract the user's primary desired outcome or goal.
|
|
15
|
+
|
|
16
|
+
The outcome should be:
|
|
17
|
+
- Specific and measurable (not vague like "make it better")
|
|
18
|
+
- Action-oriented (what needs to be built, fixed, or achieved)
|
|
19
|
+
- Concise (1-2 sentences, ideally under 100 characters)
|
|
20
|
+
- Focused on the core intent, not implementation details
|
|
21
|
+
|
|
22
|
+
Examples of good outcomes:
|
|
23
|
+
- "Add JWT authentication with refresh tokens and test coverage"
|
|
24
|
+
- "Refactor the database layer to use connection pooling"
|
|
25
|
+
- "Fix the memory leak in the file upload handler"
|
|
26
|
+
- "Implement dark mode toggle with system preference detection"
|
|
27
|
+
|
|
28
|
+
Respond with ONLY the outcome statement. No quotes, no markdown, no explanations.`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Infer a supervision outcome from the conversation history.
|
|
32
|
+
* Uses the compaction pipeline to build a structured summary for inference.
|
|
33
|
+
*/
|
|
34
|
+
export async function inferOutcome(
|
|
35
|
+
ctx: ExtensionContext,
|
|
36
|
+
provider: string,
|
|
37
|
+
modelId: string,
|
|
38
|
+
signal?: AbortSignal
|
|
39
|
+
): Promise<string | null> {
|
|
40
|
+
const messages = extractMessages(ctx);
|
|
41
|
+
if (messages.length === 0) return null;
|
|
42
|
+
|
|
43
|
+
// Use the compaction pipeline for structured context (avoids cold prefills)
|
|
44
|
+
const summary = buildCompactionSummary(messages);
|
|
45
|
+
const contextText = formatForSupervisor(summary);
|
|
46
|
+
|
|
47
|
+
if (!contextText) return null;
|
|
48
|
+
|
|
49
|
+
const userPrompt = `Analyze this conversation summary and extract the user's primary goal or desired outcome:
|
|
50
|
+
|
|
51
|
+
${contextText}
|
|
52
|
+
|
|
53
|
+
What is the specific outcome the user is trying to achieve?`;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const session = new SupervisorSession();
|
|
57
|
+
const started = await session.ensureStarted(
|
|
58
|
+
ctx,
|
|
59
|
+
provider,
|
|
60
|
+
modelId,
|
|
61
|
+
INFER_OUTCOME_SYSTEM_PROMPT
|
|
62
|
+
);
|
|
63
|
+
if (!started) return null;
|
|
64
|
+
|
|
65
|
+
const result = await session.prompt(userPrompt, signal);
|
|
66
|
+
session.dispose();
|
|
67
|
+
|
|
68
|
+
if (!result) return null;
|
|
69
|
+
return result
|
|
70
|
+
.replace(/^["']|["']$/g, '')
|
|
71
|
+
.replace(/\n/g, ' ')
|
|
72
|
+
.trim()
|
|
73
|
+
.slice(0, 200);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt builder - constructs user prompts for the supervisor LLM
|
|
3
|
+
* using structured compaction output instead of raw message dumps.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SupervisorState, SupervisorIntervention } from '../types.js';
|
|
7
|
+
import { getReframeGuidance } from './reframe.js';
|
|
8
|
+
|
|
9
|
+
/** Build the user-facing prompt for the supervisor LLM. */
|
|
10
|
+
export function buildUserPrompt(
|
|
11
|
+
state: SupervisorState,
|
|
12
|
+
contextText: string,
|
|
13
|
+
agentIsIdle: boolean,
|
|
14
|
+
ineffectivePattern?: { detected: boolean; similarCount: number; secondsSinceLastSteer: number }
|
|
15
|
+
): string {
|
|
16
|
+
// Build intervention history with full ASI display
|
|
17
|
+
const interventionHistory =
|
|
18
|
+
state.interventions.length === 0
|
|
19
|
+
? 'None yet.'
|
|
20
|
+
: state.interventions
|
|
21
|
+
.slice(-5)
|
|
22
|
+
.map((iv, i) => {
|
|
23
|
+
let entry = `[${i + 1}] "${iv.message}"`;
|
|
24
|
+
|
|
25
|
+
if (iv.asi && Object.keys(iv.asi).length > 0) {
|
|
26
|
+
const asiEntries = Object.entries(iv.asi)
|
|
27
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
28
|
+
.join(', ');
|
|
29
|
+
entry += `\n ASI {${asiEntries}}`;
|
|
30
|
+
}
|
|
31
|
+
return entry;
|
|
32
|
+
})
|
|
33
|
+
.join('\n');
|
|
34
|
+
|
|
35
|
+
// Build ASI pattern summary for loop closing
|
|
36
|
+
const asiSummary = buildASISummary(state.interventions);
|
|
37
|
+
|
|
38
|
+
const agentStatus = agentIsIdle
|
|
39
|
+
? `AGENT STATUS: IDLE — the agent has finished its turn and is now waiting for user input.
|
|
40
|
+
You MUST return "done" or "steer". Returning "continue" here means the agent stays idle forever.`
|
|
41
|
+
: `AGENT STATUS: WORKING — the agent is actively processing. Only intervene if clearly off track.`;
|
|
42
|
+
|
|
43
|
+
const reframeGuidance = getReframeGuidance(state.reframeTier ?? 0, ineffectivePattern);
|
|
44
|
+
const reframeSection = reframeGuidance ? `\n${reframeGuidance}\n` : '';
|
|
45
|
+
|
|
46
|
+
const contextBlock = contextText
|
|
47
|
+
? `STRUCTURED CONVERSATION CONTEXT:\n${contextText}`
|
|
48
|
+
: '(No conversation context available)';
|
|
49
|
+
|
|
50
|
+
return `DESIRED OUTCOME:
|
|
51
|
+
${state.outcome}
|
|
52
|
+
|
|
53
|
+
${agentStatus}${reframeSection}
|
|
54
|
+
|
|
55
|
+
${contextBlock}
|
|
56
|
+
|
|
57
|
+
YOUR INTERVENTION HISTORY (with ASI observations):
|
|
58
|
+
${interventionHistory}
|
|
59
|
+
|
|
60
|
+
${asiSummary}
|
|
61
|
+
REMINDER — DESIRED OUTCOME:
|
|
62
|
+
${state.outcome}
|
|
63
|
+
|
|
64
|
+
Has this outcome been fully achieved? Analyze and respond with JSON only.`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Build summary of ASI patterns to close the loop */
|
|
68
|
+
function buildASISummary(interventions: SupervisorIntervention[]): string {
|
|
69
|
+
if (interventions.length === 0) return '';
|
|
70
|
+
|
|
71
|
+
const patterns: string[] = [];
|
|
72
|
+
const recent = interventions.slice(-5);
|
|
73
|
+
|
|
74
|
+
const keyFrequency: Record<string, number> = {};
|
|
75
|
+
for (const iv of recent) {
|
|
76
|
+
if (!iv.asi) continue;
|
|
77
|
+
for (const key of Object.keys(iv.asi)) {
|
|
78
|
+
keyFrequency[key] = (keyFrequency[key] || 0) + 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const [key, count] of Object.entries(keyFrequency)) {
|
|
83
|
+
if (count >= 2) {
|
|
84
|
+
patterns.push(`Pattern seen ${count}x: "${key}"`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const allValues = recent
|
|
89
|
+
.filter((iv) => iv.asi)
|
|
90
|
+
.flatMap((iv) => Object.values(iv.asi!))
|
|
91
|
+
.map((v) => String(v).toLowerCase());
|
|
92
|
+
|
|
93
|
+
const suspiciousIndicators = [
|
|
94
|
+
'unverified',
|
|
95
|
+
'contradict',
|
|
96
|
+
'suspicious',
|
|
97
|
+
'fake',
|
|
98
|
+
'skip',
|
|
99
|
+
'manipulat',
|
|
100
|
+
'cheat',
|
|
101
|
+
'gaming',
|
|
102
|
+
'short-circuit',
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const hasSuspicious = suspiciousIndicators.some((indicator) =>
|
|
106
|
+
allValues.some((v) => v.includes(indicator))
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (hasSuspicious) {
|
|
110
|
+
patterns.push(
|
|
111
|
+
'⚠️ Previous interventions flagged suspicious claims — require explicit proof before accepting "done"'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const verificationFailures = interventions.filter(
|
|
116
|
+
(iv) =>
|
|
117
|
+
iv.asi &&
|
|
118
|
+
Object.entries(iv.asi).some(
|
|
119
|
+
([k, v]) =>
|
|
120
|
+
String(v).toLowerCase().includes('contradict') ||
|
|
121
|
+
String(v).toLowerCase().includes('unverified')
|
|
122
|
+
)
|
|
123
|
+
).length;
|
|
124
|
+
|
|
125
|
+
if (verificationFailures >= 2) {
|
|
126
|
+
patterns.push(
|
|
127
|
+
`⚠️ ${verificationFailures} interventions involved unverified/contradicted claims — agent has pattern of unreliable reporting`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (patterns.length === 0) return '';
|
|
132
|
+
|
|
133
|
+
return `ASI PATTERN SUMMARY (use this to inform your decision):
|
|
134
|
+
${patterns.map((p) => `- ${p}`).join('\n')}
|
|
135
|
+
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompt loading for supervisor.
|
|
3
|
+
*
|
|
4
|
+
* Discovery order (mirrors pi's SYSTEM.md convention):
|
|
5
|
+
* 1. <cwd>/.pi/SUPERVISOR.md — project-local
|
|
6
|
+
* 2. ~/.pi/agent/SUPERVISOR.md — global
|
|
7
|
+
* 3. Built-in template — fallback
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
13
|
+
|
|
14
|
+
const SUPERVISOR_MD = 'SUPERVISOR.md';
|
|
15
|
+
const CONFIG_DIR = '.pi';
|
|
16
|
+
|
|
17
|
+
/** Built-in fallback system prompt. */
|
|
18
|
+
const BUILTIN_SYSTEM_PROMPT = `You are a supervisor monitoring a coding AI assistant conversation.
|
|
19
|
+
Your job: ensure the assistant fully achieves a specific outcome without needing the human to intervene.
|
|
20
|
+
|
|
21
|
+
═══ WHEN THE AGENT IS IDLE (finished its turn, waiting for user input) ═══
|
|
22
|
+
This is your most important moment. The agent has stopped and is waiting.
|
|
23
|
+
You MUST choose "done" or "steer". Never return "continue" when the agent is idle.
|
|
24
|
+
|
|
25
|
+
- "done" → only when the outcome is completely and verifiably achieved.
|
|
26
|
+
- "steer" → everything else: incomplete work, partial progress, open questions, waiting for confirmation.
|
|
27
|
+
|
|
28
|
+
If the agent asked a clarifying question or needs a decision:
|
|
29
|
+
FIRST check: is this question necessary to achieve the goal?
|
|
30
|
+
- YES (directly blocks goal progress): answer with a sensible default and tell agent to proceed.
|
|
31
|
+
- NO (out of scope, nice-to-have, unrelated feature): do NOT answer it. Redirect:
|
|
32
|
+
"That's outside the scope of the goal. Focus on: [restate the specific missing piece of the goal]."
|
|
33
|
+
DO NOT answer: passwords, credentials, secrets, anything requiring real user knowledge.
|
|
34
|
+
|
|
35
|
+
Your steer message speaks AS the user. Make it clear, direct, and actionable (1–3 sentences).
|
|
36
|
+
Do not ask the agent to verify its own work — tell it what to do next.
|
|
37
|
+
|
|
38
|
+
═══ WHEN THE AGENT IS ACTIVELY WORKING (mid-turn) ═══
|
|
39
|
+
Only intervene if it is clearly heading in the wrong direction.
|
|
40
|
+
Trust the agent to complete what it has started. Avoid interrupting productive work.
|
|
41
|
+
|
|
42
|
+
═══ STEERING RULES ═══
|
|
43
|
+
- Be specific: reference the outcome, missing pieces, or the question being answered.
|
|
44
|
+
- Never repeat a steering message that had no effect — escalate or change approach.
|
|
45
|
+
- A good steer answers the agent's question OR redirects to the missing piece of the outcome.
|
|
46
|
+
- If the agent is taking shortcuts to satisfy the goal without properly achieving it, always steer and remind it not to take shortcuts.
|
|
47
|
+
|
|
48
|
+
"done" CRITERIA: The core outcome is complete and functional. Minor polish, style tweaks, or
|
|
49
|
+
optional improvements do NOT block "done". Prefer stopping when the goal is substantially
|
|
50
|
+
achieved rather than looping forever chasing perfection.
|
|
51
|
+
|
|
52
|
+
═══ CHEATING PREVENTION ═══
|
|
53
|
+
The agent may try to game metrics, fake results, or take shortcuts. Watch for:
|
|
54
|
+
|
|
55
|
+
1. **Unverified Claims**: Agent says "tests pass" or "works correctly" without showing proof.
|
|
56
|
+
→ Check tool_results for actual exit codes and output.
|
|
57
|
+
|
|
58
|
+
2. **Test Manipulation**: Agent edits test files to weaken assertions or skip failing tests.
|
|
59
|
+
→ Watch for edits that remove/modify test assertions while claiming progress.
|
|
60
|
+
|
|
61
|
+
3. **Metric Gaming**: Agent reports performance improvements without proof, or modifies measurement code instead of actual implementation.
|
|
62
|
+
→ Verify metrics appear in actual command output, not just agent claims.
|
|
63
|
+
|
|
64
|
+
4. **Short-Circuiting**: Agent skips required steps (e.g., doesn't run full test suite, uses smaller dataset).
|
|
65
|
+
→ Check that claimed progress matches the actual work done.
|
|
66
|
+
|
|
67
|
+
5. **Contradictions**: Agent claims success but tool output shows errors/failures.
|
|
68
|
+
→ This is immediate grounds for steering — do not accept "done" until resolved.
|
|
69
|
+
|
|
70
|
+
If you detect cheating or suspicious claims:
|
|
71
|
+
- DO NOT accept "done" — steer instead with specific challenge
|
|
72
|
+
- Require explicit proof: "Show the full test output" or "Run the verification command again"
|
|
73
|
+
- Log the pattern in ASI so future you remembers not to trust unverified claims
|
|
74
|
+
|
|
75
|
+
═══ CLOSING THE ASI LOOP ═══
|
|
76
|
+
ASI (Actionable Side Information) is your memory across turns. Use it to build up context
|
|
77
|
+
that would otherwise be lost to the 6-message window.
|
|
78
|
+
|
|
79
|
+
When you steer, you MUST populate "asi" with observations that will help future you:
|
|
80
|
+
|
|
81
|
+
- What pattern made you intervene? (e.g., "agent_claimed_tests_pass_but_exit_code_1")
|
|
82
|
+
- What have you learned about the agent's behavior? (e.g., "tends_to_skip_error_handling")
|
|
83
|
+
- What should future you watch for? (e.g., "verify_file_actually_written_before_done")
|
|
84
|
+
|
|
85
|
+
Before deciding, READ your past ASI entries. Look for:
|
|
86
|
+
- Recurring patterns (agent keeps making same mistake)
|
|
87
|
+
- Unverified claims from prior turns (don't accept "done" if you previously caught a lie)
|
|
88
|
+
- Your own past observations about what works
|
|
89
|
+
|
|
90
|
+
ASI is free-form: use whatever keys help you remember. Examples:
|
|
91
|
+
{ "repeated_unverified_claim": true, "previous_contradiction": "turn_3", "watch_for": "early_returns" }
|
|
92
|
+
|
|
93
|
+
If you previously caught the agent in a suspicious claim, require explicit proof before "done".
|
|
94
|
+
|
|
95
|
+
Respond ONLY with valid JSON — no prose, no markdown fences.
|
|
96
|
+
Response schema (strict JSON):
|
|
97
|
+
{
|
|
98
|
+
"action": "continue" | "steer" | "done",
|
|
99
|
+
"message": "...", // Required when action === "steer"
|
|
100
|
+
"reasoning": "...", // Brief internal reasoning
|
|
101
|
+
"confidence": 0.85, // Float 0-1
|
|
102
|
+
"asi": { // REQUIRED when steering. Log observations for future decisions.
|
|
103
|
+
"...": "any keys you find useful for future pattern detection"
|
|
104
|
+
}
|
|
105
|
+
}`;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Load the supervisor system prompt.
|
|
109
|
+
* Checks .pi/SUPERVISOR.md (project) then ~/.pi/agent/SUPERVISOR.md (global),
|
|
110
|
+
* falling back to the built-in template if neither exists.
|
|
111
|
+
* Returns both the prompt and its source path (or "built-in").
|
|
112
|
+
*/
|
|
113
|
+
export function loadSystemPrompt(cwd: string): { prompt: string; source: string } {
|
|
114
|
+
const projectPath = join(cwd, CONFIG_DIR, SUPERVISOR_MD);
|
|
115
|
+
if (existsSync(projectPath)) {
|
|
116
|
+
return { prompt: readFileSync(projectPath, 'utf-8').trim(), source: projectPath };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const globalPath = join(getAgentDir(), SUPERVISOR_MD);
|
|
120
|
+
if (existsSync(globalPath)) {
|
|
121
|
+
return { prompt: readFileSync(globalPath, 'utf-8').trim(), source: globalPath };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { prompt: BUILTIN_SYSTEM_PROMPT, source: 'built-in' };
|
|
125
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reframe tier management - escalation strategies for ineffective steering.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Get reframe guidance based on current tier */
|
|
6
|
+
export function getReframeGuidance(
|
|
7
|
+
tier: number,
|
|
8
|
+
ineffectivePattern?: { detected: boolean; similarCount: number; secondsSinceLastSteer: number }
|
|
9
|
+
): string {
|
|
10
|
+
if (!ineffectivePattern?.detected && tier === 0) return '';
|
|
11
|
+
|
|
12
|
+
const tierGuidance: Record<number, string> = {
|
|
13
|
+
0: '',
|
|
14
|
+
1: `🔄 REFRAME TIER 1 — DIRECTIVE: The agent needs clearer direction. Be extremely specific about the next single action to take.`,
|
|
15
|
+
2: `🔄 REFRAME TIER 2 — SUBGOAL: The agent is stuck on the full goal. Break this into a smaller, verifiable milestone. Tell it to complete just that one piece.`,
|
|
16
|
+
3: `🔄 REFRAME TIER 3 — PIVOT: The current approach isn't working. Suggest a completely different strategy or implementation path. Challenge any assumptions.`,
|
|
17
|
+
4: `🔄 REFRAME TIER 4 — MINIMAL SLICE: Strip to absolute essentials. Ask: "What's the smallest working version you can deliver right now?" Push for tangible output.`,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const stagnation = ineffectivePattern?.secondsSinceLastSteer ?? 0;
|
|
21
|
+
const stagnationNote = stagnation > 0 ? ` (${stagnation}s since last steer)` : '';
|
|
22
|
+
const patternNote = ineffectivePattern?.detected
|
|
23
|
+
? `\n⚠ INEFFECTIVE PATTERN DETECTED: Last ${ineffectivePattern.similarCount} steering messages were similar or no progress in${stagnationNote}. Escalate your approach.`
|
|
24
|
+
: '';
|
|
25
|
+
|
|
26
|
+
return tierGuidance[tier] + patternNote;
|
|
27
|
+
}
|