feat(codex-bridge): decision-model stages, Dify rules and beta.39
Owner request, 14 October 2026: use the decision model (Jev / Celeris) at several stages, let Dify supply the rules, code it without new bugs. Every new stage is switched on in Settings > Kademeli Gecis and starts disabled, so the upgrade changes no behaviour by itself. - Settings schema 2 (Python and TypeScript validate the same rules); a schema 1 file is filled from the defaults. Default chain is now Katman-1 -> Katman-2 -> Katman-3 with Katman-3, Chat-Model and bekci-sesli-bekci always passing (owner decision of the same day). - watchModels: the Bekci correction channel follows a settings list instead of the hard-coded Sesli-Hizli-Zeki. - prefilter: a predicted refusal starts the step at the chosen layer. - autoRoute: virtual "Otomatik" model; one decision per user turn picks the combo, tool steps of the same turn keep it. - finishCheck: an answer that stops early goes back to the same model once. - A layer whose window cannot hold the request is skipped. - policy: stage rules from a Dify workflow (karar_politikasi.py), fetched in the background, validated, last good copy kept; turns never wait. - karar_sor.py: verifierCheck for the dynamic-workflow acceptance step. - Bridge version 19, IDE 1.0.0-beta.39. Tests: 299 bridge tests, 36 electron tests; live run on a side bridge (port 8797) with Celeris and the AITURK gateway for every stage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f53b090f7
commit
7c4ef18df0
18 files changed
+1672
-119
No files matched your search
@@ -44,13 +44,13 @@ describe('staged transition settings UI', () => {
|
||||
show()
|
||||
await screen.findByText('Kademeli Geçiş')
|
||||
fireEvent.click(screen.getByText('+ Geçiş kuralı ekle'))
|
||||
expect(screen.getByLabelText('İtiraz edilen model 4')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Kullanılacak model 4')).toBeTruthy()
|
||||
fireEvent.click(screen.getByLabelText('Geçiş kuralı 4 sil'))
|
||||
expect(screen.queryByLabelText('İtiraz edilen model 4')).toBeNull()
|
||||
expect(screen.getByLabelText('İtiraz edilen model 3')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Kullanılacak model 3')).toBeTruthy()
|
||||
fireEvent.click(screen.getByLabelText('Geçiş kuralı 3 sil'))
|
||||
expect(screen.queryByLabelText('İtiraz edilen model 3')).toBeNull()
|
||||
fireEvent.click(screen.getByText('+ Her zaman geç kuralı ekle'))
|
||||
expect(screen.getByLabelText('Her zaman geçen model 3')).toBeTruthy()
|
||||
fireEvent.click(screen.getByLabelText('Her zaman geç kuralı 3 sil'))
|
||||
expect(screen.getByLabelText('Her zaman geçen model 4')).toBeTruthy()
|
||||
fireEvent.click(screen.getByLabelText('Her zaman geç kuralı 4 sil'))
|
||||
fireEvent.change(screen.getByLabelText('İsteği yerine getirmeme denetim metni'), {
|
||||
target: { value: 'custom prompt' }
|
||||
})
|
||||
@@ -58,6 +58,37 @@ describe('staged transition settings UI', () => {
|
||||
await screen.findByText('Kaydedildi. Yeni değerlendirmelerde uygulanır.')
|
||||
expect(set.mock.calls[0][0].settings.questions.ret).toBe('custom prompt')
|
||||
expect(set.mock.calls[0][0]).not.toHaveProperty('apiKey')
|
||||
expect(set.mock.calls[0][0]).not.toHaveProperty('policyApiKey')
|
||||
})
|
||||
it('edits the decision stages, the correction channel and the Dify rules source', async () => {
|
||||
get.mockResolvedValue({
|
||||
...state(),
|
||||
policyKeyConfigured: false,
|
||||
policyStatus: { ok: true, fetchedAt: 'dün', sections: ['autoRoute'], error: 'Dify kapalı' }
|
||||
})
|
||||
show()
|
||||
await screen.findByText('Karar aşamaları')
|
||||
fireEvent.click(screen.getByLabelText('Ret ön kontrolü'))
|
||||
fireEvent.change(screen.getByLabelText('Ret ön kontrolü eşiği'), { target: { value: '0.75' } })
|
||||
fireEvent.click(screen.getByLabelText('Otomatik model'))
|
||||
fireEvent.change(screen.getByLabelText('Zor iş sorusu'), { target: { value: 'zor mu?' } })
|
||||
fireEvent.click(screen.getByLabelText('Erken bitiş denetimi'))
|
||||
fireEvent.change(screen.getByLabelText('Modele verilecek not'), { target: { value: 'işi bitir' } })
|
||||
fireEvent.click(screen.getByLabelText('Workflow doğrulayıcı elemesi'))
|
||||
fireEvent.click(screen.getByText('+ Düzeltme kanalı modeli ekle'))
|
||||
expect(screen.getByLabelText('Düzeltme kanalı modeli 4')).toBeTruthy()
|
||||
fireEvent.click(screen.getByLabelText('Düzeltme kanalı modeli 4 sil'))
|
||||
fireEvent.change(screen.getByLabelText('Dify uygulama anahtarı'), { target: { value: 'app-key' } })
|
||||
fireEvent.click(screen.getByText('Kaydet'))
|
||||
await screen.findByText('Kaydedildi. Yeni değerlendirmelerde uygulanır.')
|
||||
const saved = set.mock.calls[0][0]
|
||||
expect(saved.settings.prefilter).toMatchObject({ enabled: true, threshold: 0.75 })
|
||||
expect(saved.settings.autoRoute).toMatchObject({ enabled: true, questions: { zor: 'zor mu?' } })
|
||||
expect(saved.settings.finishCheck).toMatchObject({ enabled: true, note: 'işi bitir' })
|
||||
expect(saved.settings.verifierCheck.enabled).toBe(true)
|
||||
expect(saved.settings.watchModels).toEqual(defaults.watchModels)
|
||||
expect(saved.policyApiKey).toBe('app-key')
|
||||
expect((screen.getByLabelText('Dify uygulama anahtarı') as HTMLInputElement).value).toBe('')
|
||||
})
|
||||
it('retains user edits if persistence fails', async () => {
|
||||
set.mockRejectedValue(new Error('disk error'))
|
||||
|
||||
@@ -21,6 +21,7 @@ export function StagedTransitionSettingsView() {
|
||||
const [state, setState] = useState<StagedTransitionState | null>(null)
|
||||
const [draft, setDraft] = useState<StagedTransitionSettings | null>(null)
|
||||
const [key, setKey] = useState('')
|
||||
const [policyKey, setPolicyKey] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [error, setError] = useState(false)
|
||||
@@ -80,7 +81,10 @@ export function StagedTransitionSettingsView() {
|
||||
const storedModels = [
|
||||
draft.fallbackModel,
|
||||
...draft.exemptModels,
|
||||
...draft.rejectionRules.flatMap(rule => [rule.from, rule.to])
|
||||
...draft.rejectionRules.flatMap(rule => [rule.from, rule.to]),
|
||||
...draft.watchModels,
|
||||
draft.prefilter.target,
|
||||
...Object.values(draft.autoRoute.targets)
|
||||
]
|
||||
|
||||
for (const id of storedModels) {
|
||||
@@ -127,14 +131,62 @@ export function StagedTransitionSettingsView() {
|
||||
/>
|
||||
)
|
||||
|
||||
// Decision stages (14 Oct 2026). Each stage has its own switch; all start disabled.
|
||||
type StageName = 'prefilter' | 'autoRoute' | 'finishCheck' | 'verifierCheck'
|
||||
|
||||
const stage = <K extends StageName>(name: K, value: Partial<StagedTransitionSettings[K]>) =>
|
||||
change(name, { ...draft[name], ...value } as StagedTransitionSettings[K])
|
||||
|
||||
const probabilityInput = (label: string, value: number, onChange: (value: number) => void) => (
|
||||
<Input
|
||||
aria-label={label}
|
||||
max={1}
|
||||
min={0}
|
||||
onChange={e => onChange(Number(e.target.value))}
|
||||
step={0.01}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
|
||||
const secondsInput = (label: string, value: number, max: number, onChange: (value: number) => void) => (
|
||||
<Input aria-label={label} max={max} min={1} onChange={e => onChange(Number(e.target.value))} type="number" value={value} />
|
||||
)
|
||||
|
||||
const question = (label: string, value: string, onChange: (value: string) => void, rows = 3) => (
|
||||
<label className="block py-2">
|
||||
<span>{label}</span>
|
||||
<Textarea aria-label={label} onChange={e => onChange(e.target.value)} rows={rows} value={value} />
|
||||
</label>
|
||||
)
|
||||
|
||||
const stageSwitch = (name: StageName, label: string, description: string) => (
|
||||
<ListRow
|
||||
action={
|
||||
<Switch aria-label={label} checked={draft[name].enabled} onCheckedChange={value => stage(name, { enabled: value })} />
|
||||
}
|
||||
description={description}
|
||||
title={label}
|
||||
/>
|
||||
)
|
||||
|
||||
const policy = state?.policyStatus
|
||||
|
||||
const save = async () => {
|
||||
setBusy(true)
|
||||
setMessage('')
|
||||
setError(false)
|
||||
|
||||
try {
|
||||
adopt(await api.setStagedTransition({ settings: draft, ...(key.trim() ? { apiKey: key.trim() } : {}) }))
|
||||
adopt(
|
||||
await api.setStagedTransition({
|
||||
settings: draft,
|
||||
...(key.trim() ? { apiKey: key.trim() } : {}),
|
||||
...(policyKey.trim() ? { policyApiKey: policyKey.trim() } : {})
|
||||
})
|
||||
)
|
||||
setKey('')
|
||||
setPolicyKey('')
|
||||
setMessage(text('Kaydedildi. Yeni değerlendirmelerde uygulanır.', 'Saved. Applies to new evaluations.'))
|
||||
} catch (failure) {
|
||||
setError(true)
|
||||
@@ -404,6 +456,248 @@ export function StagedTransitionSettingsView() {
|
||||
{text('+ Her zaman geç kuralı ekle', '+ Add always-pass rule')}
|
||||
</Button>
|
||||
</SettingsSection>
|
||||
<SettingsSection icon={Link} title={text('Karar aşamaları', 'Decision stages')}>
|
||||
<p>
|
||||
{text(
|
||||
'Karar modeli metin yazmaz; her aşamada evet/hayır olasılığı verir. Aşamalar yukarıdaki ana anahtar açıkken çalışır. Karar alınamazsa bugünkü yol izlenir.',
|
||||
'The decision model writes no text; each stage gets a yes/no probability. Stages run while the main switch above is on. Without a decision the current path is kept.'
|
||||
)}
|
||||
</p>
|
||||
{stageSwitch(
|
||||
'prefilter',
|
||||
text('Ret ön kontrolü', 'Refusal pre-check'),
|
||||
text(
|
||||
'Tur başında sorulur: normal bir model bu isteği reddeder ya da yumuşatır mı? Evetse adım beklemeden seçilen katmandan başlar.',
|
||||
'Asked at the start of a turn: would a mainstream model refuse or water down this request? If so the step starts at the chosen layer.'
|
||||
)
|
||||
)}
|
||||
<ListRow
|
||||
action={modelSelect(text('Ret ön kontrolü hedefi', 'Refusal pre-check target'), draft.prefilter.target, value =>
|
||||
stage('prefilter', { target: value })
|
||||
)}
|
||||
title={text('Reddedilecekse başlanacak model', 'Model to start with when a refusal is expected')}
|
||||
/>
|
||||
<ListRow
|
||||
action={probabilityInput(text('Ret ön kontrolü eşiği', 'Refusal pre-check threshold'), draft.prefilter.threshold, value =>
|
||||
stage('prefilter', { threshold: value })
|
||||
)}
|
||||
title={text('Eşik (0–1)', 'Threshold (0–1)')}
|
||||
/>
|
||||
{question(text('Ret ön kontrolü sorusu', 'Refusal pre-check question'), draft.prefilter.question, value =>
|
||||
stage('prefilter', { question: value })
|
||||
)}
|
||||
{stageSwitch(
|
||||
'autoRoute',
|
||||
text('Otomatik model', 'Automatic model'),
|
||||
text(
|
||||
'Codex listesinde bir model olarak görünür. Seçildiğinde her yeni istekte karar modeli işin türüne göre combo seçer; aynı turun araç adımları aynı combo ile sürer. Açınca listede birkaç dakika içinde görünür.',
|
||||
'Appears as a model in the Codex list. When selected, the decision model picks a combo for each new request; tool steps of the same turn keep it. It appears in the list within a few minutes after enabling.'
|
||||
)
|
||||
)}
|
||||
<ListRow
|
||||
action={
|
||||
<Input
|
||||
aria-label={text('Otomatik modelin adı', 'Automatic model name')}
|
||||
onChange={e => stage('autoRoute', { model: e.target.value })}
|
||||
value={draft.autoRoute.model}
|
||||
/>
|
||||
}
|
||||
title={text('Listede görünecek ad', 'Name in the model list')}
|
||||
/>
|
||||
{(['basit', 'zor', 'arastirma'] as const).map(kind => {
|
||||
const label =
|
||||
kind === 'basit'
|
||||
? text('Basit iş combo', 'Simple request combo')
|
||||
: kind === 'zor'
|
||||
? text('Zor iş combo', 'Hard request combo')
|
||||
: text('Araştırma combo', 'Research combo')
|
||||
|
||||
return (
|
||||
<ListRow
|
||||
action={modelSelect(label, draft.autoRoute.targets[kind], value =>
|
||||
stage('autoRoute', { targets: { ...draft.autoRoute.targets, [kind]: value } })
|
||||
)}
|
||||
key={kind}
|
||||
title={label}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{(['zor', 'arastirma'] as const).map(kind => {
|
||||
const label = kind === 'zor' ? text('Zor iş sorusu', 'Hard request question') : text('Araştırma sorusu', 'Research question')
|
||||
|
||||
return (
|
||||
<div key={kind}>
|
||||
<ListRow
|
||||
action={probabilityInput(`${label} ${text('eşiği', 'threshold')}`, draft.autoRoute.thresholds[kind], value =>
|
||||
stage('autoRoute', { thresholds: { ...draft.autoRoute.thresholds, [kind]: value } })
|
||||
)}
|
||||
title={`${label} ${text('eşiği', 'threshold')}`}
|
||||
/>
|
||||
{question(label, draft.autoRoute.questions[kind], value =>
|
||||
stage('autoRoute', { questions: { ...draft.autoRoute.questions, [kind]: value } })
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{stageSwitch(
|
||||
'finishCheck',
|
||||
text('Erken bitiş denetimi', 'Early finish check'),
|
||||
text(
|
||||
'Son cevapta sorulur: iş bitmeden “bitti” dendi mi ya da kanıt gösterilmedi mi? Evetse cevap aşağıdaki notla aynı modele bir kez geri verilir.',
|
||||
'Asked on the final answer: did it say done before finishing or claim success without evidence? If so it goes back to the same model once with the note below.'
|
||||
)
|
||||
)}
|
||||
<ListRow
|
||||
action={probabilityInput(text('Erken bitiş eşiği', 'Early finish threshold'), draft.finishCheck.threshold, value =>
|
||||
stage('finishCheck', { threshold: value })
|
||||
)}
|
||||
title={text('Eşik (0–1)', 'Threshold (0–1)')}
|
||||
/>
|
||||
{question(text('Erken bitiş sorusu', 'Early finish question'), draft.finishCheck.question, value =>
|
||||
stage('finishCheck', { question: value })
|
||||
)}
|
||||
{question(
|
||||
text('Modele verilecek not', 'Note given to the model'),
|
||||
draft.finishCheck.note,
|
||||
value => stage('finishCheck', { note: value }),
|
||||
2
|
||||
)}
|
||||
{stageSwitch(
|
||||
'verifierCheck',
|
||||
text('Workflow doğrulayıcı elemesi', 'Workflow reviewer check'),
|
||||
text(
|
||||
'Dinamik workflow kabulünde sorulur: doğrulayıcı somut kanıt göstermeden mi onayladı? Evetse kabul engellenir, doğrulama yeniden yapılır.',
|
||||
'Asked when a dynamic workflow is accepted: did the reviewer approve without concrete evidence? If so acceptance is blocked and verification is redone.'
|
||||
)
|
||||
)}
|
||||
<ListRow
|
||||
action={probabilityInput(text('Doğrulayıcı elemesi eşiği', 'Reviewer check threshold'), draft.verifierCheck.threshold, value =>
|
||||
stage('verifierCheck', { threshold: value })
|
||||
)}
|
||||
title={text('Eşik (0–1)', 'Threshold (0–1)')}
|
||||
/>
|
||||
{question(text('Doğrulayıcı elemesi sorusu', 'Reviewer check question'), draft.verifierCheck.question, value =>
|
||||
stage('verifierCheck', { question: value })
|
||||
)}
|
||||
<ListRow
|
||||
action={secondsInput(text('Tur başı karar bekleme (saniye)', 'Turn-start decision wait (seconds)'), draft.prefilter.timeoutSeconds, 60, value => {
|
||||
setDraft({
|
||||
...draft,
|
||||
prefilter: { ...draft.prefilter, timeoutSeconds: value },
|
||||
autoRoute: { ...draft.autoRoute, timeoutSeconds: value }
|
||||
})
|
||||
})}
|
||||
description={text(
|
||||
'Bu sürede karar gelmezse istek bugünkü yoldan devam eder.',
|
||||
'Without a decision within this time the request continues on the current path.'
|
||||
)}
|
||||
title={text('Tur başı karar bekleme (saniye)', 'Turn-start decision wait (seconds)')}
|
||||
/>
|
||||
</SettingsSection>
|
||||
<SettingsSection icon={Link} title={text('Cevaptan sonra düzeltme kanalı', 'Correction channel after answers')}>
|
||||
<p>
|
||||
{text(
|
||||
'Bekçi bu modellerin cevabından sonra düzeltme ya da ek bilgi gönderebilir. Ad tam eşleşmelidir.',
|
||||
'Bekçi may send corrections or extra information after these models answer. Names must match exactly.'
|
||||
)}
|
||||
</p>
|
||||
{draft.watchModels.map((model, index) => (
|
||||
<div className="flex flex-wrap items-center gap-2 py-2" key={index}>
|
||||
{modelSelect(text(`Düzeltme kanalı modeli ${index + 1}`, `Correction channel model ${index + 1}`), model, value =>
|
||||
change(
|
||||
'watchModels',
|
||||
draft.watchModels.map((m, i) => (i === index ? value : m))
|
||||
)
|
||||
)}
|
||||
<Button
|
||||
aria-label={text(`Düzeltme kanalı modeli ${index + 1} sil`, `Remove correction channel model ${index + 1}`)}
|
||||
onClick={() =>
|
||||
change(
|
||||
'watchModels',
|
||||
draft.watchModels.filter((_, i) => i !== index)
|
||||
)
|
||||
}
|
||||
variant="ghost"
|
||||
>
|
||||
{text('Sil', 'Remove')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button onClick={() => change('watchModels', [...draft.watchModels, ''])} variant="outline">
|
||||
{text('+ Düzeltme kanalı modeli ekle', '+ Add correction channel model')}
|
||||
</Button>
|
||||
</SettingsSection>
|
||||
<SettingsSection icon={Link} title={text('Kural kaynağı', 'Rules source')}>
|
||||
<p>
|
||||
{text(
|
||||
'Dify seçilirse karar aşamalarının soruları, eşikleri ve combo seçimleri Dify’daki iş akışından birkaç dakikada bir alınır. Codex’in konuştuğu model yine OmniRoute combo’sudur. Dify’a ulaşılamazsa son geçerli kurallar, o da yoksa bu sayfadaki ayarlar kullanılır.',
|
||||
'With Dify, the stage questions, thresholds and combo choices are fetched from the Dify workflow every few minutes. Codex still talks to OmniRoute combos. If Dify is unreachable the last good rules, otherwise the settings on this page, are used.'
|
||||
)}
|
||||
</p>
|
||||
<ListRow
|
||||
action={
|
||||
<Select
|
||||
onValueChange={value => change('policy', { ...draft.policy, source: value as 'local' | 'dify' })}
|
||||
value={draft.policy.source}
|
||||
>
|
||||
<SelectTrigger aria-label={text('Kural kaynağı', 'Rules source')}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="local">{text('Bu sayfa', 'This page')}</SelectItem>
|
||||
<SelectItem value="dify">Dify</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
title={text('Kural kaynağı', 'Rules source')}
|
||||
/>
|
||||
<ListRow
|
||||
action={
|
||||
<Input
|
||||
aria-label={text('Dify API adresi', 'Dify API endpoint')}
|
||||
onChange={e => change('policy', { ...draft.policy, endpoint: e.target.value })}
|
||||
type="url"
|
||||
value={draft.policy.endpoint}
|
||||
/>
|
||||
}
|
||||
title={text('Dify API adresi', 'Dify API endpoint')}
|
||||
/>
|
||||
<ListRow
|
||||
action={
|
||||
<Input
|
||||
aria-label={text('Dify uygulama anahtarı', 'Dify app key')}
|
||||
autoComplete="new-password"
|
||||
onChange={e => setPolicyKey(e.target.value)}
|
||||
type="password"
|
||||
value={policyKey}
|
||||
/>
|
||||
}
|
||||
description={
|
||||
state?.policyKeyConfigured
|
||||
? text('Anahtar tanımlı. Değiştirmek için yeni anahtarı girin.', 'Key configured. Enter a new key to replace it.')
|
||||
: text('Henüz anahtar tanımlı değil.', 'No key configured.')
|
||||
}
|
||||
title={text('Dify uygulama anahtarı', 'Dify app key')}
|
||||
/>
|
||||
<ListRow
|
||||
action={secondsInput(text('Yenileme aralığı (dakika)', 'Refresh interval (minutes)'), draft.policy.refreshMinutes, 1440, value =>
|
||||
change('policy', { ...draft.policy, refreshMinutes: value })
|
||||
)}
|
||||
title={text('Yenileme aralığı (dakika)', 'Refresh interval (minutes)')}
|
||||
/>
|
||||
{draft.policy.source === 'dify' && (
|
||||
<p role="status">
|
||||
{!policy
|
||||
? text('Dify kuralları henüz alınmadı.', 'Dify rules not fetched yet.')
|
||||
: policy.ok
|
||||
? text(
|
||||
`Son geçerli kurallar: ${policy.lastGoodAt ?? policy.fetchedAt ?? '-'} (${policy.sections.join(', ') || 'boş'})${policy.error ? ` · son deneme hatası: ${policy.error}` : ''}`,
|
||||
`Last good rules: ${policy.lastGoodAt ?? policy.fetchedAt ?? '-'} (${policy.sections.join(', ') || 'empty'})${policy.error ? ` · last attempt failed: ${policy.error}` : ''}`
|
||||
)
|
||||
: text(`Kurallar alınamadı: ${policy.error ?? '-'}`, `Rules could not be fetched: ${policy.error ?? '-'}`)}
|
||||
</p>
|
||||
)}
|
||||
</SettingsSection>
|
||||
<SettingsSection icon={Link} title={text('Paralellik ve bekleme', 'Concurrency and waiting')}>
|
||||
{number(
|
||||
'maxConcurrent',
|
||||
|
||||
Reference in new issue
Block a user