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,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Training Lab type definitions.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `simocracy-v2/lib/training/types.ts` byte-for-byte (modulo
|
|
5
|
+
* import paths). Keep in sync — drift here means the CLI builds a
|
|
6
|
+
* different state shape than the web app for the same sim.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Vote = "yes" | "no" | "abstain";
|
|
10
|
+
|
|
11
|
+
export interface BaselineProposal {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
summary: string;
|
|
15
|
+
topic: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BaselineVote {
|
|
19
|
+
proposalId: string;
|
|
20
|
+
vote: Vote;
|
|
21
|
+
importance: number;
|
|
22
|
+
reasoning: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface InterviewTurn {
|
|
26
|
+
role: "assistant" | "user";
|
|
27
|
+
content: string;
|
|
28
|
+
target?: "values" | "tradeoffs" | "red_lines" | "uncertainty" | "priority";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A turn in the Feedback tab — the user chats directly with the sim
|
|
33
|
+
* (in character) to give concrete feedback on the constitution.
|
|
34
|
+
* "Apply feedback" then synthesises the transcript into a
|
|
35
|
+
* constitution rewrite. Identical wire shape to InterviewTurn but
|
|
36
|
+
* separately named so the two transcripts don't get mixed in storage.
|
|
37
|
+
*/
|
|
38
|
+
export interface FeedbackTurn {
|
|
39
|
+
role: "assistant" | "user";
|
|
40
|
+
content: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface IssuePriority {
|
|
44
|
+
issue: string;
|
|
45
|
+
stance: string;
|
|
46
|
+
importance: number;
|
|
47
|
+
negotiability: number;
|
|
48
|
+
confidence: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TrainingProfile {
|
|
52
|
+
summary: string;
|
|
53
|
+
coreValues: string[];
|
|
54
|
+
issuePriorities: IssuePriority[];
|
|
55
|
+
redLines: string[];
|
|
56
|
+
acceptableTradeoffs: string[];
|
|
57
|
+
uncertaintyAreas: string[];
|
|
58
|
+
representationRules: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AlignmentResult {
|
|
62
|
+
matchedCount: number;
|
|
63
|
+
totalCount: number;
|
|
64
|
+
results: Array<{
|
|
65
|
+
proposalId: string;
|
|
66
|
+
userVote: Vote;
|
|
67
|
+
simVote: Vote;
|
|
68
|
+
matched: boolean;
|
|
69
|
+
confidence: number;
|
|
70
|
+
explanation: string;
|
|
71
|
+
}>;
|
|
72
|
+
weakAreas: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type BaselineQuestionSet =
|
|
76
|
+
| { source: "default"; proposals: BaselineProposal[] }
|
|
77
|
+
| {
|
|
78
|
+
source: "template";
|
|
79
|
+
templateUri: string;
|
|
80
|
+
templateName: string;
|
|
81
|
+
proposals: BaselineProposal[];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export interface TrainingLabState {
|
|
85
|
+
baselineVotes: BaselineVote[];
|
|
86
|
+
interviewTurns: InterviewTurn[];
|
|
87
|
+
feedbackTurns?: FeedbackTurn[];
|
|
88
|
+
profile: TrainingProfile | null;
|
|
89
|
+
alignment: AlignmentResult | null;
|
|
90
|
+
questionSet?: BaselineQuestionSet;
|
|
91
|
+
updatedAt: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Interview templates (mirrors simocracy-v2's lib/lexicon-types.ts subset)
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export type InterviewQuestionType = "open" | "text" | "yesNo";
|
|
99
|
+
|
|
100
|
+
export interface InterviewQuestion {
|
|
101
|
+
id: string;
|
|
102
|
+
type: InterviewQuestionType;
|
|
103
|
+
prompt: string;
|
|
104
|
+
required?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface InterviewTemplateRecord {
|
|
108
|
+
$type: "org.simocracy.interviewTemplate";
|
|
109
|
+
name: string;
|
|
110
|
+
description?: string;
|
|
111
|
+
questions: InterviewQuestion[];
|
|
112
|
+
createdAt: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface LoadedInterviewTemplate {
|
|
116
|
+
uri: string;
|
|
117
|
+
cid: string;
|
|
118
|
+
did: string;
|
|
119
|
+
rkey: string;
|
|
120
|
+
template: InterviewTemplateRecord;
|
|
121
|
+
}
|
package/src/writes.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDS writes via the authenticated OAuth session.
|
|
3
|
+
*
|
|
4
|
+
* Three record types are written here:
|
|
5
|
+
* - org.simocracy.interview (one per Apply, append-only)
|
|
6
|
+
* - org.simocracy.agents (1:1 with sim — create or update)
|
|
7
|
+
* - org.simocracy.style (1:1 with sim — create or update)
|
|
8
|
+
*
|
|
9
|
+
* `getAuthenticatedAgent()` restores the session via the OAuth
|
|
10
|
+
* client's session store and returns an `Agent` from `@atproto/api`,
|
|
11
|
+
* which exposes the same `com.atproto.repo.*` XRPC methods we'd use
|
|
12
|
+
* with an app-password agent.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Agent } from "@atproto/api";
|
|
16
|
+
|
|
17
|
+
import { getOAuthClient } from "./auth/oauth.ts";
|
|
18
|
+
import { readAuth } from "./auth/storage.ts";
|
|
19
|
+
|
|
20
|
+
export class NotSignedInError extends Error {
|
|
21
|
+
constructor(message = "Not signed into ATProto. Run `/sim login <handle>` first (e.g. `/sim login alice.bsky.social`). This is separate from pi's built-in `/login` (Anthropic).") {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "NotSignedInError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getAuthenticatedAgent(): Promise<{ agent: Agent; did: string }> {
|
|
28
|
+
const auth = readAuth();
|
|
29
|
+
if (!auth) throw new NotSignedInError();
|
|
30
|
+
const client = getOAuthClient();
|
|
31
|
+
// refresh="auto" — the OAuth client refreshes the access token if
|
|
32
|
+
// it's about to expire and persists the new tokens via the session
|
|
33
|
+
// store. If refresh fails (e.g. revoked, expired refresh token),
|
|
34
|
+
// restore() throws; we surface it as a NotSignedInError so callers
|
|
35
|
+
// get a consistent shape.
|
|
36
|
+
let oauthSession;
|
|
37
|
+
try {
|
|
38
|
+
oauthSession = await client.restore(auth.did);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw new NotSignedInError(
|
|
41
|
+
`Stored ATProto session for ${auth.did} could not be restored — please run /sim login again. (${(err as Error).message})`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const agent = new Agent(oauthSession);
|
|
45
|
+
return { agent, did: auth.did };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const COLLECTION_INTERVIEW = "org.simocracy.interview";
|
|
49
|
+
const COLLECTION_AGENTS = "org.simocracy.agents";
|
|
50
|
+
const COLLECTION_STYLE = "org.simocracy.style";
|
|
51
|
+
|
|
52
|
+
interface OpenAnswer {
|
|
53
|
+
questionId?: string;
|
|
54
|
+
question: string;
|
|
55
|
+
answer: string;
|
|
56
|
+
}
|
|
57
|
+
interface YesNoAnswer {
|
|
58
|
+
questionId?: string;
|
|
59
|
+
statement: string;
|
|
60
|
+
answer: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* POST `org.simocracy.interview`. Mirrors `interview-modal.tsx`'s
|
|
65
|
+
* save payload — with optional template StrongRef.
|
|
66
|
+
*/
|
|
67
|
+
export async function createInterview(opts: {
|
|
68
|
+
agent: Agent;
|
|
69
|
+
did: string;
|
|
70
|
+
simUri: string;
|
|
71
|
+
simCid: string;
|
|
72
|
+
openAnswers: OpenAnswer[];
|
|
73
|
+
yesNoAnswers: YesNoAnswer[];
|
|
74
|
+
templateUri?: string;
|
|
75
|
+
templateCid?: string;
|
|
76
|
+
}): Promise<{ uri: string; cid: string }> {
|
|
77
|
+
const record: Record<string, unknown> = {
|
|
78
|
+
$type: COLLECTION_INTERVIEW,
|
|
79
|
+
sim: { uri: opts.simUri, cid: opts.simCid },
|
|
80
|
+
openAnswers: opts.openAnswers.map((a) => ({
|
|
81
|
+
questionId: a.questionId,
|
|
82
|
+
question: a.question,
|
|
83
|
+
answer: a.answer,
|
|
84
|
+
})),
|
|
85
|
+
yesNoAnswers: opts.yesNoAnswers.map((a) => ({
|
|
86
|
+
questionId: a.questionId,
|
|
87
|
+
statement: a.statement,
|
|
88
|
+
answer: a.answer,
|
|
89
|
+
})),
|
|
90
|
+
createdAt: new Date().toISOString(),
|
|
91
|
+
};
|
|
92
|
+
if (opts.templateUri && opts.templateCid) {
|
|
93
|
+
record.template = { uri: opts.templateUri, cid: opts.templateCid };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const res = await opts.agent.com.atproto.repo.createRecord({
|
|
97
|
+
repo: opts.did,
|
|
98
|
+
collection: COLLECTION_INTERVIEW,
|
|
99
|
+
record,
|
|
100
|
+
});
|
|
101
|
+
return { uri: res.data.uri, cid: res.data.cid };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* POST `org.simocracy.agents` (constitution + short description).
|
|
106
|
+
* Plain text only — no facets in PR 3 to keep dep count down. The web
|
|
107
|
+
* app keeps the markdown variant as a follow-up; readers handle a
|
|
108
|
+
* facet-less record fine since the lexicon's `descriptionFacets` is
|
|
109
|
+
* optional.
|
|
110
|
+
*/
|
|
111
|
+
export async function createAgents(opts: {
|
|
112
|
+
agent: Agent;
|
|
113
|
+
did: string;
|
|
114
|
+
simUri: string;
|
|
115
|
+
simCid: string;
|
|
116
|
+
shortDescription: string;
|
|
117
|
+
description: string;
|
|
118
|
+
}): Promise<{ uri: string; cid: string; rkey: string }> {
|
|
119
|
+
const record = {
|
|
120
|
+
$type: COLLECTION_AGENTS,
|
|
121
|
+
sim: { uri: opts.simUri, cid: opts.simCid },
|
|
122
|
+
shortDescription: opts.shortDescription.slice(0, 300),
|
|
123
|
+
description: opts.description,
|
|
124
|
+
createdAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
126
|
+
const res = await opts.agent.com.atproto.repo.createRecord({
|
|
127
|
+
repo: opts.did,
|
|
128
|
+
collection: COLLECTION_AGENTS,
|
|
129
|
+
record,
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
uri: res.data.uri,
|
|
133
|
+
cid: res.data.cid,
|
|
134
|
+
rkey: res.data.uri.split("/").pop() ?? "",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* PUT `org.simocracy.agents` at a known rkey. Used when the sim
|
|
140
|
+
* already has an agents record we want to overwrite.
|
|
141
|
+
*/
|
|
142
|
+
export async function updateAgents(opts: {
|
|
143
|
+
agent: Agent;
|
|
144
|
+
did: string;
|
|
145
|
+
rkey: string;
|
|
146
|
+
simUri: string;
|
|
147
|
+
simCid: string;
|
|
148
|
+
shortDescription: string;
|
|
149
|
+
description: string;
|
|
150
|
+
}): Promise<{ uri: string; cid: string }> {
|
|
151
|
+
const record = {
|
|
152
|
+
$type: COLLECTION_AGENTS,
|
|
153
|
+
sim: { uri: opts.simUri, cid: opts.simCid },
|
|
154
|
+
shortDescription: opts.shortDescription.slice(0, 300),
|
|
155
|
+
description: opts.description,
|
|
156
|
+
createdAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
const res = await opts.agent.com.atproto.repo.putRecord({
|
|
159
|
+
repo: opts.did,
|
|
160
|
+
collection: COLLECTION_AGENTS,
|
|
161
|
+
rkey: opts.rkey,
|
|
162
|
+
record,
|
|
163
|
+
});
|
|
164
|
+
return { uri: res.data.uri, cid: res.data.cid };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function createStyle(opts: {
|
|
168
|
+
agent: Agent;
|
|
169
|
+
did: string;
|
|
170
|
+
simUri: string;
|
|
171
|
+
simCid: string;
|
|
172
|
+
description: string;
|
|
173
|
+
}): Promise<{ uri: string; cid: string; rkey: string }> {
|
|
174
|
+
const record = {
|
|
175
|
+
$type: COLLECTION_STYLE,
|
|
176
|
+
sim: { uri: opts.simUri, cid: opts.simCid },
|
|
177
|
+
description: opts.description,
|
|
178
|
+
createdAt: new Date().toISOString(),
|
|
179
|
+
};
|
|
180
|
+
const res = await opts.agent.com.atproto.repo.createRecord({
|
|
181
|
+
repo: opts.did,
|
|
182
|
+
collection: COLLECTION_STYLE,
|
|
183
|
+
record,
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
uri: res.data.uri,
|
|
187
|
+
cid: res.data.cid,
|
|
188
|
+
rkey: res.data.uri.split("/").pop() ?? "",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function updateStyle(opts: {
|
|
193
|
+
agent: Agent;
|
|
194
|
+
did: string;
|
|
195
|
+
rkey: string;
|
|
196
|
+
simUri: string;
|
|
197
|
+
simCid: string;
|
|
198
|
+
description: string;
|
|
199
|
+
}): Promise<{ uri: string; cid: string }> {
|
|
200
|
+
const record = {
|
|
201
|
+
$type: COLLECTION_STYLE,
|
|
202
|
+
sim: { uri: opts.simUri, cid: opts.simCid },
|
|
203
|
+
description: opts.description,
|
|
204
|
+
createdAt: new Date().toISOString(),
|
|
205
|
+
};
|
|
206
|
+
const res = await opts.agent.com.atproto.repo.putRecord({
|
|
207
|
+
repo: opts.did,
|
|
208
|
+
collection: COLLECTION_STYLE,
|
|
209
|
+
rkey: opts.rkey,
|
|
210
|
+
record,
|
|
211
|
+
});
|
|
212
|
+
return { uri: res.data.uri, cid: res.data.cid };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Best-effort lookup of an existing rkey by listing the collection
|
|
217
|
+
* and finding the record whose `sim.uri` matches. Used by the Apply
|
|
218
|
+
* paths to decide between create vs update.
|
|
219
|
+
*/
|
|
220
|
+
export async function findRkeyForSim(
|
|
221
|
+
agent: Agent,
|
|
222
|
+
did: string,
|
|
223
|
+
collection: string,
|
|
224
|
+
simUri: string,
|
|
225
|
+
): Promise<string | null> {
|
|
226
|
+
let cursor: string | undefined;
|
|
227
|
+
for (let page = 0; page < 10; page++) {
|
|
228
|
+
const res = await agent.com.atproto.repo.listRecords({
|
|
229
|
+
repo: did,
|
|
230
|
+
collection,
|
|
231
|
+
limit: 100,
|
|
232
|
+
cursor,
|
|
233
|
+
});
|
|
234
|
+
for (const rec of res.data.records) {
|
|
235
|
+
const value = rec.value as { sim?: { uri?: string } };
|
|
236
|
+
if (value?.sim?.uri === simUri) {
|
|
237
|
+
const rkey = rec.uri.split("/").pop();
|
|
238
|
+
if (rkey) return rkey;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
cursor = res.data.cursor;
|
|
242
|
+
if (!cursor) break;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|