fix(agent-pool): ban -fast model slugs
Never map or request fast=true. Slugs ending in -fast are stripped to the base (with a warn); model-selection and the pool plan document the ban.
This commit is contained in:
parent
629364f3e4
commit
18fe4f8e9b
4 changed files with 102 additions and 94 deletions
|
|
@ -17,7 +17,7 @@ Dois eixos, nesta ordem: **(1) classe da tarefa → família de modelo**, **(2)
|
|||
|
||||
| Prioridade | Família | Slug(s) no frontmatter `model:` | Quando |
|
||||
| ---------- | ------- | -------------------------------- | ------ |
|
||||
| 1 (default) | **Composer 2.5** | `composer-2.5` (evitar `-fast` salvo pedido explícito) | Features, chores simples, fixes localizados, docs leves — sempre que couber |
|
||||
| 1 (default) | **Composer 2.5** | `composer-2.5` | Features, chores simples, fixes localizados, docs leves — sempre que couber |
|
||||
| 2 | **Grok 4.5** | `cursor-grok-4.5-low` \| `cursor-grok-4.5-medium` \| `cursor-grok-4.5-high` | Precisa de deliberação além do Composer (abaixo) |
|
||||
| 3 | **Kimi K3 Low** | `kimi-k3-low` | Só na **fase de execução** de issues bipartidas (refactor grande / simplify / migrations+RBAC de blast radius alto), depois de um plano fechado |
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ Depois de escolher Grok, **obrigatório** pickar um effort. Escreva o slug compl
|
|||
| **Medium** | `cursor-grok-4.5-medium` | Cursor Grok 4.5 · Effort Medium | **Discovery / análise moderada**, critique/polish visual, harvest de padrões → guardrail, glossário a partir de evidência, chores de processo com trade-offs claros. Default quando Grok cabe e **não** é multi-domínio de alto risco. |
|
||||
| **High** | `cursor-grok-4.5-high` | Cursor Grok 4.5 · Effort High | **Multi-domínio ou design não óbvio** com custo de falha alto: fundações (auth/sessão/schema), Consent/LGPD, plan half de bipartição, arquitetura nova. |
|
||||
|
||||
Opcional: sufixo `-fast` (`cursor-grok-4.5-high-fast`) só sob pedido explícito — o pool mapeia para `fast=true` na API.
|
||||
**Proibido:** sufixo `-fast` em qualquer slug (`composer-2.5-fast`, `cursor-grok-4.5-high-fast`, …). O pool **não** envia `fast=true`; se a Issue trouxer `-fast`, o resolver remove o sufixo, resolve a base e emite warn.
|
||||
|
||||
**Heurística anti-viés:** se você ia marcar High por hábito, pergunte “a falha é cara **e** o desenho é aberto?”. Se só uma das duas → Medium (ou Low). Se nenhuma → Composer.
|
||||
|
||||
|
|
@ -71,14 +71,15 @@ Não despachar Kimi K3 Low sem plano fechado na Issue de plan.
|
|||
|
||||
1. **Composer 2.5 é o default.** Se a tarefa cabe nele, não suba de modelo.
|
||||
2. **Grok exige effort explícito** (`-low` / `-medium` / `-high` no slug). Não deixe `cursor-grok-4.5` sem sufixo; não use High como coringa.
|
||||
3. **Kimi K3 Low só na exec bipartida** (ou quando a Issue já declara `kimi-k3-low` após um plan).
|
||||
4. **Nunca economizar em:** migrations (história congelada), access control (`overrideAccess: false`), Consent/LGPD — se for uma Issue só disso e complexa, bipartir (plan Grok High → exec K3).
|
||||
5. **Usuário pediu modelo específico:** a escolha dele vence; avise uma vez se houver desperdício claro.
|
||||
3. **Nunca `-fast`.** Nem em Issue, nem em `Task.model`, nem sob pedido — recusar e usar o slug sem `-fast`.
|
||||
4. **Kimi K3 Low só na exec bipartida** (ou quando a Issue já declara `kimi-k3-low` após um plan).
|
||||
5. **Nunca economizar em:** migrations (história congelada), access control (`overrideAccess: false`), Consent/LGPD — se for uma Issue só disso e complexa, bipartir (plan Grok High → exec K3).
|
||||
6. **Usuário pediu modelo específico:** a escolha dele vence (exceto `-fast`, sempre recusado); avise uma vez se houver desperdício claro.
|
||||
|
||||
## Como aplicar
|
||||
|
||||
- **Frontmatter da Issue / pool:** use exatamente os slugs da tabela (incl. `-low`/`-medium`/`-high`).
|
||||
- **Subagentes (`Task.model`):** prefira o slug da Issue quando o enum do produto listar; hoje o enum costuma expor `composer-2.5`, `cursor-grok-4.5-high`, `cursor-grok-4.5-high-fast`, `kimi-k3-low`. Se a Issue é `-low`/`-medium` e o enum não tem o slug, use `inherit` (sessão já no effort certo) ou `cursor-grok-4.5-high` só se a subtarefa for a parte difícil — **não** “promova” a Issue inteira para High.
|
||||
- **Subagentes (`Task.model`):** prefira o slug da Issue quando o enum do produto listar (`composer-2.5`, `cursor-grok-4.5-high`, `kimi-k3-low`, …). **Nunca** escolha variantes `-fast` do enum. Se a Issue é `-low`/`-medium` e o enum não tem o slug, use `inherit` (sessão já no effort certo) ou `cursor-grok-4.5-high` só se a subtarefa for a parte difícil — **não** “promova” a Issue inteira para High.
|
||||
- **Sessão principal:** o agente não troca o próprio modelo. Se o atual não for o da tabela, diga em uma linha e siga.
|
||||
- **Pool:** `POOL_DEFAULT_MODEL_SLUG` = `composer-2.5`; `resolvePoolModel` mapeia os slugs canônicos → API.
|
||||
|
||||
|
|
|
|||
|
|
@ -220,8 +220,9 @@ Parâmetros (só relevantes ao backoff de retry de tick): `spawnRetryBackoff = p
|
|||
- **Namespace:** os slugs do repo (Task tool) e os da Cloud API são tabelas irmãs, não idênticas.
|
||||
O spike (Fase 0) despeja `GET /v1/models` e codifica o mapeamento explícito em
|
||||
`scripts/lib/agent-pool-models.mjs`: `Map<repoSlug, cloudModelId>` com entradas conhecidas
|
||||
(`composer-2.5` → id composer 2.5 da API, `composer-2.5-fast` → mesmo id + `params:[{id:'fast',value:'true'}]`,
|
||||
`cursor-grok-4.5-high`, `kimi-k3-low`, `gemini-3.1-pro`, …).
|
||||
(`composer-2.5` → id composer 2.5 da API,
|
||||
`cursor-grok-4.5-{low,medium,high}` → `grok-4.5` + `effort`,
|
||||
`kimi-k3-low` → `kimi-k3` + `reasoning=low`; **sem** variantes `-fast`).
|
||||
- **Fallback:** `model:` ausente, desconhecido ou rejeitado pela API → **`composer-2.5`**
|
||||
(pool "Cursor Models", custo incluído — regra 1 da model-selection). Warn no sumário do tick.
|
||||
- Validação: o tick consulta `GET /v1/models` uma vez por tick (cache em memória do processo)
|
||||
|
|
@ -319,8 +320,8 @@ ou deixar bloqueada.
|
|||
- Derivação de ativos (fixtures de Issues + runs): occupied / freed-by-done / waiting-auto-merge /
|
||||
failure-path / circuit breaker (2ª falha → blocked permanente).
|
||||
- Cálculo de `gap` e cap hard (nunca spawna além de `maxSlots` mesmo com fila longa).
|
||||
- Model mapping: slug válido → id Cloud; `-fast` → params; ausente/inválido → `composer-2.5`;
|
||||
slug fora do `GET /v1/models` → fallback + warn.
|
||||
- Model mapping: slug válido → id Cloud (+ effort/reasoning); `-fast` **proibido** (strip + warn);
|
||||
ausente/inválido → `composer-2.5`; slug fora do `GET /v1/models` → fallback + warn.
|
||||
- Prompt: contém `#N`, instrução de não-claim, `gh pr create --base stage`, `Closes #N`.
|
||||
- State: parse/serialize de variables; defaults.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,20 +6,20 @@
|
|||
* `model:`) and the Cloud API ids. The live table (`GET /v1/models`) is the
|
||||
* authority.
|
||||
*
|
||||
* Repo slugs (Issue frontmatter / pool / skills):
|
||||
* - `composer-2.5` / `composer-2.5-fast`
|
||||
* Repo slugs (Issue frontmatter / pool / skills) — **no `-fast`**:
|
||||
* - `composer-2.5`
|
||||
* - `cursor-grok-4.5-low` | `cursor-grok-4.5-medium` | `cursor-grok-4.5-high`
|
||||
* (+ optional `-fast`) → API `grok-4.5` + `effort` (+ `fast`)
|
||||
* → API `grok-4.5` + `effort`
|
||||
* - `kimi-k3-low` (also `kimi-k3-high` / `kimi-k3-max`) → API `kimi-k3` + `reasoning`
|
||||
*
|
||||
* Anything else falls back to composer-2.5 (pool "Cursor Models", included
|
||||
* cost — model-selection rule 1). The fallback never crashes a tick.
|
||||
* A slug ending in `-fast` is rejected (stripped + warn, then resolve the
|
||||
* base). Anything else unknown falls back to composer-2.5. The fallback
|
||||
* never crashes a tick.
|
||||
*/
|
||||
|
||||
export const POOL_DEFAULT_MODEL_SLUG = 'composer-2.5'
|
||||
|
||||
const FAST_SUFFIX = '-fast'
|
||||
const GROK_EFFORT_SLUG = /^cursor-grok-4\.5-(low|medium|high)(-fast)?$/
|
||||
const GROK_EFFORT_SLUG = /^cursor-grok-4\.5-(low|medium|high)$/
|
||||
const KIMI_REASONING_SLUG = /^kimi-k3-(low|high|max)$/
|
||||
|
||||
const findModel = (models, slug) =>
|
||||
|
|
@ -33,6 +33,47 @@ const paramSupportsValue = (model, paramId, value) => {
|
|||
return values.some((entry) => entry.value === value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a canonical (non-fast) slug against the live table.
|
||||
* @returns {{ model: { id: string, params?: Array<{ id: string, value: string }> }, ok: true } | { ok: false }}
|
||||
*/
|
||||
const resolveCanonical = (slug, apiModels) => {
|
||||
const grokMatch = GROK_EFFORT_SLUG.exec(slug)
|
||||
if (grokMatch) {
|
||||
const effort = grokMatch[1]
|
||||
const base =
|
||||
findModel(apiModels, 'grok-4.5') ??
|
||||
findModel(apiModels, 'cursor-grok-4.5') ??
|
||||
(apiModels.length === 0 ? { id: 'grok-4.5', parameters: [{ id: 'effort' }] } : null)
|
||||
if (base && paramSupportsValue(base, 'effort', effort)) {
|
||||
return {
|
||||
ok: true,
|
||||
model: { id: base.id, params: [{ id: 'effort', value: effort }] },
|
||||
}
|
||||
}
|
||||
return { ok: false }
|
||||
}
|
||||
|
||||
const kimiMatch = KIMI_REASONING_SLUG.exec(slug)
|
||||
if (kimiMatch) {
|
||||
const reasoning = kimiMatch[1]
|
||||
const base =
|
||||
findModel(apiModels, 'kimi-k3') ??
|
||||
(apiModels.length === 0 ? { id: 'kimi-k3', parameters: [{ id: 'reasoning' }] } : null)
|
||||
if (base && paramSupportsValue(base, 'reasoning', reasoning)) {
|
||||
return {
|
||||
ok: true,
|
||||
model: { id: base.id, params: [{ id: 'reasoning', value: reasoning }] },
|
||||
}
|
||||
}
|
||||
return { ok: false }
|
||||
}
|
||||
|
||||
const direct = findModel(apiModels, slug)
|
||||
if (direct) return { ok: true, model: { id: direct.id } }
|
||||
return { ok: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} issueModel frontmatter `model:` value
|
||||
* @param {Array<{ id: string, aliases?: string[], parameters?: Array<{ id: string, values?: Array<{ value: string }> }> }>} [apiModels] live /v1/models items
|
||||
|
|
@ -42,7 +83,7 @@ export const resolvePoolModel = (issueModel, apiModels = []) => {
|
|||
const requested =
|
||||
typeof issueModel === 'string' && issueModel.trim() !== '' ? issueModel.trim() : null
|
||||
|
||||
const fallback = () => {
|
||||
const fallback = (warn) => {
|
||||
const known = findModel(apiModels, POOL_DEFAULT_MODEL_SLUG)
|
||||
const tableNote = apiModels.length === 0 ? ' (tabela /v1/models indisponível)' : ''
|
||||
return {
|
||||
|
|
@ -51,7 +92,9 @@ export const resolvePoolModel = (issueModel, apiModels = []) => {
|
|||
usedFallback: requested !== null,
|
||||
...(requested
|
||||
? {
|
||||
warn: `modelo "${requested}" fora da tabela Cloud — fallback ${POOL_DEFAULT_MODEL_SLUG}${tableNote}`,
|
||||
warn:
|
||||
warn ??
|
||||
`modelo "${requested}" fora da tabela Cloud — fallback ${POOL_DEFAULT_MODEL_SLUG}${tableNote}`,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
|
|
@ -59,57 +102,27 @@ export const resolvePoolModel = (issueModel, apiModels = []) => {
|
|||
|
||||
if (!requested) return fallback()
|
||||
|
||||
const grokMatch = GROK_EFFORT_SLUG.exec(requested)
|
||||
if (grokMatch) {
|
||||
const effort = grokMatch[1]
|
||||
const wantFast = Boolean(grokMatch[2])
|
||||
const base =
|
||||
findModel(apiModels, 'grok-4.5') ??
|
||||
findModel(apiModels, 'cursor-grok-4.5') ??
|
||||
(apiModels.length === 0
|
||||
? { id: 'grok-4.5', parameters: [{ id: 'effort' }, { id: 'fast' }] }
|
||||
: null)
|
||||
if (
|
||||
base &&
|
||||
paramSupportsValue(base, 'effort', effort) &&
|
||||
(!wantFast || paramSupportsValue(base, 'fast', 'true'))
|
||||
) {
|
||||
/** @type {Array<{ id: string, value: string }>} */
|
||||
const params = [{ id: 'effort', value: effort }]
|
||||
if (wantFast) params.push({ id: 'fast', value: 'true' })
|
||||
return { model: { id: base.id, params }, requested, usedFallback: false }
|
||||
let slug = requested
|
||||
/** @type {string | undefined} */
|
||||
let fastWarn
|
||||
if (slug.endsWith('-fast')) {
|
||||
slug = slug.slice(0, -'-fast'.length)
|
||||
fastWarn = `sufixo -fast não é permitido (pedido "${requested}") — resolvendo como "${slug}"`
|
||||
}
|
||||
|
||||
const resolved = resolveCanonical(slug, apiModels)
|
||||
if (resolved.ok) {
|
||||
return {
|
||||
model: resolved.model,
|
||||
requested,
|
||||
usedFallback: false,
|
||||
...(fastWarn ? { warn: fastWarn } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
const kimiMatch = KIMI_REASONING_SLUG.exec(requested)
|
||||
if (kimiMatch) {
|
||||
const reasoning = kimiMatch[1]
|
||||
const base =
|
||||
findModel(apiModels, 'kimi-k3') ??
|
||||
(apiModels.length === 0 ? { id: 'kimi-k3', parameters: [{ id: 'reasoning' }] } : null)
|
||||
if (base && paramSupportsValue(base, 'reasoning', reasoning)) {
|
||||
return {
|
||||
model: { id: base.id, params: [{ id: 'reasoning', value: reasoning }] },
|
||||
requested,
|
||||
usedFallback: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requested.endsWith(FAST_SUFFIX)) {
|
||||
const base = findModel(apiModels, requested.slice(0, -FAST_SUFFIX.length))
|
||||
const supportsFast = paramSupportsValue(base ?? {}, 'fast', 'true')
|
||||
if (base && supportsFast) {
|
||||
return {
|
||||
model: { id: base.id, params: [{ id: 'fast', value: 'true' }] },
|
||||
requested,
|
||||
usedFallback: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const direct = findModel(apiModels, requested)
|
||||
if (direct) return { model: { id: direct.id }, requested, usedFallback: false }
|
||||
|
||||
return fallback()
|
||||
return fallback(
|
||||
fastWarn
|
||||
? `${fastWarn}; base também inválida — fallback ${POOL_DEFAULT_MODEL_SLUG}`
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,15 +41,6 @@ describe('resolvePoolModel', () => {
|
|||
expect(resolvePoolModel('composer-latest', API_MODELS).model).toEqual({ id: 'composer-2.5' })
|
||||
})
|
||||
|
||||
it('maps the repo -fast suffix to the fast param when the table supports it', () => {
|
||||
const result = resolvePoolModel('composer-2.5-fast', API_MODELS)
|
||||
expect(result.model).toEqual({
|
||||
id: 'composer-2.5',
|
||||
params: [{ id: 'fast', value: 'true' }],
|
||||
})
|
||||
expect(result.usedFallback).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['cursor-grok-4.5-low', 'low'],
|
||||
['cursor-grok-4.5-medium', 'medium'],
|
||||
|
|
@ -62,20 +53,6 @@ describe('resolvePoolModel', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('maps cursor-grok-4.5-high-fast → effort high + fast', () => {
|
||||
expect(resolvePoolModel('cursor-grok-4.5-high-fast', API_MODELS)).toEqual({
|
||||
model: {
|
||||
id: 'grok-4.5',
|
||||
params: [
|
||||
{ id: 'effort', value: 'high' },
|
||||
{ id: 'fast', value: 'true' },
|
||||
],
|
||||
},
|
||||
requested: 'cursor-grok-4.5-high-fast',
|
||||
usedFallback: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('maps kimi-k3-low → kimi-k3 reasoning=low', () => {
|
||||
expect(resolvePoolModel('kimi-k3-low', API_MODELS)).toEqual({
|
||||
model: { id: 'kimi-k3', params: [{ id: 'reasoning', value: 'low' }] },
|
||||
|
|
@ -84,10 +61,26 @@ describe('resolvePoolModel', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('falls back when a -fast slug base lacks the fast param', () => {
|
||||
const result = resolvePoolModel('kimi-k3-fast', API_MODELS)
|
||||
it('rejects -fast: strips suffix, resolves base, warns', () => {
|
||||
const composerFast = resolvePoolModel('composer-2.5-fast', API_MODELS)
|
||||
expect(composerFast.model).toEqual({ id: 'composer-2.5' })
|
||||
expect(composerFast.usedFallback).toBe(false)
|
||||
expect(composerFast.warn).toContain('-fast')
|
||||
|
||||
const grokFast = resolvePoolModel('cursor-grok-4.5-high-fast', API_MODELS)
|
||||
expect(grokFast.model).toEqual({
|
||||
id: 'grok-4.5',
|
||||
params: [{ id: 'effort', value: 'high' }],
|
||||
})
|
||||
expect(grokFast.warn).toContain('-fast')
|
||||
expect(grokFast.model.params?.some((param) => param.id === 'fast')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('falls back when -fast base is also unknown', () => {
|
||||
const result = resolvePoolModel('modelo-inventado-fast', API_MODELS)
|
||||
expect(result.model).toEqual({ id: POOL_DEFAULT_MODEL_SLUG })
|
||||
expect(result.usedFallback).toBe(true)
|
||||
expect(result.warn).toContain('-fast')
|
||||
})
|
||||
|
||||
it('falls back to composer-2.5 with a warn for unknown slugs', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue