headlesscode 1.2.2 → 1.3.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/README.md +46 -16
- package/package.json +7 -3
- package/src/cli.ts +47 -18
- package/src/cloud/openshell-preflight.ts +2 -2
- package/src/cloud/openshell-provider.ts +84 -24
- package/src/llm/ollama.ts +25 -19
- package/src/project-store.ts +4 -1
- package/src/rsi/adaptive.ts +49 -0
- package/src/rsi/adversarial.ts +106 -0
- package/src/rsi/archive.ts +6 -5
- package/src/rsi/artifact-store.ts +158 -0
- package/src/rsi/config.ts +50 -2
- package/src/rsi/controller.ts +561 -42
- package/src/rsi/curriculum.ts +135 -16
- package/src/rsi/evaluator.ts +6 -37
- package/src/rsi/fitness.ts +39 -4
- package/src/rsi/index.ts +1 -0
- package/src/rsi/migrations/001_postgres_fleet_queue.sql +65 -0
- package/src/rsi/migrations/002_external_artifacts_and_job_leases.sql +39 -0
- package/src/rsi/migrations/003_model_training_jobs.sql +6 -0
- package/src/rsi/model-training.ts +256 -0
- package/src/rsi/mutation.ts +1 -77
- package/src/rsi/openshell.ts +639 -0
- package/src/rsi/postgres-queue.ts +424 -0
- package/src/rsi/promote-curriculum.ts +21 -0
- package/src/rsi/reports.ts +29 -2
- package/src/rsi/roles.ts +13 -3
- package/src/rsi/selection.ts +7 -1
- package/src/rsi/training-data.ts +103 -0
- package/src/rsi/trajectory.ts +1 -1
- package/src/rsi/types.ts +115 -2
- package/src/rsi/worker.ts +264 -0
- package/src/rsi/workspace.ts +14 -3
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { Pool, type PoolClient } from "pg"
|
|
4
|
+
import type { ResourceClass } from "./types.js"
|
|
5
|
+
import { createRsiArtifactStore, type RsiArtifactReference, type RsiArtifactStore } from "./artifact-store.js"
|
|
6
|
+
|
|
7
|
+
const MIGRATIONS = [
|
|
8
|
+
{ version: 1, sql: readFileSync(new URL("./migrations/001_postgres_fleet_queue.sql", import.meta.url), "utf8") },
|
|
9
|
+
{ version: 2, sql: readFileSync(new URL("./migrations/002_external_artifacts_and_job_leases.sql", import.meta.url), "utf8") },
|
|
10
|
+
{ version: 3, sql: readFileSync(new URL("./migrations/003_model_training_jobs.sql", import.meta.url), "utf8") },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export interface FleetQueuePolicy {
|
|
14
|
+
queueName: string
|
|
15
|
+
maxInFlight: number
|
|
16
|
+
leaseMs: number
|
|
17
|
+
maxAttempts: number
|
|
18
|
+
classCapacity?: Partial<Record<ResourceClass, number>>
|
|
19
|
+
concurrencyKeyCapacity?: Record<string, number>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseCapacityMap(raw: string | undefined, name: string): Record<string, number> {
|
|
23
|
+
if (!raw?.trim()) return {}
|
|
24
|
+
let value: unknown
|
|
25
|
+
try { value = JSON.parse(raw) } catch { throw new Error(`${name} must be a JSON object of positive integer capacities`) }
|
|
26
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be a JSON object of positive integer capacities`)
|
|
27
|
+
const result: Record<string, number> = {}
|
|
28
|
+
for (const [key, limit] of Object.entries(value)) {
|
|
29
|
+
if (!key || !Number.isInteger(limit) || Number(limit) < 1) throw new Error(`${name} must contain only positive integer capacities`)
|
|
30
|
+
result[key] = Number(limit)
|
|
31
|
+
}
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function fleetQueuePolicy(queueName: string, maxInFlight: number, env: NodeJS.ProcessEnv = process.env): FleetQueuePolicy {
|
|
36
|
+
const leaseMs = env.HEADLESSCODE_RSI_LEASE_MS?.trim() ? Number(env.HEADLESSCODE_RSI_LEASE_MS) : 60 * 60_000
|
|
37
|
+
const maxAttempts = env.HEADLESSCODE_RSI_MAX_ATTEMPTS?.trim() ? Number(env.HEADLESSCODE_RSI_MAX_ATTEMPTS) : 2
|
|
38
|
+
return validatePolicy({
|
|
39
|
+
queueName,
|
|
40
|
+
maxInFlight,
|
|
41
|
+
leaseMs,
|
|
42
|
+
maxAttempts,
|
|
43
|
+
classCapacity: parseCapacityMap(env.HEADLESSCODE_RSI_CLASS_CAPACITY, "HEADLESSCODE_RSI_CLASS_CAPACITY") as FleetQueuePolicy["classCapacity"],
|
|
44
|
+
concurrencyKeyCapacity: parseCapacityMap(env.HEADLESSCODE_RSI_KEY_CAPACITY, "HEADLESSCODE_RSI_KEY_CAPACITY"),
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FleetWorkerRegistration {
|
|
49
|
+
workerId: string
|
|
50
|
+
gatewayId: string
|
|
51
|
+
hostname: string
|
|
52
|
+
maxActive: number
|
|
53
|
+
resourceClasses: ResourceClass[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FleetJobPayload {
|
|
57
|
+
schemaVersion: 1
|
|
58
|
+
snapshotSha256: string
|
|
59
|
+
snapshotBytes: number
|
|
60
|
+
snapshotCommit: string
|
|
61
|
+
task: string
|
|
62
|
+
model: string
|
|
63
|
+
maxIterations: number
|
|
64
|
+
timeoutMs: number
|
|
65
|
+
protectedPaths: string[]
|
|
66
|
+
candidate: Record<string, unknown>
|
|
67
|
+
regressionCommand?: string
|
|
68
|
+
evalCommands?: string[]
|
|
69
|
+
modelTask?: {
|
|
70
|
+
kind: "qlora-train" | "model-evaluation"
|
|
71
|
+
datasetVersion: string
|
|
72
|
+
datasetSha256: string
|
|
73
|
+
manifestSha256: string
|
|
74
|
+
baseModelId: string
|
|
75
|
+
baseModelPath: "/workspace/.rsi-base-model"
|
|
76
|
+
seed: 1337
|
|
77
|
+
maxSteps?: 8
|
|
78
|
+
modelCandidateId?: string
|
|
79
|
+
adapterPath?: string
|
|
80
|
+
modelKind?: "base" | "adapter"
|
|
81
|
+
cellSetSha256: string
|
|
82
|
+
cellIds: string[]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface FleetJob {
|
|
87
|
+
jobId: string
|
|
88
|
+
idempotencyKey: string
|
|
89
|
+
runId: string
|
|
90
|
+
candidateId: string
|
|
91
|
+
jobKind: "mutation" | "evaluation" | "training" | "model-evaluation"
|
|
92
|
+
resourceClass: ResourceClass
|
|
93
|
+
concurrencyKey?: string
|
|
94
|
+
artifactSha256: string
|
|
95
|
+
payload: FleetJobPayload
|
|
96
|
+
status: "queued" | "leased" | "completed" | "failed" | "cancelled"
|
|
97
|
+
attempts: number
|
|
98
|
+
maxAttempts: number
|
|
99
|
+
leaseDurationMs: number
|
|
100
|
+
leaseOwner?: string
|
|
101
|
+
leaseToken?: string
|
|
102
|
+
leaseExpiresAt?: string
|
|
103
|
+
result?: unknown
|
|
104
|
+
error?: string
|
|
105
|
+
createdAt: string
|
|
106
|
+
updatedAt: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface FleetJobLease { job: FleetJob; token: string; workerId: string }
|
|
110
|
+
export interface QueueMutationResult { accepted: boolean; duplicate: boolean; job?: FleetJob }
|
|
111
|
+
export type FleetJobIdentity = Pick<FleetJob, "idempotencyKey" | "runId" | "candidateId" | "jobKind" | "resourceClass" | "concurrencyKey" | "artifactSha256" | "payload">
|
|
112
|
+
|
|
113
|
+
function stableJson(value: unknown): string {
|
|
114
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`
|
|
115
|
+
if (value && typeof value === "object") {
|
|
116
|
+
const object = value as Record<string, unknown>
|
|
117
|
+
return `{${Object.keys(object).filter((key) => object[key] !== undefined).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`
|
|
118
|
+
}
|
|
119
|
+
const encoded = JSON.stringify(value)
|
|
120
|
+
return encoded === undefined ? "undefined" : encoded
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Compare every queued execution input before a coordinator reuses an idempotent job. */
|
|
124
|
+
export function fleetJobMatches(job: FleetJob, expected: FleetJobIdentity): boolean {
|
|
125
|
+
return job.idempotencyKey === expected.idempotencyKey
|
|
126
|
+
&& job.runId === expected.runId
|
|
127
|
+
&& job.candidateId === expected.candidateId
|
|
128
|
+
&& job.jobKind === expected.jobKind
|
|
129
|
+
&& job.resourceClass === expected.resourceClass
|
|
130
|
+
&& (job.concurrencyKey ?? undefined) === (expected.concurrencyKey ?? undefined)
|
|
131
|
+
&& job.artifactSha256 === expected.artifactSha256
|
|
132
|
+
&& stableJson(job.payload) === stableJson(expected.payload)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validatePolicy(policy: FleetQueuePolicy): FleetQueuePolicy {
|
|
136
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,62}$/.test(policy.queueName)) throw new Error("queueName must be a simple lowercase identifier")
|
|
137
|
+
for (const [name, value, minimum] of [["maxInFlight", policy.maxInFlight, 1], ["leaseMs", policy.leaseMs, 1000], ["maxAttempts", policy.maxAttempts, 1]] as const) {
|
|
138
|
+
if (!Number.isInteger(value) || value < minimum) throw new Error(`queue ${name} must be an integer of at least ${minimum}`)
|
|
139
|
+
}
|
|
140
|
+
for (const [key, value] of Object.entries({ ...(policy.classCapacity ?? {}), ...(policy.concurrencyKeyCapacity ?? {}) })) {
|
|
141
|
+
if (!Number.isInteger(value) || value < 1) throw new Error(`queue capacity ${key} must be a positive integer`)
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
...policy,
|
|
145
|
+
classCapacity: policy.classCapacity ?? {},
|
|
146
|
+
concurrencyKeyCapacity: policy.concurrencyKeyCapacity ?? {},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function rowToJob(row: Record<string, unknown>): FleetJob {
|
|
151
|
+
return {
|
|
152
|
+
jobId: String(row.job_id),
|
|
153
|
+
idempotencyKey: String(row.idempotency_key),
|
|
154
|
+
runId: String(row.run_id),
|
|
155
|
+
candidateId: String(row.candidate_id),
|
|
156
|
+
jobKind: row.job_kind as FleetJob["jobKind"],
|
|
157
|
+
resourceClass: row.resource_class as ResourceClass,
|
|
158
|
+
...(typeof row.concurrency_key === "string" ? { concurrencyKey: row.concurrency_key } : {}),
|
|
159
|
+
artifactSha256: String(row.artifact_sha256).trim(),
|
|
160
|
+
payload: row.payload as FleetJobPayload,
|
|
161
|
+
status: row.status as FleetJob["status"],
|
|
162
|
+
attempts: Number(row.attempts),
|
|
163
|
+
maxAttempts: Number(row.max_attempts),
|
|
164
|
+
leaseDurationMs: Number(row.lease_duration_ms),
|
|
165
|
+
...(typeof row.lease_owner === "string" ? { leaseOwner: row.lease_owner } : {}),
|
|
166
|
+
...(typeof row.lease_token === "string" ? { leaseToken: row.lease_token } : {}),
|
|
167
|
+
...(row.lease_expires_at ? { leaseExpiresAt: new Date(String(row.lease_expires_at)).toISOString() } : {}),
|
|
168
|
+
...(row.result !== null && row.result !== undefined ? { result: row.result } : {}),
|
|
169
|
+
...(typeof row.error === "string" ? { error: row.error } : {}),
|
|
170
|
+
createdAt: new Date(String(row.created_at)).toISOString(),
|
|
171
|
+
updatedAt: new Date(String(row.updated_at)).toISOString(),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Worst-case synchronous OpenShell CLI budget, including setup and teardown. */
|
|
176
|
+
export function requiredFleetJobLeaseMs(jobKind: FleetJob["jobKind"], timeoutMs: number, visibleCommandCount = 0): number {
|
|
177
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || !Number.isSafeInteger(visibleCommandCount) || visibleCommandCount < 0) throw new Error("invalid fleet job timeout or visible command count")
|
|
178
|
+
const sessions = jobKind === "evaluation" ? visibleCommandCount + 1 : 1
|
|
179
|
+
const required = sessions * (timeoutMs * 3 + 180_000)
|
|
180
|
+
if (!Number.isSafeInteger(required) || required > 2_147_483_647) throw new Error("RSI job timeout budget exceeds PostgreSQL's 24-day lease range")
|
|
181
|
+
return required
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function inTransaction<T>(pool: Pool, run: (client: PoolClient) => Promise<T>): Promise<T> {
|
|
185
|
+
const client = await pool.connect()
|
|
186
|
+
try {
|
|
187
|
+
await client.query("BEGIN")
|
|
188
|
+
const result = await run(client)
|
|
189
|
+
await client.query("COMMIT")
|
|
190
|
+
return result
|
|
191
|
+
} catch (error) {
|
|
192
|
+
await client.query("ROLLBACK").catch(() => undefined)
|
|
193
|
+
throw error
|
|
194
|
+
} finally {
|
|
195
|
+
client.release()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** PostgreSQL is the source of truth for workers, admission, leases and results. */
|
|
200
|
+
export class PostgresRsiJobQueue {
|
|
201
|
+
readonly policy: FleetQueuePolicy
|
|
202
|
+
readonly artifactBackend: RsiArtifactStore["backend"]
|
|
203
|
+
constructor(readonly pool: Pool, policy: FleetQueuePolicy, private readonly artifactStore: RsiArtifactStore) {
|
|
204
|
+
this.policy = validatePolicy(policy)
|
|
205
|
+
this.artifactBackend = artifactStore.backend
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static fromEnvironment(policy: FleetQueuePolicy, env: NodeJS.ProcessEnv = process.env): PostgresRsiJobQueue {
|
|
209
|
+
const connectionString = env.HEADLESSCODE_RSI_DATABASE_URL?.trim()
|
|
210
|
+
if (!connectionString) throw new Error("HEADLESSCODE_RSI_DATABASE_URL is required for RSI fleet execution")
|
|
211
|
+
return new PostgresRsiJobQueue(new Pool({ connectionString, max: 8, application_name: `headlesscode-rsi-${policy.queueName}` }), policy, createRsiArtifactStore(env))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async migrate(): Promise<void> {
|
|
215
|
+
await this.pool.query(`CREATE TABLE IF NOT EXISTS headlesscode_rsi_schema_migrations (
|
|
216
|
+
version integer PRIMARY KEY,
|
|
217
|
+
applied_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
|
218
|
+
)`)
|
|
219
|
+
for (const migration of MIGRATIONS) {
|
|
220
|
+
const applied = await this.pool.query("SELECT 1 FROM headlesscode_rsi_schema_migrations WHERE version=$1", [migration.version])
|
|
221
|
+
if (applied.rowCount) continue
|
|
222
|
+
await inTransaction(this.pool, async (client) => {
|
|
223
|
+
await client.query(migration.sql)
|
|
224
|
+
await client.query("INSERT INTO headlesscode_rsi_schema_migrations(version) VALUES($1) ON CONFLICT DO NOTHING", [migration.version])
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
const inserted = await this.pool.query(
|
|
228
|
+
`INSERT INTO headlesscode_rsi_queue_policy(queue_name, max_in_flight, lease_ms, max_attempts, class_capacity, key_capacity)
|
|
229
|
+
VALUES($1,$2,$3,$4,$5::jsonb,$6::jsonb) ON CONFLICT(queue_name) DO NOTHING`,
|
|
230
|
+
[this.policy.queueName, this.policy.maxInFlight, this.policy.leaseMs, this.policy.maxAttempts, JSON.stringify(this.policy.classCapacity), JSON.stringify(this.policy.concurrencyKeyCapacity)],
|
|
231
|
+
)
|
|
232
|
+
void inserted
|
|
233
|
+
const persisted = await this.pool.query("SELECT max_in_flight, lease_ms, max_attempts, class_capacity, key_capacity FROM headlesscode_rsi_queue_policy WHERE queue_name=$1", [this.policy.queueName])
|
|
234
|
+
const row = persisted.rows[0]
|
|
235
|
+
if (!row || Number(row.max_in_flight) !== this.policy.maxInFlight || Number(row.lease_ms) !== this.policy.leaseMs || Number(row.max_attempts) !== this.policy.maxAttempts || stableJson(row.class_capacity) !== stableJson(this.policy.classCapacity) || stableJson(row.key_capacity) !== stableJson(this.policy.concurrencyKeyCapacity)) {
|
|
236
|
+
throw new Error(`persisted RSI queue policy for ${this.policy.queueName} differs from requested admission settings`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async registerWorker(registration: FleetWorkerRegistration): Promise<void> {
|
|
241
|
+
if (!registration.workerId.trim() || !registration.gatewayId.trim() || !registration.hostname.trim()) throw new Error("worker id, gateway id and hostname are required")
|
|
242
|
+
if (!Number.isInteger(registration.maxActive) || registration.maxActive < 1) throw new Error("worker maxActive must be a positive integer")
|
|
243
|
+
await this.pool.query(
|
|
244
|
+
`INSERT INTO headlesscode_rsi_workers(queue_name,worker_id,gateway_id,hostname,max_active,resource_classes,active,registered_at,heartbeat_at)
|
|
245
|
+
VALUES($1,$2,$3,$4,$5,$6::jsonb,true,clock_timestamp(),clock_timestamp())
|
|
246
|
+
ON CONFLICT(queue_name,worker_id) DO UPDATE SET gateway_id=EXCLUDED.gateway_id, hostname=EXCLUDED.hostname, max_active=EXCLUDED.max_active,
|
|
247
|
+
resource_classes=EXCLUDED.resource_classes, active=true, heartbeat_at=clock_timestamp()`,
|
|
248
|
+
[this.policy.queueName, registration.workerId, registration.gatewayId, registration.hostname, registration.maxActive, JSON.stringify(registration.resourceClasses)],
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async heartbeatWorker(workerId: string): Promise<boolean> {
|
|
253
|
+
const result = await this.pool.query("UPDATE headlesscode_rsi_workers SET heartbeat_at=clock_timestamp() WHERE queue_name=$1 AND worker_id=$2 AND active=true", [this.policy.queueName, workerId])
|
|
254
|
+
return result.rowCount === 1
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async activeWorkerCount(resourceClass?: ResourceClass): Promise<number> {
|
|
258
|
+
const result = await this.pool.query(
|
|
259
|
+
`SELECT count(*)::int AS count FROM headlesscode_rsi_workers
|
|
260
|
+
WHERE queue_name=$1 AND active=true AND heartbeat_at > clock_timestamp()-($2::text || ' milliseconds')::interval
|
|
261
|
+
AND ($3::text IS NULL OR resource_classes @> jsonb_build_array($3::text))`,
|
|
262
|
+
[this.policy.queueName, this.policy.leaseMs * 2, resourceClass ?? null],
|
|
263
|
+
)
|
|
264
|
+
return Number(result.rows[0]?.count ?? 0)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async stopWorker(workerId: string): Promise<void> {
|
|
268
|
+
await this.pool.query("UPDATE headlesscode_rsi_workers SET active=false, heartbeat_at=clock_timestamp() WHERE queue_name=$1 AND worker_id=$2", [this.policy.queueName, workerId])
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async putArtifact(content: Buffer): Promise<RsiArtifactReference> {
|
|
272
|
+
const reference = await this.artifactStore.put(content)
|
|
273
|
+
await this.pool.query("INSERT INTO headlesscode_rsi_artifacts(sha256,storage_id,object_key,backend,byte_length) VALUES($1,$2,$3,$4,$5) ON CONFLICT(sha256) DO NOTHING", [reference.sha256, reference.storageId, reference.objectKey, reference.backend, reference.byteLength])
|
|
274
|
+
const verified = await this.pool.query("SELECT storage_id,object_key,backend,byte_length FROM headlesscode_rsi_artifacts WHERE sha256=$1", [reference.sha256])
|
|
275
|
+
if (verified.rows[0]?.storage_id !== reference.storageId || verified.rows[0]?.object_key !== reference.objectKey || Number(verified.rows[0]?.byte_length) !== reference.byteLength || verified.rows[0]?.backend !== reference.backend) throw new Error("stored RSI artifact metadata did not match uploaded content")
|
|
276
|
+
return reference
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async getArtifact(sha256: string): Promise<Buffer> {
|
|
280
|
+
const result = await this.pool.query("SELECT storage_id,object_key,backend,byte_length FROM headlesscode_rsi_artifacts WHERE sha256=$1", [sha256])
|
|
281
|
+
if (result.rowCount !== 1) throw new Error(`RSI artifact not found: ${sha256}`)
|
|
282
|
+
const row = result.rows[0]
|
|
283
|
+
const reference: RsiArtifactReference = { sha256, storageId: row.storage_id, objectKey: row.object_key, byteLength: Number(row.byte_length), backend: row.backend }
|
|
284
|
+
if (reference.backend !== this.artifactStore.backend) throw new Error("RSI artifact backend differs from the configured worker artifact store")
|
|
285
|
+
return this.artifactStore.get(reference)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async enqueue(input: { idempotencyKey: string; runId: string; candidateId: string; jobKind: FleetJob["jobKind"]; resourceClass: ResourceClass; concurrencyKey?: string; artifactSha256: string; payload: FleetJobPayload }): Promise<FleetJob> {
|
|
289
|
+
if (!input.idempotencyKey.trim()) throw new Error("job idempotencyKey must not be empty")
|
|
290
|
+
if (input.payload.schemaVersion !== 1 || input.payload.snapshotSha256 !== input.artifactSha256 || !/^[0-9a-f]{40,64}$/.test(input.payload.snapshotCommit)) throw new Error("job payload must bind to its sanitized snapshot artifact and commit")
|
|
291
|
+
if (input.payload.snapshotBytes < 1) throw new Error("job payload declares an invalid snapshot size")
|
|
292
|
+
return inTransaction(this.pool, async (client) => {
|
|
293
|
+
const jobId = randomUUID()
|
|
294
|
+
const inserted = await client.query(
|
|
295
|
+
`INSERT INTO headlesscode_rsi_jobs(job_id,queue_name,idempotency_key,run_id,candidate_id,job_kind,resource_class,concurrency_key,artifact_sha256,payload,status,max_attempts,lease_duration_ms)
|
|
296
|
+
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb,'queued',$11,$12) ON CONFLICT(queue_name,idempotency_key) DO NOTHING RETURNING *`,
|
|
297
|
+
[jobId, this.policy.queueName, input.idempotencyKey, input.runId, input.candidateId, input.jobKind, input.resourceClass, input.concurrencyKey ?? null, input.artifactSha256, JSON.stringify(input.payload), this.policy.maxAttempts, Math.max(this.policy.leaseMs, requiredFleetJobLeaseMs(input.jobKind, input.payload.timeoutMs, input.payload.evalCommands?.length ?? 0))],
|
|
298
|
+
)
|
|
299
|
+
if (inserted.rowCount === 1) return rowToJob(inserted.rows[0])
|
|
300
|
+
const existing = await client.query("SELECT * FROM headlesscode_rsi_jobs WHERE queue_name=$1 AND idempotency_key=$2 FOR UPDATE", [this.policy.queueName, input.idempotencyKey])
|
|
301
|
+
const row = existing.rows[0]
|
|
302
|
+
if (!row || row.run_id !== input.runId || row.candidate_id !== input.candidateId || row.job_kind !== input.jobKind || row.resource_class !== input.resourceClass || row.concurrency_key !== (input.concurrencyKey ?? null) || row.artifact_sha256.trim() !== input.artifactSha256 || stableJson(row.payload) !== stableJson(input.payload)) {
|
|
303
|
+
throw new Error(`idempotency key ${input.idempotencyKey} was reused for different RSI job contents`)
|
|
304
|
+
}
|
|
305
|
+
return rowToJob(row)
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async claim(workerId: string): Promise<FleetJobLease | undefined> {
|
|
310
|
+
return inTransaction(this.pool, async (client) => {
|
|
311
|
+
const policyResult = await client.query("SELECT * FROM headlesscode_rsi_queue_policy WHERE queue_name=$1 FOR UPDATE", [this.policy.queueName])
|
|
312
|
+
const policy = policyResult.rows[0]
|
|
313
|
+
if (!policy) throw new Error("RSI fleet queue has not been migrated")
|
|
314
|
+
const workerResult = await client.query(
|
|
315
|
+
`SELECT *, heartbeat_at > clock_timestamp()-($3::text || ' milliseconds')::interval AS heartbeat_fresh
|
|
316
|
+
FROM headlesscode_rsi_workers WHERE queue_name=$1 AND worker_id=$2 FOR UPDATE`,
|
|
317
|
+
[this.policy.queueName, workerId, Number(policy.lease_ms) * 2],
|
|
318
|
+
)
|
|
319
|
+
const worker = workerResult.rows[0]
|
|
320
|
+
if (!worker?.active || worker.heartbeat_fresh !== true || !Array.isArray(worker.resource_classes)) return undefined
|
|
321
|
+
|
|
322
|
+
await client.query(
|
|
323
|
+
`WITH expired AS (
|
|
324
|
+
SELECT job_id FROM headlesscode_rsi_jobs WHERE queue_name=$1 AND status='leased' AND lease_expires_at <= clock_timestamp()
|
|
325
|
+
ORDER BY lease_expires_at LIMIT 100 FOR UPDATE SKIP LOCKED
|
|
326
|
+
)
|
|
327
|
+
UPDATE headlesscode_rsi_jobs AS jobs SET status=CASE WHEN jobs.attempts >= jobs.max_attempts THEN 'failed' ELSE 'queued' END,
|
|
328
|
+
error='worker lease expired before completion', lease_owner=NULL, lease_token=NULL, lease_expires_at=NULL, updated_at=clock_timestamp()
|
|
329
|
+
FROM expired WHERE jobs.job_id=expired.job_id`,
|
|
330
|
+
[this.policy.queueName],
|
|
331
|
+
)
|
|
332
|
+
const capacities = await client.query(
|
|
333
|
+
`SELECT count(*)::int AS total,
|
|
334
|
+
count(*) FILTER (WHERE lease_owner=$1)::int AS worker_total
|
|
335
|
+
FROM headlesscode_rsi_jobs WHERE queue_name=$2 AND status='leased' AND lease_expires_at > clock_timestamp()`,
|
|
336
|
+
[workerId, this.policy.queueName],
|
|
337
|
+
)
|
|
338
|
+
const active = capacities.rows[0]
|
|
339
|
+
if (Number(active.total) >= Number(policy.max_in_flight) || Number(active.worker_total) >= Number(worker.max_active)) return undefined
|
|
340
|
+
const allowedClasses = worker.resource_classes as ResourceClass[]
|
|
341
|
+
const next = await client.query(
|
|
342
|
+
`SELECT job.* FROM headlesscode_rsi_jobs AS job
|
|
343
|
+
JOIN headlesscode_rsi_queue_policy AS policy ON policy.queue_name=$1
|
|
344
|
+
WHERE job.queue_name=$1 AND job.status='queued' AND job.available_at <= clock_timestamp()
|
|
345
|
+
AND job.resource_class = ANY($2::text[])
|
|
346
|
+
AND (COALESCE((policy.class_capacity->>job.resource_class)::int, policy.max_in_flight) >
|
|
347
|
+
(SELECT count(*) FROM headlesscode_rsi_jobs AS active WHERE active.queue_name=$1 AND active.status='leased' AND active.lease_expires_at > clock_timestamp() AND active.resource_class=job.resource_class))
|
|
348
|
+
AND (job.concurrency_key IS NULL OR COALESCE((policy.key_capacity->>job.concurrency_key)::int, policy.max_in_flight) >
|
|
349
|
+
(SELECT count(*) FROM headlesscode_rsi_jobs AS active WHERE active.queue_name=$1 AND active.status='leased' AND active.lease_expires_at > clock_timestamp() AND active.concurrency_key=job.concurrency_key))
|
|
350
|
+
ORDER BY job.created_at, job.job_id LIMIT 1 FOR UPDATE OF job SKIP LOCKED`,
|
|
351
|
+
[this.policy.queueName, allowedClasses],
|
|
352
|
+
)
|
|
353
|
+
const jobRow = next.rows[0]
|
|
354
|
+
if (!jobRow) return undefined
|
|
355
|
+
const token = randomUUID()
|
|
356
|
+
const updated = await client.query(
|
|
357
|
+
`UPDATE headlesscode_rsi_jobs SET status='leased', attempts=attempts+1, lease_owner=$2, lease_token=$3,
|
|
358
|
+
lease_expires_at=clock_timestamp()+(lease_duration_ms::text || ' milliseconds')::interval, updated_at=clock_timestamp()
|
|
359
|
+
WHERE job_id=$1 AND queue_name=$4 AND status='queued' RETURNING *`,
|
|
360
|
+
[jobRow.job_id, workerId, token, this.policy.queueName],
|
|
361
|
+
)
|
|
362
|
+
return { job: rowToJob(updated.rows[0]), token, workerId }
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async heartbeat(lease: FleetJobLease): Promise<boolean> {
|
|
367
|
+
const result = await this.pool.query(
|
|
368
|
+
`UPDATE headlesscode_rsi_jobs SET lease_expires_at=clock_timestamp()+(lease_duration_ms::text || ' milliseconds')::interval, updated_at=clock_timestamp()
|
|
369
|
+
WHERE job_id=$1 AND queue_name=$4 AND lease_owner=$2 AND lease_token=$3 AND status='leased' AND lease_expires_at > clock_timestamp()`,
|
|
370
|
+
[lease.job.jobId, lease.workerId, lease.token, this.policy.queueName],
|
|
371
|
+
)
|
|
372
|
+
return result.rowCount === 1
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async complete(lease: FleetJobLease, resultValue: unknown): Promise<QueueMutationResult> {
|
|
376
|
+
const resultJson = JSON.stringify(resultValue)
|
|
377
|
+
return inTransaction(this.pool, async (client) => {
|
|
378
|
+
const updated = await client.query(
|
|
379
|
+
`UPDATE headlesscode_rsi_jobs SET status='completed', result=$4::jsonb, completed_at=clock_timestamp(), updated_at=clock_timestamp(),
|
|
380
|
+
lease_owner=NULL, lease_token=NULL, lease_expires_at=NULL
|
|
381
|
+
WHERE job_id=$1 AND queue_name=$5 AND lease_owner=$2 AND lease_token=$3 AND status='leased' AND lease_expires_at > clock_timestamp() RETURNING *`,
|
|
382
|
+
[lease.job.jobId, lease.workerId, lease.token, resultJson, this.policy.queueName],
|
|
383
|
+
)
|
|
384
|
+
if (updated.rowCount === 1) return { accepted: true, duplicate: false, job: rowToJob(updated.rows[0]) }
|
|
385
|
+
const existing = await client.query("SELECT * FROM headlesscode_rsi_jobs WHERE job_id=$1 AND queue_name=$2", [lease.job.jobId, this.policy.queueName])
|
|
386
|
+
const row = existing.rows[0]
|
|
387
|
+
if (row?.status === "completed" && stableJson(row.result) === stableJson(resultValue)) return { accepted: true, duplicate: true, job: rowToJob(row) }
|
|
388
|
+
return { accepted: false, duplicate: false, ...(row ? { job: rowToJob(row) } : {}) }
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async fail(lease: FleetJobLease, error: string): Promise<boolean> {
|
|
393
|
+
const result = await this.pool.query(
|
|
394
|
+
`UPDATE headlesscode_rsi_jobs SET status=CASE WHEN attempts >= max_attempts THEN 'failed' ELSE 'queued' END, error=$4,
|
|
395
|
+
updated_at=clock_timestamp(), lease_owner=NULL, lease_token=NULL, lease_expires_at=NULL
|
|
396
|
+
WHERE job_id=$1 AND queue_name=$5 AND lease_owner=$2 AND lease_token=$3 AND status='leased' AND lease_expires_at > clock_timestamp()`,
|
|
397
|
+
[lease.job.jobId, lease.workerId, lease.token, error.slice(0, 4000), this.policy.queueName],
|
|
398
|
+
)
|
|
399
|
+
return result.rowCount === 1
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async getJob(jobId: string): Promise<FleetJob | undefined> {
|
|
403
|
+
const result = await this.pool.query("SELECT * FROM headlesscode_rsi_jobs WHERE job_id=$1 AND queue_name=$2", [jobId, this.policy.queueName])
|
|
404
|
+
return result.rowCount === 1 ? rowToJob(result.rows[0]) : undefined
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async getJobByIdempotencyKey(idempotencyKey: string): Promise<FleetJob | undefined> {
|
|
408
|
+
const result = await this.pool.query("SELECT * FROM headlesscode_rsi_jobs WHERE queue_name=$1 AND idempotency_key=$2", [this.policy.queueName, idempotencyKey])
|
|
409
|
+
return result.rowCount === 1 ? rowToJob(result.rows[0]) : undefined
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async cancel(jobId: string, reason: string): Promise<boolean> {
|
|
413
|
+
const result = await this.pool.query(
|
|
414
|
+
`UPDATE headlesscode_rsi_jobs SET status='cancelled', error=$3, updated_at=clock_timestamp(), lease_owner=NULL, lease_token=NULL, lease_expires_at=NULL
|
|
415
|
+
WHERE queue_name=$1 AND job_id=$2 AND status='queued'`,
|
|
416
|
+
[this.policy.queueName, jobId, reason.slice(0, 4000)],
|
|
417
|
+
)
|
|
418
|
+
return result.rowCount === 1
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async close(): Promise<void> {
|
|
422
|
+
await Promise.all([this.pool.end(), Promise.resolve(this.artifactStore.close?.())])
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { promoteArchivedCurriculumTask } from "./curriculum.js"
|
|
2
|
+
|
|
3
|
+
function argument(flag: string): string | undefined {
|
|
4
|
+
const index = process.argv.indexOf(flag)
|
|
5
|
+
return index >= 0 ? process.argv[index + 1] : undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function main(): Promise<void> {
|
|
9
|
+
const archiveDir = argument("--archive-dir")
|
|
10
|
+
const taskId = argument("--task-id")
|
|
11
|
+
if (!archiveDir || !taskId || process.argv.some((value, index) => value.startsWith("--") && !["--archive-dir", "--task-id"].includes(value)) || process.argv.filter((value) => value === "--archive-dir").length !== 1 || process.argv.filter((value) => value === "--task-id").length !== 1) {
|
|
12
|
+
throw new Error("usage: headlesscode-rsi-promote-curriculum --archive-dir <path> --task-id <id>")
|
|
13
|
+
}
|
|
14
|
+
const task = await promoteArchivedCurriculumTask(archiveDir, taskId)
|
|
15
|
+
process.stdout.write(`${task.id} promoted at ${task.promotedAt}\n`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
main().catch((error: unknown) => {
|
|
19
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
20
|
+
process.exitCode = 1
|
|
21
|
+
})
|
package/src/rsi/reports.ts
CHANGED
|
@@ -5,7 +5,8 @@ export function formatCandidate(candidate: CandidateRecord): string {
|
|
|
5
5
|
const parent = candidate.parentSelection ? `, parent=${candidate.parent} (${candidate.parentSelection.strategy})` : ""
|
|
6
6
|
const mutation = candidate.mutationKind ? `, mutation=${candidate.mutationKind}` : ""
|
|
7
7
|
const metrics = fitness ? `, metrics=${JSON.stringify(fitness.metrics)}` : ""
|
|
8
|
-
|
|
8
|
+
const comparison = candidate.pairedComparison ? `, paired=${candidate.pairedComparison.reason} (${candidate.pairedComparison.baselinePasses}/${candidate.pairedComparison.trialCount} → ${candidate.pairedComparison.candidatePasses}/${candidate.pairedComparison.trialCount}; duration ratio ${candidate.pairedComparison.durationRatio.toFixed(3)})` : ""
|
|
9
|
+
return `${candidate.id}: ${candidate.status}${parent}${mutation}${fitness ? `, score=${fitness.score}, ${fitness.reason}` : ""}${comparison}${metrics}`
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function formatRunReport(run: RsiRunRecord, config: RsiConfig): string {
|
|
@@ -17,20 +18,46 @@ export function formatRunReport(run: RsiRunRecord, config: RsiConfig): string {
|
|
|
17
18
|
`- Generations: ${run.generations}`,
|
|
18
19
|
`- Started: ${run.startedAt}`,
|
|
19
20
|
`- Finished: ${run.finishedAt ?? "in progress"}`,
|
|
20
|
-
`- Baseline: ${run.baseline ? (run.baseline.ok ? "pass" : "fail") : "not run (dry-run)"}`,
|
|
21
|
+
`- Baseline: ${run.baseline ? (run.baseline.ok ? "pass" : "fail") : "not run (dry-run)"}${run.baselineFitness ? `, score=${run.baselineFitness.score}` : ""}`,
|
|
21
22
|
`- Selected candidate: ${run.selected ?? "none"}`,
|
|
22
23
|
`- Pareto candidates: ${run.selectedCandidates?.join(", ") || "none"}`,
|
|
23
24
|
`- Parent policy: ${run.parentSelectionPolicy ?? config.parentSelectionPolicy ?? "champion-specialist-novelty"}`,
|
|
24
25
|
`- Model candidates: ${run.modelCandidates?.map((model) => `${model.id} (${model.status})`).join(", ") || "none"}`,
|
|
25
26
|
`- Experiment jobs: ${run.jobs?.length ?? 0}`,
|
|
26
27
|
`- Trajectories: ${run.trajectoryRefs?.length ?? 0}`,
|
|
28
|
+
...(config.computePolicy === "adaptive-independent" ? [
|
|
29
|
+
`- Adaptive budgets: trajectories ${config.maxTrajectories ?? config.population + 1}; total iterations ${config.maxTotalIterations ?? (config.population + 1) * config.maxIterations}; runtime admission ${config.maxRuntimeMs ?? 60 * 60_000} ms; max concurrent ${config.maxConcurrent}`,
|
|
30
|
+
] : []),
|
|
27
31
|
"",
|
|
28
32
|
"## Candidates",
|
|
29
33
|
"",
|
|
30
34
|
...run.candidates.map((candidate) => `- ${formatCandidate(candidate)}${candidate.failure ? `; failure=${candidate.failure}` : ""}`),
|
|
31
35
|
"",
|
|
36
|
+
...(run.adaptiveSearch?.length ? [
|
|
37
|
+
"## Adaptive allocation decisions",
|
|
38
|
+
"",
|
|
39
|
+
...run.adaptiveSearch.map((decision) => `- Generation ${decision.generation}: ${decision.decision} (${decision.reason}); candidates=${decision.candidateIds.join(", ") || "none"}; evidence=${decision.evidence.map((entry) => `${entry.candidateId}:${entry.status}${entry.visiblePassRate === undefined ? "" : `:${entry.visiblePassRate.toFixed(3)}`}`).join(", ") || "none"}; allocated=${decision.allocatedTrajectories} trajectories/${decision.allocatedIterations} iterations; remaining=${decision.remainingTrajectories} trajectories/${decision.remainingIterations} iterations; elapsed=${decision.elapsedMs} ms.`),
|
|
40
|
+
"",
|
|
41
|
+
] : []),
|
|
42
|
+
...(run.curriculumTasks?.length ? [
|
|
43
|
+
"## Curriculum fixture status",
|
|
44
|
+
"",
|
|
45
|
+
...run.curriculumTasks.map((task) => `- ${task.id}: ${task.promotedAt ? `promoted at ${task.promotedAt}` : task.validated ? "fixture-replay validated; not promoted" : "proposal; unvalidated"}; fixture=${task.fixtureId}; ${task.fixtureDescription}${task.validation ? `; base=${task.validation.baseCommit}; replays=${task.validation.replays.length}; reproducible=${task.validation.reproducible}; reason=${task.validation.reason}` : "; no executable validation recorded"}`),
|
|
46
|
+
"",
|
|
47
|
+
"Validated means the registered fixture reference passed, a known incorrect mutant was rejected, and two clean OpenShell replays produced identical output digests. It does not certify that task wording measures the named capability.",
|
|
48
|
+
"",
|
|
49
|
+
] : []),
|
|
50
|
+
...(run.adversarialReviews?.length ? [
|
|
51
|
+
"## Adversarial review",
|
|
52
|
+
"",
|
|
53
|
+
...run.adversarialReviews.map((review) => `- ${review.candidateId}: ${review.status}; role=${review.role}; provider=${review.provider}; model=${review.model}; promptVersion=${review.promptVersion}; prompt=${review.promptSha256 || "unavailable"}; result=${review.resultSha256 || "unavailable"}; error=${review.errorSha256 || "none"}; tests=${review.testArtifactSha256 || "unavailable"}; OpenShell output=${review.testResults.map((entry) => entry.outputArtifactSha256).filter(Boolean).join(", ") || "none"}; penalty=${review.penaltyPoints}; findings=${review.findings.map((finding) => finding.severity + ":" + finding.message.replace(/\s+/g, " ").slice(0, 240)).join(" | ") || "none"}; ${review.testResults.map((entry) => entry.id + ":" + (entry.passed ? "pass" : "fail")).join(", ") || review.error || "no test result"}${review.summary ? `; summary=${review.summary.replace(/\s+/g, " ").slice(0, 400)}` : ""}`),
|
|
54
|
+
"",
|
|
55
|
+
"Adversarial tests run in a fresh OpenShell evaluation guest. The coordinator alone applies their pass/fail gate and bounded finding penalty.",
|
|
56
|
+
"",
|
|
57
|
+
] : []),
|
|
32
58
|
"## Guardrails",
|
|
33
59
|
"",
|
|
60
|
+
`- Regression gate: ${config.regressionCommand}`,
|
|
34
61
|
`- Visible evaluations: ${config.evalCommands.join("; ") || "none"}`,
|
|
35
62
|
`- Hidden evaluations: ${config.hiddenEvalCommands.length || 0}`,
|
|
36
63
|
`- Compute policy: ${config.computePolicy ?? "single"}`,
|
package/src/rsi/roles.ts
CHANGED
|
@@ -28,9 +28,19 @@ export function resolveRoles(
|
|
|
28
28
|
const roles: RsiRoleConfig = { ...defaultRoles(workerModel), ...configured }
|
|
29
29
|
for (const role of ROLE_ENV_NAMES) {
|
|
30
30
|
const model = env[envKey(role)]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const prefix = `HEADLESSCODE_RSI_ROLE_${role.toUpperCase().replace(/-/g, "_")}_`
|
|
32
|
+
const provider = env[`${prefix}PROVIDER`]?.trim()
|
|
33
|
+
const baseUrl = env[`${prefix}BASE_URL`]?.trim()
|
|
34
|
+
const command = env[`${prefix}COMMAND`]?.trim()
|
|
35
|
+
if (provider && !["ollama", "openrouter", "command"].includes(provider)) throw new Error(`${prefix}PROVIDER must be ollama, openrouter, or command`)
|
|
36
|
+
if (model || provider || baseUrl || command) {
|
|
37
|
+
roles[role] = {
|
|
38
|
+
...(roles[role] ?? { provider: "ollama" as const, model: workerModel }),
|
|
39
|
+
...(model ? { model } : {}),
|
|
40
|
+
...(provider ? { provider: provider as RoleModelConfig["provider"] } : {}),
|
|
41
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
42
|
+
...(command ? { command } : {}),
|
|
43
|
+
}
|
|
34
44
|
}
|
|
35
45
|
}
|
|
36
46
|
return roles
|
package/src/rsi/selection.ts
CHANGED
|
@@ -31,7 +31,13 @@ function metrics(candidate: CandidateRecord): MetricVector {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function eligible(candidates: CandidateRecord[]): CandidateRecord[] {
|
|
34
|
-
return candidates.filter((candidate) =>
|
|
34
|
+
return candidates.filter((candidate) =>
|
|
35
|
+
candidate.status === "accepted" &&
|
|
36
|
+
candidate.fitness &&
|
|
37
|
+
Object.values(candidate.fitness.hardGates).every(Boolean) &&
|
|
38
|
+
candidate.pairedComparison?.improved === true &&
|
|
39
|
+
candidate.commits.length > 0,
|
|
40
|
+
)
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export interface ParentChoice {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { execFileSync } from "node:child_process"
|
|
3
|
+
import * as fs from "node:fs"
|
|
4
|
+
import * as path from "node:path"
|
|
5
|
+
|
|
6
|
+
export const RSI_TRAINING_FIXTURE_IDS = ["completion-discipline", "regression-recovery", "tool-efficiency"] as const
|
|
7
|
+
export const RSI_MODEL_EVAL_FIXTURE_IDS = ["generalization"] as const
|
|
8
|
+
|
|
9
|
+
export interface VerifiedTrainingDataset {
|
|
10
|
+
version: string
|
|
11
|
+
dataset: Buffer
|
|
12
|
+
datasetSha256: string
|
|
13
|
+
evaluationCells: Buffer
|
|
14
|
+
evaluationCellsSha256: string
|
|
15
|
+
manifest: Buffer
|
|
16
|
+
manifestSha256: string
|
|
17
|
+
trainingCellIds: string[]
|
|
18
|
+
evaluationCellIds: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function digest(value: string | Buffer): string {
|
|
22
|
+
return createHash("sha256").update(value).digest("hex")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sourceRecord(root: string, relativePath: string): { path: string; sha256: string } {
|
|
26
|
+
const content = fs.readFileSync(path.join(root, relativePath))
|
|
27
|
+
return { path: relativePath, sha256: digest(content) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Build only from evaluator-owned fixtures whose reference passes and known mutant fails. */
|
|
31
|
+
export function buildVerifiedTrainingDataset(repoRoot: string): VerifiedTrainingDataset {
|
|
32
|
+
const fixtureRoot = path.join(repoRoot, "fixtures", "rsi-curriculum")
|
|
33
|
+
const validatorPath = path.join(fixtureRoot, "validate.mjs")
|
|
34
|
+
const sourceFiles = [sourceRecord(repoRoot, "fixtures/rsi-curriculum/validate.mjs")]
|
|
35
|
+
const trainingCellIds: string[] = []
|
|
36
|
+
const evaluationCellIds: string[] = []
|
|
37
|
+
const rows: Array<{ id: string; prompt: string; completion: string }> = []
|
|
38
|
+
const evaluationRows: Array<{ id: string; prompt: string; completion: string }> = []
|
|
39
|
+
const fixtureDigests: Record<string, string> = {}
|
|
40
|
+
for (const fixtureId of [...RSI_TRAINING_FIXTURE_IDS, ...RSI_MODEL_EVAL_FIXTURE_IDS]) {
|
|
41
|
+
execFileSync(process.execPath, [validatorPath, "--fixture", fixtureId], { cwd: repoRoot, timeout: 10_000, stdio: "pipe" })
|
|
42
|
+
const fixture = JSON.parse(fs.readFileSync(path.join(fixtureRoot, fixtureId + ".json"), "utf8")) as { id?: string; task?: string; cases?: Array<{ name?: string; input?: unknown[]; expected?: unknown }> }
|
|
43
|
+
if (fixture.id !== fixtureId || typeof fixture.task !== "string" || !Array.isArray(fixture.cases) || fixture.cases.length === 0) throw new Error("registered RSI training fixture is malformed: " + fixtureId)
|
|
44
|
+
const paths = [
|
|
45
|
+
"fixtures/rsi-curriculum/" + fixtureId + ".json",
|
|
46
|
+
"fixtures/rsi-curriculum/cases/" + fixtureId + ".mjs",
|
|
47
|
+
"fixtures/rsi-curriculum/mutants/" + fixtureId + ".mjs",
|
|
48
|
+
]
|
|
49
|
+
for (const source of paths) sourceFiles.push(sourceRecord(repoRoot, source))
|
|
50
|
+
fixtureDigests[fixtureId] = digest(fs.readFileSync(path.join(repoRoot, paths[0]!)))
|
|
51
|
+
if ((RSI_TRAINING_FIXTURE_IDS as readonly string[]).includes(fixtureId)) {
|
|
52
|
+
const completion = fs.readFileSync(path.join(fixtureRoot, "cases", fixtureId + ".mjs"), "utf8").trim()
|
|
53
|
+
rows.push({
|
|
54
|
+
id: fixtureId,
|
|
55
|
+
prompt: "Implement this task as a JavaScript module exporting solve(...): " + fixture.task,
|
|
56
|
+
completion,
|
|
57
|
+
})
|
|
58
|
+
trainingCellIds.push(fixtureId)
|
|
59
|
+
} else {
|
|
60
|
+
const completion = fs.readFileSync(path.join(fixtureRoot, "cases", fixtureId + ".mjs"), "utf8").trim()
|
|
61
|
+
for (const item of fixture.cases) {
|
|
62
|
+
const id = fixtureId + ":" + (item.name ?? "")
|
|
63
|
+
evaluationCellIds.push(id)
|
|
64
|
+
evaluationRows.push({
|
|
65
|
+
id,
|
|
66
|
+
prompt: "Implement this task as a JavaScript module exporting solve(...): " + fixture.task + "\nCase input: " + JSON.stringify(item.input) + "\nExpected behavior: " + JSON.stringify(item.expected),
|
|
67
|
+
completion,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const dataset = Buffer.from(rows.map((row) => JSON.stringify(row)).join("\n") + "\n")
|
|
73
|
+
const datasetSha256 = digest(dataset)
|
|
74
|
+
const evaluationCells = Buffer.from(evaluationRows.map((row) => JSON.stringify(row)).join("\n") + "\n")
|
|
75
|
+
const evaluationCellsSha256 = digest(evaluationCells)
|
|
76
|
+
const manifestValue = {
|
|
77
|
+
schemaVersion: 1,
|
|
78
|
+
backend: "transformers-peft-qlora-v1",
|
|
79
|
+
trainingFixtureIds: [...RSI_TRAINING_FIXTURE_IDS],
|
|
80
|
+
evaluationFixtureIds: [...RSI_MODEL_EVAL_FIXTURE_IDS],
|
|
81
|
+
trainingCellIds,
|
|
82
|
+
evaluationCellIds,
|
|
83
|
+
fixtureDigests,
|
|
84
|
+
sourceFiles: sourceFiles.sort((a, b) => a.path.localeCompare(b.path)),
|
|
85
|
+
datasetSha256,
|
|
86
|
+
evaluationCellsSha256,
|
|
87
|
+
rowCount: rows.length,
|
|
88
|
+
maxSteps: 8,
|
|
89
|
+
seed: 1337,
|
|
90
|
+
}
|
|
91
|
+
const manifest = Buffer.from(JSON.stringify(manifestValue, null, 2) + "\n")
|
|
92
|
+
return {
|
|
93
|
+
version: digest(manifest),
|
|
94
|
+
dataset,
|
|
95
|
+
datasetSha256,
|
|
96
|
+
evaluationCells,
|
|
97
|
+
evaluationCellsSha256,
|
|
98
|
+
manifest,
|
|
99
|
+
manifestSha256: digest(manifest),
|
|
100
|
+
trainingCellIds,
|
|
101
|
+
evaluationCellIds,
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/rsi/trajectory.ts
CHANGED
|
@@ -59,7 +59,7 @@ export async function captureCandidateTrajectory(
|
|
|
59
59
|
const messages = calls.flatMap((call) => call.messages ?? [])
|
|
60
60
|
const toolCalls = calls.reduce((count, call) => count + (call.response?.message?.tool_calls?.length ?? 0), 0)
|
|
61
61
|
const outcome = trajectoryOutcome(candidate)
|
|
62
|
-
const trusted = outcome === "success" && candidate.fitness
|
|
62
|
+
const trusted = outcome === "success" && candidate.fitness !== undefined && Object.values(candidate.fitness.hardGates).every(Boolean)
|
|
63
63
|
const record: TrajectoryRecord = {
|
|
64
64
|
id: `trajectory-${candidate.id}`,
|
|
65
65
|
task: candidate.mutation,
|