orizu 0.5.21 → 0.5.22
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 +9 -6
- package/dist/gepa-engine-dispatch.d.ts.map +1 -1
- package/dist/gepa-engine-dispatch.js +20 -1
- package/dist/gepa-python-paths.d.ts.map +1 -1
- package/dist/gepa-python-paths.js +25 -6
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +36 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -39
- package/dist/instruction-set-loader.d.ts.map +1 -1
- package/dist/instruction-set-loader.js +57 -5
- package/dist/instruction-sets-cli.d.ts +5 -2
- package/dist/instruction-sets-cli.d.ts.map +1 -1
- package/dist/instruction-sets-cli.js +134 -24
- package/dist/skilled-proposer-config.d.ts +2 -0
- package/dist/skilled-proposer-config.d.ts.map +1 -0
- package/dist/skilled-proposer-config.js +212 -0
- package/dist/skilled-proposer-launch.d.ts +4 -0
- package/dist/skilled-proposer-launch.d.ts.map +1 -1
- package/dist/skilled-proposer-launch.js +65 -1
- package/package.json +1 -1
- package/src/gepa-engine-dispatch.ts +21 -1
- package/src/gepa-python-paths.ts +24 -6
- package/src/help.ts +36 -14
- package/src/index.ts +6 -53
- package/src/instruction-set-loader.ts +55 -5
- package/src/instruction-sets-cli.ts +173 -19
- package/src/skilled-proposer-config.ts +191 -0
- package/src/skilled-proposer-launch.ts +59 -1
- package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +36 -3
- package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +31 -3
- package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +354 -5
- package/vendor/skills/orizu-cli/SKILL.md +50 -16
- package/vendor/skills/orizu-cli/references/building-judges.md +2 -2
- package/vendor/skills/orizu-cli/references/cli-reference.md +57 -28
- package/vendor/skills/orizu-cli/references/instructions-after-prompts.md +120 -0
- package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +109 -49
- package/vendor/skills/orizu-cli/references/optimization-reports.md +16 -11
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +29 -28
- package/vendor/skills/orizu-cli/references/primer.md +21 -5
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +132 -85
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { readFileSync, statSync } from 'node:fs'
|
|
3
|
+
import { basename, dirname, extname, relative, resolve, sep } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
interface NormalizedTextSource { source: string, content: string, sha256: string, byteLength: number }
|
|
7
|
+
interface NormalizedSkill extends NormalizedTextSource { name: string, description: string | null }
|
|
8
|
+
|
|
9
|
+
const CONFIG_KEYS = new Set(['schemaVersion', 'skills', 'additionalInstructionsFile', 'baseInstructionsFile',
|
|
10
|
+
'maxWords', 'maxTokens', 'maxExamples', 'onError',
|
|
11
|
+
])
|
|
12
|
+
const PATH_SKILL_KEYS = new Set(['path', 'name', 'description'])
|
|
13
|
+
const INLINE_SKILL_KEYS = new Set(['name', 'description', 'inline'])
|
|
14
|
+
|
|
15
|
+
function configError(field: string, detail: string): never { throw new Error(`--candidate-proposer-config ${field} ${detail}`) }
|
|
16
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
18
|
+
}
|
|
19
|
+
function rejectUnknownKeys(value: Record<string, unknown>, allowed: Set<string>, field = ''): void {
|
|
20
|
+
const unknown = Object.keys(value).find(key => !allowed.has(key))
|
|
21
|
+
if (unknown) configError(field ? `${field}.${unknown}` : unknown, 'is not supported')
|
|
22
|
+
}
|
|
23
|
+
function sha256(bytes: Uint8Array): string { return createHash('sha256').update(bytes).digest('hex') }
|
|
24
|
+
function decodeUtf8(bytes: Uint8Array, field: string): string {
|
|
25
|
+
try {
|
|
26
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes)
|
|
27
|
+
} catch {
|
|
28
|
+
configError(field, 'must contain valid UTF-8 text')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function readRegularFile(path: string, field: string): Buffer {
|
|
32
|
+
try {
|
|
33
|
+
if (!statSync(path).isFile()) configError(field, 'must resolve to a file')
|
|
34
|
+
return readFileSync(path)
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error instanceof Error && error.message.startsWith('--candidate-proposer-config')) throw error
|
|
37
|
+
configError(field, `is missing or unreadable: ${error instanceof Error ? error.message : String(error)}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function logicalSource(configDirectory: string, path: string): string {
|
|
41
|
+
return relative(configDirectory, path).split(sep).join('/') || basename(path)
|
|
42
|
+
}
|
|
43
|
+
function normalizedText(bytes: Buffer, source: string, field: string): NormalizedTextSource {
|
|
44
|
+
const content = decodeUtf8(bytes, field)
|
|
45
|
+
if (!content.trim()) configError(field, 'must not be empty')
|
|
46
|
+
return { source, content, sha256: sha256(bytes), byteLength: bytes.byteLength }
|
|
47
|
+
}
|
|
48
|
+
function parseFrontmatter(content: string, field: string): { name?: string, description?: string, body: string } {
|
|
49
|
+
const lines = content.split(/\r?\n/)
|
|
50
|
+
if (lines[0]?.replace(/^\uFEFF/, '').trim() !== '---') return { body: content }
|
|
51
|
+
const end = lines.slice(1).findIndex(line => line.trim() === '---')
|
|
52
|
+
if (end < 0) configError(field, 'has malformed frontmatter without a closing ---')
|
|
53
|
+
const metadata: Record<string, string> = {}
|
|
54
|
+
for (const raw of lines.slice(1, end + 1)) {
|
|
55
|
+
if (/^[ \t]/.test(raw) || !raw.includes(':')) continue
|
|
56
|
+
const colon = raw.indexOf(':')
|
|
57
|
+
metadata[raw.slice(0, colon).trim()] = raw.slice(colon + 1).trim().replace(/^(['"])(.*)\1$/, '$2')
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
name: metadata.name || undefined,
|
|
61
|
+
description: metadata.description || undefined,
|
|
62
|
+
body: lines.slice(end + 2).join('\n'),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function wellFormedString(value: string, field: string): string {
|
|
66
|
+
if (!(value as string & { isWellFormed(): boolean }).isWellFormed()) {
|
|
67
|
+
configError(field, 'must contain well-formed Unicode text')
|
|
68
|
+
}
|
|
69
|
+
return value
|
|
70
|
+
}
|
|
71
|
+
function nonEmptyString(value: unknown, field: string): string {
|
|
72
|
+
if (typeof value !== 'string' || !value.trim()) configError(field, 'must be a non-empty string')
|
|
73
|
+
return wellFormedString(value, field)
|
|
74
|
+
}
|
|
75
|
+
function nullableDescription(value: unknown, field: string): string | null {
|
|
76
|
+
if (value === null) return null
|
|
77
|
+
if (typeof value !== 'string') configError(field, 'must be a string or null')
|
|
78
|
+
return wellFormedString(value, field) || null
|
|
79
|
+
}
|
|
80
|
+
function normalizePathSkill(entry: Record<string, unknown>, index: number, configDirectory: string): NormalizedSkill {
|
|
81
|
+
const field = `skills[${index}]`
|
|
82
|
+
rejectUnknownKeys(entry, PATH_SKILL_KEYS, field)
|
|
83
|
+
const configuredPath = nonEmptyString(entry.path, `${field}.path`)
|
|
84
|
+
let resolvedPath = resolve(configDirectory, configuredPath)
|
|
85
|
+
try {
|
|
86
|
+
if (statSync(resolvedPath).isDirectory()) {
|
|
87
|
+
resolvedPath = resolve(resolvedPath, 'SKILL.md')
|
|
88
|
+
try {
|
|
89
|
+
if (!statSync(resolvedPath).isFile()) configError(`${field}.path`, 'directory must contain SKILL.md')
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof Error && error.message.startsWith('--candidate-proposer-config')) throw error
|
|
92
|
+
configError(`${field}.path`, 'directory must contain SKILL.md')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof Error && error.message.startsWith('--candidate-proposer-config')) throw error
|
|
97
|
+
configError(`${field}.path`, 'is missing or unreadable')
|
|
98
|
+
}
|
|
99
|
+
const text = normalizedText(readRegularFile(resolvedPath, `${field}.path`),
|
|
100
|
+
logicalSource(configDirectory, resolvedPath), `${field}.path`)
|
|
101
|
+
const frontmatter = parseFrontmatter(text.content, `${field}.path`)
|
|
102
|
+
if (!frontmatter.body.trim()) configError(`${field}.path`, 'must contain non-empty skill content')
|
|
103
|
+
const fallbackName = extname(resolvedPath)
|
|
104
|
+
? basename(resolvedPath, extname(resolvedPath))
|
|
105
|
+
: basename(resolvedPath)
|
|
106
|
+
const name = entry.name === undefined
|
|
107
|
+
? frontmatter.name ?? (basename(resolvedPath) === 'SKILL.md' ? basename(dirname(resolvedPath)) : fallbackName)
|
|
108
|
+
: nonEmptyString(entry.name, `${field}.name`)
|
|
109
|
+
const description = entry.description === undefined
|
|
110
|
+
? frontmatter.description ?? null
|
|
111
|
+
: nullableDescription(entry.description, `${field}.description`)
|
|
112
|
+
return { name, description, ...text }
|
|
113
|
+
}
|
|
114
|
+
function normalizeInlineSkill(entry: Record<string, unknown>, index: number): NormalizedSkill {
|
|
115
|
+
const field = `skills[${index}]`
|
|
116
|
+
rejectUnknownKeys(entry, INLINE_SKILL_KEYS, field)
|
|
117
|
+
const name = nonEmptyString(entry.name, `${field}.name`)
|
|
118
|
+
const description = nonEmptyString(entry.description, `${field}.description`)
|
|
119
|
+
const content = nonEmptyString(entry.inline, `${field}.inline`)
|
|
120
|
+
const bytes = Buffer.from(content, 'utf8')
|
|
121
|
+
return { name, description, source: `inline:${name}`, content,
|
|
122
|
+
sha256: sha256(bytes), byteLength: bytes.byteLength }
|
|
123
|
+
}
|
|
124
|
+
function normalizeSkill(value: unknown, index: number, configDirectory: string): NormalizedSkill {
|
|
125
|
+
const field = `skills[${index}]`
|
|
126
|
+
if (!isRecord(value)) configError(field, 'must be an object')
|
|
127
|
+
const hasPath = Object.hasOwn(value, 'path')
|
|
128
|
+
const hasInline = Object.hasOwn(value, 'inline')
|
|
129
|
+
if (hasPath === hasInline) configError(field, 'must contain exactly one of path or inline')
|
|
130
|
+
return hasPath ? normalizePathSkill(value, index, configDirectory) : normalizeInlineSkill(value, index)
|
|
131
|
+
}
|
|
132
|
+
function optionalPositiveInteger(value: unknown, field: string): number | null {
|
|
133
|
+
if (value === undefined || value === null) return null
|
|
134
|
+
if (!Number.isSafeInteger(value) || (value as number) <= 0) configError(field, 'must be a positive integer or null')
|
|
135
|
+
return value as number
|
|
136
|
+
}
|
|
137
|
+
function normalizeInstructionFile(value: unknown, field: string, configDirectory: string): NormalizedTextSource | null {
|
|
138
|
+
if (value === undefined || value === null) return null
|
|
139
|
+
const configuredPath = nonEmptyString(value, field)
|
|
140
|
+
const resolvedPath = resolve(configDirectory, configuredPath)
|
|
141
|
+
return normalizedText(readRegularFile(resolvedPath, field), logicalSource(configDirectory, resolvedPath), field)
|
|
142
|
+
}
|
|
143
|
+
function lockedPackageVersion(): string {
|
|
144
|
+
const lockPath = fileURLToPath(new URL('../requirements/skilled-proposer.lock', import.meta.url))
|
|
145
|
+
const match = /^skilled-proposer==([^\s\\]+)/m.exec(readFileSync(lockPath, 'utf8'))
|
|
146
|
+
if (!match) throw new Error('ALI_1505_SKILLED_PROPOSER_LOCK_VERSION_MISSING')
|
|
147
|
+
return match[1]
|
|
148
|
+
}
|
|
149
|
+
export function normalizedSkilledProposerConfig(value: string): string {
|
|
150
|
+
const suppliedPath = nonEmptyString(value.startsWith('@') ? value.slice(1) : value, 'path')
|
|
151
|
+
const configPath = resolve(suppliedPath)
|
|
152
|
+
const configBytes = readRegularFile(configPath, 'path')
|
|
153
|
+
const raw = decodeUtf8(configBytes, 'path').replace(/^\uFEFF/, '')
|
|
154
|
+
let parsed: unknown
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(raw)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
configError('path', `must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`)
|
|
159
|
+
}
|
|
160
|
+
if (!isRecord(parsed)) configError('path', 'must contain a JSON object')
|
|
161
|
+
rejectUnknownKeys(parsed, CONFIG_KEYS)
|
|
162
|
+
if (parsed.schemaVersion !== 1) configError('schemaVersion', 'must be 1')
|
|
163
|
+
if (!Array.isArray(parsed.skills)) configError('skills', 'must be an array')
|
|
164
|
+
|
|
165
|
+
const configDirectory = dirname(configPath)
|
|
166
|
+
const skills = parsed.skills.map((skill, index) => normalizeSkill(skill, index, configDirectory))
|
|
167
|
+
const names = new Set<string>()
|
|
168
|
+
for (const skill of skills) {
|
|
169
|
+
if (names.has(skill.name)) configError('skills', `contains duplicate resolved name ${skill.name}`)
|
|
170
|
+
names.add(skill.name)
|
|
171
|
+
}
|
|
172
|
+
const onError = parsed.onError ?? 'keep'
|
|
173
|
+
if (onError !== 'keep' && onError !== 'raise') configError('onError', 'must be keep or raise')
|
|
174
|
+
|
|
175
|
+
const normalized = {
|
|
176
|
+
schemaVersion: 1,
|
|
177
|
+
implementation: 'cmpnd-ai/skilled-proposer',
|
|
178
|
+
packageVersion: lockedPackageVersion(),
|
|
179
|
+
skills,
|
|
180
|
+
additionalInstructions: normalizeInstructionFile(parsed.additionalInstructionsFile,
|
|
181
|
+
'additionalInstructionsFile', configDirectory),
|
|
182
|
+
baseInstructions: normalizeInstructionFile(parsed.baseInstructionsFile, 'baseInstructionsFile', configDirectory),
|
|
183
|
+
maxWords: optionalPositiveInteger(parsed.maxWords, 'maxWords'),
|
|
184
|
+
maxTokens: optionalPositiveInteger(parsed.maxTokens, 'maxTokens'),
|
|
185
|
+
maxExamples: optionalPositiveInteger(parsed.maxExamples, 'maxExamples'),
|
|
186
|
+
onError,
|
|
187
|
+
}
|
|
188
|
+
const configFileSha256 = sha256(configBytes)
|
|
189
|
+
const configSha256 = sha256(Buffer.from(JSON.stringify({ configFileSha256, ...normalized }), 'utf8'))
|
|
190
|
+
return JSON.stringify({ ...normalized, configFileSha256, configSha256 })
|
|
191
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { spawnSync } from 'child_process'
|
|
1
|
+
import { spawn, spawnSync } from 'child_process'
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
3
|
+
import { tmpdir } from 'os'
|
|
2
4
|
import { join } from 'path'
|
|
3
5
|
import { fileURLToPath } from 'url'
|
|
4
6
|
|
|
@@ -20,6 +22,62 @@ export interface SkilledProposerLaunch {
|
|
|
20
22
|
environment: NodeJS.ProcessEnv
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
function materializeConfigPayload(engine: GepaEngine, environment: NodeJS.ProcessEnv): (() => void) | undefined {
|
|
26
|
+
const payload = environment.ORIZU_SKILLED_PROPOSER_CONFIG
|
|
27
|
+
if (engine !== 'official' || environment.ORIZU_CANDIDATE_PROPOSER !== 'skilled-proposer' || payload === undefined) return undefined
|
|
28
|
+
const root = mkdtempSync(join(tmpdir(), 'orizu-skilled-proposer-payload-'))
|
|
29
|
+
try {
|
|
30
|
+
const path = join(root, 'config.json')
|
|
31
|
+
writeFileSync(path, payload, { encoding: 'utf8', mode: 0o600 })
|
|
32
|
+
environment.ORIZU_SKILLED_PROPOSER_CONFIG = `@${path}`
|
|
33
|
+
} catch (error) {
|
|
34
|
+
rmSync(root, { recursive: true, force: true })
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
return () => rmSync(root, { recursive: true, force: true })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function spawnSkilledProposerChild(
|
|
41
|
+
command: string, args: string[], engine: GepaEngine, environment: NodeJS.ProcessEnv,
|
|
42
|
+
) {
|
|
43
|
+
const childEnvironment = { ...environment }
|
|
44
|
+
const shouldMaterialize = engine === 'official'
|
|
45
|
+
&& childEnvironment.ORIZU_CANDIDATE_PROPOSER === 'skilled-proposer'
|
|
46
|
+
&& childEnvironment.ORIZU_SKILLED_PROPOSER_CONFIG !== undefined
|
|
47
|
+
if (!shouldMaterialize) return spawnSync(command, args, { stdio: 'inherit', env: childEnvironment })
|
|
48
|
+
|
|
49
|
+
let child: ReturnType<typeof spawn> | undefined
|
|
50
|
+
let cleanup = () => {}
|
|
51
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'] as const
|
|
52
|
+
const handlers = signals.map(signal => ({
|
|
53
|
+
signal,
|
|
54
|
+
handle: () => {
|
|
55
|
+
try { cleanup() }
|
|
56
|
+
finally {
|
|
57
|
+
try { child?.kill(signal) }
|
|
58
|
+
finally {
|
|
59
|
+
for (const entry of handlers) process.off(entry.signal, entry.handle)
|
|
60
|
+
process.kill(process.pid, signal)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}))
|
|
65
|
+
for (const entry of handlers) process.once(entry.signal, entry.handle)
|
|
66
|
+
try {
|
|
67
|
+
cleanup = materializeConfigPayload(engine, childEnvironment) ?? cleanup
|
|
68
|
+
await new Promise<void>(resolve => setImmediate(resolve))
|
|
69
|
+
return await new Promise<{ status: number | null, error?: Error }>(resolve => {
|
|
70
|
+
let error: Error | undefined
|
|
71
|
+
child = spawn(command, args, { stdio: 'inherit', env: childEnvironment })
|
|
72
|
+
child.once('error', cause => { error = cause })
|
|
73
|
+
child.once('close', status => resolve(error ? { status, error } : { status }))
|
|
74
|
+
})
|
|
75
|
+
} finally {
|
|
76
|
+
for (const entry of handlers) process.off(entry.signal, entry.handle)
|
|
77
|
+
cleanup()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
23
81
|
function ensureSelectedSkilledProposerVenv(
|
|
24
82
|
python: string,
|
|
25
83
|
vendoredGepaPath: string,
|
|
@@ -219,6 +219,32 @@ def run_official_gepa(
|
|
|
219
219
|
owned_stop_callbacks = list(stop_callbacks)
|
|
220
220
|
if proposal_budget is not None:
|
|
221
221
|
owned_stop_callbacks.append(ProposalBudgetStopper(proposal_budget))
|
|
222
|
+
proposer_failures: list[Exception] = []
|
|
223
|
+
gepa_candidate_proposer = custom_candidate_proposer
|
|
224
|
+
if custom_candidate_proposer is not None:
|
|
225
|
+
delegate = custom_candidate_proposer
|
|
226
|
+
|
|
227
|
+
def capture_proposer_failure(
|
|
228
|
+
candidate: dict[str, str], reflective_dataset: Any, components_to_update: list[str],
|
|
229
|
+
) -> dict[str, str]:
|
|
230
|
+
if proposer_failures:
|
|
231
|
+
raise proposer_failures[0]
|
|
232
|
+
try:
|
|
233
|
+
return delegate(candidate, reflective_dataset, components_to_update)
|
|
234
|
+
except Exception as error:
|
|
235
|
+
if not proposer_failures:
|
|
236
|
+
proposer_failures.append(error)
|
|
237
|
+
raise
|
|
238
|
+
|
|
239
|
+
gepa_candidate_proposer = capture_proposer_failure
|
|
240
|
+
def raise_proposer_failure(_state: Any) -> bool:
|
|
241
|
+
if proposer_failures:
|
|
242
|
+
raise proposer_failures[0]
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
# Failure must win over every normal stopper in CompositeStopper(any),
|
|
246
|
+
# otherwise a simultaneous budget stop emits terminal success first.
|
|
247
|
+
owned_stop_callbacks.insert(0, raise_proposer_failure)
|
|
222
248
|
|
|
223
249
|
reflection_minibatch_size = config.minibatch_size
|
|
224
250
|
if custom_candidate_proposer is not None and max_metric_calls is not None:
|
|
@@ -252,11 +278,18 @@ def run_official_gepa(
|
|
|
252
278
|
# The connector implements GEPA's component-aware strategy protocol;
|
|
253
279
|
# passing it as a bare callable would discard the selected component.
|
|
254
280
|
kwargs["reflection_strategy"] = reflection_lm
|
|
255
|
-
if
|
|
256
|
-
kwargs["custom_candidate_proposer"] =
|
|
281
|
+
if gepa_candidate_proposer is not None:
|
|
282
|
+
kwargs["custom_candidate_proposer"] = gepa_candidate_proposer
|
|
257
283
|
kwargs["trainset"] = _with_row_identities(trainset)
|
|
258
284
|
kwargs["valset"] = _with_row_identities(valset)
|
|
259
|
-
|
|
285
|
+
try:
|
|
286
|
+
result = optimize(**kwargs)
|
|
287
|
+
except Exception:
|
|
288
|
+
if proposer_failures:
|
|
289
|
+
raise proposer_failures[0]
|
|
290
|
+
raise
|
|
291
|
+
if proposer_failures:
|
|
292
|
+
raise proposer_failures[0]
|
|
260
293
|
if custom_candidate_proposer is not None and preflight_evaluation is not None:
|
|
261
294
|
# GEPA's state correctly counts its seed evaluation, but this run
|
|
262
295
|
# served that evaluation from the already-accounted launch preflight.
|
|
@@ -18,10 +18,38 @@ def _inside(root: Path, candidate: Path) -> bool:
|
|
|
18
18
|
return False
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
if not
|
|
21
|
+
def _resolve_set_root(directory: str, reference: str) -> Path:
|
|
22
|
+
directory_root = Path(directory).resolve()
|
|
23
|
+
if not directory_root.is_dir():
|
|
24
24
|
raise InstructionSetLoaderError("instruction_set_not_synced")
|
|
25
|
+
direct = (directory_root / reference).resolve()
|
|
26
|
+
if _inside(directory_root, direct) and direct.is_dir():
|
|
27
|
+
try:
|
|
28
|
+
direct_manifest = json.loads((direct / "manifest.json").read_text())
|
|
29
|
+
except (OSError, json.JSONDecodeError):
|
|
30
|
+
# Preserve the exact-path loader's specific missing/invalid manifest error.
|
|
31
|
+
return direct
|
|
32
|
+
if direct_manifest.get("name") == reference or direct_manifest.get("slug") == reference:
|
|
33
|
+
return direct
|
|
34
|
+
matches: list[Path] = []
|
|
35
|
+
for child in directory_root.iterdir():
|
|
36
|
+
candidate = child.resolve()
|
|
37
|
+
if child.name.startswith(".") or not child.is_dir() or not _inside(directory_root, candidate):
|
|
38
|
+
continue
|
|
39
|
+
try:
|
|
40
|
+
manifest = json.loads((candidate / "manifest.json").read_text())
|
|
41
|
+
except (OSError, json.JSONDecodeError):
|
|
42
|
+
continue
|
|
43
|
+
if manifest.get("name") == reference or manifest.get("slug") == reference:
|
|
44
|
+
matches.append(candidate)
|
|
45
|
+
if len(matches) != 1:
|
|
46
|
+
code = "instruction_set_reference_ambiguous" if len(matches) > 1 else "instruction_set_not_synced"
|
|
47
|
+
raise InstructionSetLoaderError(code)
|
|
48
|
+
return matches[0]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_instruction_set(directory: str, name: str, model_config_identity: str) -> dict[str, str]:
|
|
52
|
+
root = _resolve_set_root(directory, name)
|
|
25
53
|
manifest_path = root / "manifest.json"
|
|
26
54
|
if not manifest_path.is_file():
|
|
27
55
|
raise InstructionSetLoaderError("instruction_set_manifest_missing")
|