pi-simocracy 0.1.1 → 0.2.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/package.json +3 -1
- package/src/auth/callback-server.ts +93 -0
- package/src/auth/commands.ts +159 -0
- package/src/auth/oauth.ts +55 -0
- package/src/auth/pages.ts +100 -0
- package/src/auth/storage.ts +121 -0
- package/src/index.ts +646 -64
- package/src/interview.ts +516 -0
- package/src/openrouter.ts +16 -0
- package/src/persona.ts +60 -0
- package/src/simocracy.ts +121 -0
- package/src/training/alignment.ts +259 -0
- package/src/training/apply.ts +271 -0
- package/src/training/baseline.ts +159 -0
- package/src/training/chat.ts +131 -0
- package/src/training/feedback.ts +81 -0
- package/src/training/index.ts +142 -0
- package/src/training/profile.ts +229 -0
- package/src/training/prompt-helpers.ts +70 -0
- package/src/training/prompts.ts +131 -0
- package/src/training/question-set.ts +134 -0
- package/src/training/storage.ts +81 -0
- package/src/training/types.ts +121 -0
- package/src/writes.ts +245 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/sim train baseline` — vote yes/no/abstain on the loaded sim's
|
|
3
|
+
* baseline proposals, with importance + optional reasoning.
|
|
4
|
+
*
|
|
5
|
+
* Web parity: `simocracy-v2/components/sim/training-lab/baseline-tab.tsx`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
import type { LoadedSim } from "../persona.ts";
|
|
11
|
+
import {
|
|
12
|
+
loadTrainingLabState,
|
|
13
|
+
saveTrainingLabState,
|
|
14
|
+
} from "./storage.ts";
|
|
15
|
+
import {
|
|
16
|
+
pickInterviewTemplate,
|
|
17
|
+
questionSetFromTemplate,
|
|
18
|
+
} from "./question-set.ts";
|
|
19
|
+
import type { BaselineProposal, BaselineVote, Vote } from "./types.ts";
|
|
20
|
+
|
|
21
|
+
const IMPORTANCE_OPTIONS: Array<{ label: string; value: number }> = [
|
|
22
|
+
{ label: "Negligible (0.1)", value: 0.1 },
|
|
23
|
+
{ label: "Low (0.3)", value: 0.3 },
|
|
24
|
+
{ label: "Medium (0.5)", value: 0.5 },
|
|
25
|
+
{ label: "High (0.7)", value: 0.7 },
|
|
26
|
+
{ label: "Critical (0.9)", value: 0.9 },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const VOTE_OPTIONS: Array<{ label: string; value: Vote | "skip" }> = [
|
|
30
|
+
{ label: "Yes — agree", value: "yes" },
|
|
31
|
+
{ label: "No — disagree", value: "no" },
|
|
32
|
+
{ label: "Abstain — uncertain", value: "abstain" },
|
|
33
|
+
{ label: "Skip — don't vote on this one", value: "skip" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export async function runBaseline(
|
|
37
|
+
ctx: ExtensionCommandContext,
|
|
38
|
+
loadedSim: LoadedSim,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
let state = loadTrainingLabState(loadedSim.rkey);
|
|
41
|
+
|
|
42
|
+
// Bootstrap or refresh the question set if missing.
|
|
43
|
+
if (!state.questionSet || state.questionSet.proposals.length === 0) {
|
|
44
|
+
const picked = await pickInterviewTemplate(ctx);
|
|
45
|
+
if (!picked) {
|
|
46
|
+
ctx.ui.notify("Cancelled.", "info");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
state = {
|
|
50
|
+
...state,
|
|
51
|
+
questionSet: questionSetFromTemplate(picked.template, picked.uri),
|
|
52
|
+
profile: null,
|
|
53
|
+
alignment: null,
|
|
54
|
+
};
|
|
55
|
+
saveTrainingLabState(loadedSim.rkey, state);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const proposals = state.questionSet?.proposals ?? [];
|
|
59
|
+
if (proposals.length === 0) {
|
|
60
|
+
ctx.ui.notify("Picked template has no yes/no questions — nothing to vote on.", "warning");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ctx.ui.notify(
|
|
65
|
+
`Voting on ${proposals.length} baseline proposals for ${loadedSim.name}. Cancel any prompt to stop early.`,
|
|
66
|
+
"info",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < proposals.length; i++) {
|
|
70
|
+
const proposal = proposals[i];
|
|
71
|
+
const existing = state.baselineVotes.find((v) => v.proposalId === proposal.id);
|
|
72
|
+
const answer = await askVote(ctx, proposal, i, proposals.length, existing);
|
|
73
|
+
if (answer === null) {
|
|
74
|
+
ctx.ui.notify("Stopped — votes saved up to this point.", "info");
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
state = {
|
|
78
|
+
...state,
|
|
79
|
+
baselineVotes: recordBaselineVote(state.baselineVotes, proposal.id, answer),
|
|
80
|
+
// Re-distilling and alignment depend on the votes — clear when votes change.
|
|
81
|
+
profile: null,
|
|
82
|
+
alignment: null,
|
|
83
|
+
};
|
|
84
|
+
saveTrainingLabState(loadedSim.rkey, state);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cast = state.baselineVotes.filter(
|
|
88
|
+
(v) => !(v.vote === "abstain" && Math.abs(v.importance - 0.5) < 0.01 && !v.reasoning),
|
|
89
|
+
).length;
|
|
90
|
+
ctx.ui.notify(
|
|
91
|
+
`Saved ${cast} baseline votes. Run \`/sim train chat\` next, then \`/sim train profile\`.`,
|
|
92
|
+
"info",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface AnsweredVote {
|
|
97
|
+
vote: Vote;
|
|
98
|
+
importance: number;
|
|
99
|
+
reasoning: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function askVote(
|
|
103
|
+
ctx: ExtensionCommandContext,
|
|
104
|
+
proposal: BaselineProposal,
|
|
105
|
+
index: number,
|
|
106
|
+
total: number,
|
|
107
|
+
existing: BaselineVote | undefined,
|
|
108
|
+
): Promise<AnsweredVote | null> {
|
|
109
|
+
const header = existing
|
|
110
|
+
? `[${index + 1}/${total}] ${proposal.title} (current: ${existing.vote.toUpperCase()})`
|
|
111
|
+
: `[${index + 1}/${total}] ${proposal.title}`;
|
|
112
|
+
|
|
113
|
+
const voteLabel = await ctx.ui.select(header, VOTE_OPTIONS.map((o) => o.label));
|
|
114
|
+
if (!voteLabel) return null;
|
|
115
|
+
const voteEntry = VOTE_OPTIONS.find((o) => o.label === voteLabel);
|
|
116
|
+
if (!voteEntry) return null;
|
|
117
|
+
|
|
118
|
+
// Skip leaves the abstain default at importance 0.5 with no reasoning,
|
|
119
|
+
// which the prompt-helpers' renderer treats as untouched.
|
|
120
|
+
if (voteEntry.value === "skip") {
|
|
121
|
+
return { vote: "abstain", importance: 0.5, reasoning: "" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const importanceLabel = await ctx.ui.select(
|
|
125
|
+
"How important is this to you?",
|
|
126
|
+
IMPORTANCE_OPTIONS.map((o) => o.label),
|
|
127
|
+
);
|
|
128
|
+
if (!importanceLabel) return null;
|
|
129
|
+
const importance =
|
|
130
|
+
IMPORTANCE_OPTIONS.find((o) => o.label === importanceLabel)?.value ?? 0.5;
|
|
131
|
+
|
|
132
|
+
const reasoning = await ctx.ui.input(
|
|
133
|
+
"Optional reasoning (Enter to skip)",
|
|
134
|
+
`Why did you vote ${voteEntry.value}?`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
vote: voteEntry.value,
|
|
139
|
+
importance,
|
|
140
|
+
reasoning: (reasoning ?? "").trim(),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function recordBaselineVote(
|
|
145
|
+
votes: BaselineVote[],
|
|
146
|
+
proposalId: string,
|
|
147
|
+
next: AnsweredVote,
|
|
148
|
+
): BaselineVote[] {
|
|
149
|
+
const without = votes.filter((v) => v.proposalId !== proposalId);
|
|
150
|
+
return [
|
|
151
|
+
...without,
|
|
152
|
+
{
|
|
153
|
+
proposalId,
|
|
154
|
+
vote: next.vote,
|
|
155
|
+
importance: next.importance,
|
|
156
|
+
reasoning: next.reasoning,
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/sim train chat` — adaptive interview turn loop.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `/api/training/next-question` from simocracy-v2 but calls
|
|
5
|
+
* OpenRouter directly (no pi loop / persona injection). End the chat
|
|
6
|
+
* by typing `/done` (or cancelling the prompt).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
import type { LoadedSim } from "../persona.ts";
|
|
12
|
+
import { openRouterComplete, TRAINING_CHAT_MODEL } from "../openrouter.ts";
|
|
13
|
+
import {
|
|
14
|
+
loadTrainingLabState,
|
|
15
|
+
saveTrainingLabState,
|
|
16
|
+
} from "./storage.ts";
|
|
17
|
+
import {
|
|
18
|
+
clampConstitution,
|
|
19
|
+
clampTranscript,
|
|
20
|
+
renderBaselineForPrompt,
|
|
21
|
+
wrapAsData,
|
|
22
|
+
} from "./prompt-helpers.ts";
|
|
23
|
+
import { buildNextQuestionSystemPrompt } from "./prompts.ts";
|
|
24
|
+
import type { InterviewTurn } from "./types.ts";
|
|
25
|
+
|
|
26
|
+
export async function runChat(
|
|
27
|
+
ctx: ExtensionCommandContext,
|
|
28
|
+
loadedSim: LoadedSim,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
let state = loadTrainingLabState(loadedSim.rkey);
|
|
31
|
+
const proposals = state.questionSet?.proposals ?? [];
|
|
32
|
+
|
|
33
|
+
if (proposals.length === 0) {
|
|
34
|
+
ctx.ui.notify(
|
|
35
|
+
"Run `/sim train baseline` first — chat needs a baseline questionnaire to ground its questions.",
|
|
36
|
+
"warning",
|
|
37
|
+
);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ctx.ui.notify(
|
|
42
|
+
`Chatting with ${loadedSim.name}. Type /done to end the conversation.`,
|
|
43
|
+
"info",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Always print the existing transcript so the user has context.
|
|
47
|
+
if (state.interviewTurns.length > 0) {
|
|
48
|
+
ctx.ui.notify(
|
|
49
|
+
`Loaded ${state.interviewTurns.length} prior turns from disk.`,
|
|
50
|
+
"info",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Prime the assistant with the first question if the transcript is empty.
|
|
55
|
+
if (state.interviewTurns.length === 0) {
|
|
56
|
+
const reply = await callNextQuestion(loadedSim, state.interviewTurns, state, proposals);
|
|
57
|
+
if (reply) {
|
|
58
|
+
console.log(`\n${loadedSim.name}: ${reply}\n`);
|
|
59
|
+
state = appendTurn(state, { role: "assistant", content: reply });
|
|
60
|
+
saveTrainingLabState(loadedSim.rkey, state);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (;;) {
|
|
65
|
+
const userMessage = await ctx.ui.input(
|
|
66
|
+
`Reply to ${loadedSim.name} (or /done to finish)`,
|
|
67
|
+
"Your reply",
|
|
68
|
+
);
|
|
69
|
+
if (userMessage === undefined) break;
|
|
70
|
+
const trimmed = userMessage.trim();
|
|
71
|
+
if (!trimmed) continue;
|
|
72
|
+
if (trimmed === "/done" || trimmed === "/exit" || trimmed === "/quit") break;
|
|
73
|
+
|
|
74
|
+
state = appendTurn(state, { role: "user", content: trimmed });
|
|
75
|
+
saveTrainingLabState(loadedSim.rkey, state);
|
|
76
|
+
|
|
77
|
+
let reply: string;
|
|
78
|
+
try {
|
|
79
|
+
reply = await callNextQuestion(loadedSim, state.interviewTurns, state, proposals);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
ctx.ui.notify(`OpenRouter error: ${(err as Error).message}`, "error");
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (!reply) {
|
|
85
|
+
ctx.ui.notify("Empty model response. Try again or /done.", "warning");
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
console.log(`\n${loadedSim.name}: ${reply}\n`);
|
|
89
|
+
state = appendTurn(state, { role: "assistant", content: reply });
|
|
90
|
+
saveTrainingLabState(loadedSim.rkey, state);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ctx.ui.notify(
|
|
94
|
+
`Saved ${state.interviewTurns.length} turns. Run \`/sim train profile\` to distill.`,
|
|
95
|
+
"info",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function callNextQuestion(
|
|
100
|
+
loadedSim: LoadedSim,
|
|
101
|
+
transcript: InterviewTurn[],
|
|
102
|
+
state: ReturnType<typeof loadTrainingLabState>,
|
|
103
|
+
proposals: { id: string; title: string; summary: string; topic: string }[],
|
|
104
|
+
): Promise<string> {
|
|
105
|
+
const system = buildNextQuestionSystemPrompt(loadedSim.name, loadedSim.style);
|
|
106
|
+
const constitution = clampConstitution(loadedSim.description);
|
|
107
|
+
const baseline = renderBaselineForPrompt(state.baselineVotes, proposals);
|
|
108
|
+
const clamped = clampTranscript(transcript);
|
|
109
|
+
|
|
110
|
+
const userPrompt = [
|
|
111
|
+
`simName: ${loadedSim.name}`,
|
|
112
|
+
wrapAsData("existingConstitution", constitution),
|
|
113
|
+
wrapAsData("baselineQuestionnaire", baseline),
|
|
114
|
+
wrapAsData("transcript", JSON.stringify(clamped, null, 2)),
|
|
115
|
+
].join("\n\n");
|
|
116
|
+
|
|
117
|
+
return openRouterComplete(
|
|
118
|
+
[
|
|
119
|
+
{ role: "system", content: system },
|
|
120
|
+
{ role: "user", content: userPrompt },
|
|
121
|
+
],
|
|
122
|
+
{ model: TRAINING_CHAT_MODEL, maxTokens: 600, temperature: 0.6 },
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function appendTurn<T extends { interviewTurns: InterviewTurn[] }>(
|
|
127
|
+
state: T,
|
|
128
|
+
turn: InterviewTurn,
|
|
129
|
+
): T {
|
|
130
|
+
return { ...state, interviewTurns: [...state.interviewTurns, turn] };
|
|
131
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/sim train feedback` — free-form chat with the loaded sim about
|
|
3
|
+
* its constitution. Same pattern as `/sim train chat` but uses the
|
|
4
|
+
* regular sim persona prompt (`buildSimPrompt`) — i.e., the sim
|
|
5
|
+
* speaks in character — so the user gets real feedback on how the
|
|
6
|
+
* sim sounds, then can use the transcript to inform an Apply step.
|
|
7
|
+
*
|
|
8
|
+
* Web parity: `simocracy-v2/components/sim/training-lab/feedback-tab.tsx`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
import { buildSimPrompt, type LoadedSim } from "../persona.ts";
|
|
14
|
+
import { openRouterComplete, TRAINING_CHAT_MODEL } from "../openrouter.ts";
|
|
15
|
+
import {
|
|
16
|
+
loadTrainingLabState,
|
|
17
|
+
saveTrainingLabState,
|
|
18
|
+
} from "./storage.ts";
|
|
19
|
+
import type { FeedbackTurn } from "./types.ts";
|
|
20
|
+
|
|
21
|
+
export async function runFeedback(
|
|
22
|
+
ctx: ExtensionCommandContext,
|
|
23
|
+
loadedSim: LoadedSim,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
let state = loadTrainingLabState(loadedSim.rkey);
|
|
26
|
+
let turns: FeedbackTurn[] = state.feedbackTurns ?? [];
|
|
27
|
+
|
|
28
|
+
ctx.ui.notify(
|
|
29
|
+
`Free-form chat with ${loadedSim.name}. Type /done to end. Transcript persists for the Apply step.`,
|
|
30
|
+
"info",
|
|
31
|
+
);
|
|
32
|
+
if (turns.length > 0) {
|
|
33
|
+
ctx.ui.notify(`Loaded ${turns.length} prior feedback turns from disk.`, "info");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (;;) {
|
|
37
|
+
const userMessage = await ctx.ui.input(
|
|
38
|
+
`Ask ${loadedSim.name} (or /done to finish)`,
|
|
39
|
+
"Your message",
|
|
40
|
+
);
|
|
41
|
+
if (userMessage === undefined) break;
|
|
42
|
+
const trimmed = userMessage.trim();
|
|
43
|
+
if (!trimmed) continue;
|
|
44
|
+
if (trimmed === "/done" || trimmed === "/exit" || trimmed === "/quit") break;
|
|
45
|
+
|
|
46
|
+
turns = [...turns, { role: "user", content: trimmed }];
|
|
47
|
+
state = { ...state, feedbackTurns: turns };
|
|
48
|
+
saveTrainingLabState(loadedSim.rkey, state);
|
|
49
|
+
|
|
50
|
+
let reply: string;
|
|
51
|
+
try {
|
|
52
|
+
reply = await callSimChat(loadedSim, turns);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
ctx.ui.notify(`OpenRouter error: ${(err as Error).message}`, "error");
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (!reply) {
|
|
58
|
+
ctx.ui.notify("Empty model response. Try again or /done.", "warning");
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
console.log(`\n${loadedSim.name}: ${reply}\n`);
|
|
62
|
+
turns = [...turns, { role: "assistant", content: reply }];
|
|
63
|
+
state = { ...state, feedbackTurns: turns };
|
|
64
|
+
saveTrainingLabState(loadedSim.rkey, state);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ctx.ui.notify(`Saved ${turns.length} feedback turns.`, "info");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function callSimChat(loadedSim: LoadedSim, turns: FeedbackTurn[]): Promise<string> {
|
|
71
|
+
const systemPrompt = buildSimPrompt(loadedSim);
|
|
72
|
+
const messages = [
|
|
73
|
+
{ role: "system" as const, content: systemPrompt },
|
|
74
|
+
...turns.map((t) => ({ role: t.role, content: t.content })),
|
|
75
|
+
];
|
|
76
|
+
return openRouterComplete(messages, {
|
|
77
|
+
model: TRAINING_CHAT_MODEL,
|
|
78
|
+
maxTokens: 600,
|
|
79
|
+
temperature: 0.7,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/sim train …` command dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Sub-commands mirror the five tabs of the Training Lab in
|
|
5
|
+
* simocracy-v2 plus `status` / `reset`. Run `/sim train` (no arg) for
|
|
6
|
+
* usage. All flows operate on the currently-loaded sim and persist
|
|
7
|
+
* state to `~/.local/share/pi-simocracy/training/<rkey>.json`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
import type { LoadedSim } from "../persona.ts";
|
|
13
|
+
import { runBaseline } from "./baseline.ts";
|
|
14
|
+
import { runChat } from "./chat.ts";
|
|
15
|
+
import { runProfile } from "./profile.ts";
|
|
16
|
+
import { runFeedback } from "./feedback.ts";
|
|
17
|
+
import { runAlignment } from "./alignment.ts";
|
|
18
|
+
import { runApply } from "./apply.ts";
|
|
19
|
+
import {
|
|
20
|
+
clearTrainingLabState,
|
|
21
|
+
loadTrainingLabState,
|
|
22
|
+
trainingFilePath,
|
|
23
|
+
} from "./storage.ts";
|
|
24
|
+
|
|
25
|
+
const HELP = [
|
|
26
|
+
"Usage: /sim train <subcommand>",
|
|
27
|
+
"",
|
|
28
|
+
" baseline Vote yes/no/abstain on the loaded sim's baseline proposals.",
|
|
29
|
+
" chat Adaptive interview with the sim — fill gaps in your stance.",
|
|
30
|
+
" profile Distill baseline + transcript into a TrainingProfile.",
|
|
31
|
+
" feedback Free-form chat with the sim about its constitution.",
|
|
32
|
+
" alignment Score the sim against your hidden baseline votes.",
|
|
33
|
+
" apply Merge the profile into the constitution (clipboard copy in PR 2).",
|
|
34
|
+
" status Print local state path + counts.",
|
|
35
|
+
" reset Delete local training data for this sim.",
|
|
36
|
+
].join("\n");
|
|
37
|
+
|
|
38
|
+
export async function runTrainCommand(
|
|
39
|
+
ctx: ExtensionCommandContext,
|
|
40
|
+
arg: string,
|
|
41
|
+
loadedSim: LoadedSim | null,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const tokens = arg.trim().split(/\s+/).filter(Boolean);
|
|
44
|
+
const sub = tokens[0] ?? "";
|
|
45
|
+
const flags = new Set(tokens.slice(1));
|
|
46
|
+
|
|
47
|
+
if (!sub || sub === "help" || sub === "--help") {
|
|
48
|
+
ctx.ui.notify(HELP, "info");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (sub === "status") {
|
|
53
|
+
await runStatus(ctx, loadedSim);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (sub === "reset") {
|
|
57
|
+
await runReset(ctx, loadedSim);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!loadedSim) {
|
|
62
|
+
ctx.ui.notify(
|
|
63
|
+
"No sim loaded. Run `/sim <name>` first, then `/sim train …`.",
|
|
64
|
+
"error",
|
|
65
|
+
);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (sub) {
|
|
70
|
+
case "baseline":
|
|
71
|
+
await runBaseline(ctx, loadedSim);
|
|
72
|
+
return;
|
|
73
|
+
case "chat":
|
|
74
|
+
await runChat(ctx, loadedSim);
|
|
75
|
+
return;
|
|
76
|
+
case "profile":
|
|
77
|
+
await runProfile(ctx, loadedSim);
|
|
78
|
+
return;
|
|
79
|
+
case "feedback":
|
|
80
|
+
await runFeedback(ctx, loadedSim);
|
|
81
|
+
return;
|
|
82
|
+
case "alignment":
|
|
83
|
+
await runAlignment(ctx, loadedSim);
|
|
84
|
+
return;
|
|
85
|
+
case "apply":
|
|
86
|
+
await runApply(ctx, loadedSim, { apply: flags.has("--apply") });
|
|
87
|
+
return;
|
|
88
|
+
default:
|
|
89
|
+
ctx.ui.notify(`Unknown subcommand: ${sub}\n\n${HELP}`, "error");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runStatus(
|
|
95
|
+
ctx: ExtensionCommandContext,
|
|
96
|
+
loadedSim: LoadedSim | null,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
if (!loadedSim) {
|
|
99
|
+
ctx.ui.notify("No sim loaded.", "info");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const path = trainingFilePath(loadedSim.rkey);
|
|
103
|
+
const state = loadTrainingLabState(loadedSim.rkey);
|
|
104
|
+
const cast = state.baselineVotes.filter(
|
|
105
|
+
(v) => !(v.vote === "abstain" && Math.abs(v.importance - 0.5) < 0.01 && !v.reasoning),
|
|
106
|
+
).length;
|
|
107
|
+
const lines = [
|
|
108
|
+
`Sim: ${loadedSim.name} (${loadedSim.uri})`,
|
|
109
|
+
`State file: ${path}`,
|
|
110
|
+
`Updated: ${state.updatedAt}`,
|
|
111
|
+
`Baseline: ${cast}/${state.baselineVotes.length} cast votes`,
|
|
112
|
+
`Interview: ${state.interviewTurns.length} turns`,
|
|
113
|
+
`Feedback: ${(state.feedbackTurns ?? []).length} turns`,
|
|
114
|
+
`Profile: ${state.profile ? "yes" : "no"}`,
|
|
115
|
+
`Alignment: ${state.alignment ? `${state.alignment.matchedCount}/${state.alignment.totalCount}` : "no"}`,
|
|
116
|
+
`Question set: ${state.questionSet?.source === "template" ? state.questionSet.templateName : "(none)"}`,
|
|
117
|
+
];
|
|
118
|
+
console.log("\n" + lines.join("\n") + "\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function runReset(
|
|
122
|
+
ctx: ExtensionCommandContext,
|
|
123
|
+
loadedSim: LoadedSim | null,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
if (!loadedSim) {
|
|
126
|
+
ctx.ui.notify("No sim loaded.", "info");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const confirmed = await ctx.ui.confirm(
|
|
130
|
+
"Reset training data?",
|
|
131
|
+
`Delete all local training data for ${loadedSim.name}?`,
|
|
132
|
+
);
|
|
133
|
+
if (!confirmed) {
|
|
134
|
+
ctx.ui.notify("Cancelled.", "info");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const removed = clearTrainingLabState(loadedSim.rkey);
|
|
138
|
+
ctx.ui.notify(
|
|
139
|
+
removed ? `Training data reset for ${loadedSim.name}.` : "No training data on disk.",
|
|
140
|
+
"info",
|
|
141
|
+
);
|
|
142
|
+
}
|