feat(agents): pin explicit reasoning effort on every agent and workflow call
Effort joins model as a mandatory pin: an omitted field inherits the session effort, coupling every dispatch's thinking budget to whatever the user happens to be chatting at (often xhigh) — the same session-state coupling the model pin removes. The assignment follows the model split: - xhigh on every opus agent (judgement roles are the pipeline's quality floor and must not degrade with the session); - high on every sonnet agent (tightly-scoped plan execution gains little from xhigh but pays its latency per dispatch, and these are the per-task in-loop roles — wall-clock is the efficiency metric; not lower than high, since re-loops cost more than saved thinking); - medium inline in the workflow scripts for schema-bound extraction/verification stages that author no code (preflight, plan-extract, mini-verify, tree-check, finalize, build/suite verify). Workflow agent() calls pass effort explicitly on every call — whether frontmatter effort propagates through an agentType dispatch is undocumented, so the scripts do not rely on it. Policy documented in docs/agent-template.md § effort, mirroring § model.
This commit is contained in:
@@ -47,6 +47,11 @@
|
||||
// fable — banned for all plugin agents and workflows). Mechanical and
|
||||
// in-loop steps run sonnet; the quality gate is the one deliberate opus
|
||||
// call (last correctness check before a task is marked DONE).
|
||||
// • Effort policy mirrors the model policy: every agent() call pins an
|
||||
// explicit effort — never the session inherit. Code-writing and review
|
||||
// steps run high, the opus quality gate runs xhigh, and schema-bound
|
||||
// extraction/check steps (preflight, plan-extract, mini-verify,
|
||||
// tree-check, finalize) run medium. See docs/agent-template.md § effort.
|
||||
//
|
||||
// Carrier — passed as the Workflow `args` object by the orchestrator:
|
||||
// mode: 'standard' | 'mini'
|
||||
@@ -250,7 +255,7 @@ const pre = await agent(
|
||||
'Run `git status --porcelain`, `git rev-parse HEAD`, and `git rev-parse --abbrev-ref HEAD`. ' +
|
||||
'Report whether the tree is clean (no porcelain output), the HEAD sha, the branch, and any dirty paths. ' +
|
||||
'Do NOT modify anything, do NOT commit. The HEAD sha is an informational anchor only — never a reset target.',
|
||||
{ model: 'sonnet', label: 'preflight', phase: 'Preflight', schema: PREFLIGHT_SCHEMA },
|
||||
{ model: 'sonnet', effort: 'medium', label: 'preflight', phase: 'Preflight', schema: PREFLIGHT_SCHEMA },
|
||||
)
|
||||
if (!pre) return { status: 'BLOCKED', iter_id, reason: 'infra: preflight agent did not return' }
|
||||
if (!pre.clean) {
|
||||
@@ -281,7 +286,7 @@ if (mode === 'mini') {
|
||||
`${STANDING}\n\nRead the plan at ${plan_path}. Extract ` +
|
||||
(task_range ? `tasks ${task_range[0]}..${task_range[1]} (inclusive, 1-based)` : 'every task') +
|
||||
', each as its VERBATIM block with the task id and a one-line title. Do not summarise or reorder. Do not edit anything.',
|
||||
{ model: 'sonnet', label: 'plan-extract', phase: 'Per-task loop', schema: TASKS_SCHEMA },
|
||||
{ model: 'sonnet', effort: 'medium', label: 'plan-extract', phase: 'Per-task loop', schema: TASKS_SCHEMA },
|
||||
)
|
||||
if (!extracted || !extracted.tasks || extracted.tasks.length === 0) {
|
||||
return { status: 'NEEDS_CONTEXT', iter_id, reason: `no tasks extracted from ${plan_path}` }
|
||||
@@ -299,7 +304,7 @@ async function runTask(task) {
|
||||
'inner-loop discipline — add the failing test inline even if the task text forgot to script it). ' +
|
||||
'Build and test must be green before you report DONE. Leave all edits UNSTAGED; never commit.\n\n' +
|
||||
`mode: ${mode}\niter_id: ${iter_id}\n\nTASK ${task.id} — ${task.title}\n${task.text}`,
|
||||
{ agentType: 'implementer', model: 'sonnet', label: `impl:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
||||
{ agentType: 'implementer', model: 'sonnet', effort: 'high', label: `impl:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
||||
)
|
||||
if (!impl) return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'implementer agent did not return' }
|
||||
if (impl.status === 'BLOCKED') return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: impl.reason || 'worker-blocked' }
|
||||
@@ -318,7 +323,7 @@ async function runTask(task) {
|
||||
`${STANDING}\n\nSpec-compliance review of task ${task.id}. Compare \`git diff HEAD\` against the task ` +
|
||||
'text ONLY — list missing requirements and unrequested extras. No quality opinions, no fix proposals. ' +
|
||||
`Focus on the files this task claims to touch.\n\nTASK ${task.id} — ${task.title}\n${task.text}`,
|
||||
{ agentType: 'spec-reviewer', model: 'sonnet', label: `spec:${task.id}`, phase: 'Per-task loop', schema: SPEC_SCHEMA },
|
||||
{ agentType: 'spec-reviewer', model: 'sonnet', effort: 'high', label: `spec:${task.id}`, phase: 'Per-task loop', schema: SPEC_SCHEMA },
|
||||
)
|
||||
if (spec && spec.status === 'compliant') break
|
||||
if (spec && spec.status === 'unclear') return { id: task.id, title: task.title, outcome: 'BLOCKED', reason: 'spec-ambiguous', detail: spec.ambiguity }
|
||||
@@ -328,7 +333,7 @@ async function runTask(task) {
|
||||
`${STANDING}\n\nRepair task ${task.id} to satisfy the spec-compliance review. Address EACH finding; ` +
|
||||
'do not widen scope. Keep build + tests green; leave edits unstaged.\n\n' +
|
||||
`TASK ${task.id} — ${task.title}\n${task.text}\n\nFINDINGS:\n- ${(spec?.findings || ['(none reported)']).join('\n- ')}`,
|
||||
{ agentType: 'implementer', model: 'sonnet', label: `impl-fix-spec:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
||||
{ agentType: 'implementer', model: 'sonnet', effort: 'high', label: `impl-fix-spec:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
||||
)
|
||||
if (specFix && specFix.applied_changes === true) wroteAnyFile = true
|
||||
}
|
||||
@@ -353,7 +358,7 @@ async function runTask(task) {
|
||||
'The loop compares it across rounds to detect a no-op/cyclic repair.',
|
||||
// opus by design: the loop's last correctness gate (spec-reviewer only checks
|
||||
// task-text correspondence) — real-bug finding is the documented opus strength.
|
||||
{ agentType: 'quality-reviewer', model: 'opus', label: `qual:${task.id}`, phase: 'Per-task loop', schema: QUAL_SCHEMA },
|
||||
{ agentType: 'quality-reviewer', model: 'opus', effort: 'xhigh', label: `qual:${task.id}`, phase: 'Per-task loop', schema: QUAL_SCHEMA },
|
||||
)
|
||||
if (qual && qual.status === 'approved') break
|
||||
// No-op / cyclic-repair backstop: a changes_requested verdict over a diff-state already reviewed-and-flagged
|
||||
@@ -387,7 +392,7 @@ async function runTask(task) {
|
||||
'A finding whose only remedy is to diverge from the ratified plan must be held or escalated, never chased ' +
|
||||
'into a deviation the next review flags.\n\n' +
|
||||
`TASK ${task.id} — ${task.title}\n${task.text}\n\nISSUES:\n- ` + (qual?.issues || ['(none reported)']).join('\n- '),
|
||||
{ agentType: 'implementer', model: 'sonnet', label: `impl-fix-qual:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
||||
{ agentType: 'implementer', model: 'sonnet', effort: 'high', label: `impl-fix-qual:${task.id}`, phase: 'Per-task loop', schema: IMPL_SCHEMA },
|
||||
)
|
||||
if (fix && fix.applied_changes === true) wroteAnyFile = true
|
||||
if (fix && (fix.status === 'BLOCKED' || fix.status === 'NEEDS_CONTEXT')) {
|
||||
@@ -471,7 +476,7 @@ if (mode === 'mini' && outcome === 'DONE') {
|
||||
'fix). Then run the full suite and confirm it is green (no regression). Also report working_tree_dirty — ' +
|
||||
'whether `git status --porcelain` (NOT `git diff HEAD`, which misses new untracked files) shows any change at ' +
|
||||
'all (a clean tree means no fix landed). Do NOT edit anything; do NOT commit.',
|
||||
{ model: 'sonnet', label: 'mini-verify', phase: 'Verify', schema: MINI_VERIFY_SCHEMA },
|
||||
{ model: 'sonnet', effort: 'medium', label: 'mini-verify', phase: 'Verify', schema: MINI_VERIFY_SCHEMA },
|
||||
)
|
||||
if (!miniVerify || !miniVerify.red_test_now_green || !miniVerify.suite_green || !miniVerify.working_tree_dirty) {
|
||||
outcome = 'BLOCKED'
|
||||
@@ -498,7 +503,7 @@ if (mode === 'mini' && outcome === 'DONE') {
|
||||
'`git status --porcelain` and report files_touched = its line count (every changed, added, or untracked ' +
|
||||
'path). Use `git status --porcelain`, NOT `git diff`, so brand-new untracked files are counted. Do NOT edit, ' +
|
||||
'do NOT commit.',
|
||||
{ model: 'sonnet', label: 'tree-check', phase: 'Verify', schema: TREE_SCHEMA },
|
||||
{ model: 'sonnet', effort: 'medium', label: 'tree-check', phase: 'Verify', schema: TREE_SCHEMA },
|
||||
)
|
||||
if (!treeState || treeState.files_touched === 0) {
|
||||
outcome = 'BLOCKED'
|
||||
@@ -526,7 +531,7 @@ if (mode === 'standard' && outcome === 'DONE') {
|
||||
'E2E fixtures for them in the project\'s canonical fixture form; each test\'s doc comment names the property ' +
|
||||
'it protects. Run the project\'s test command — all green. Leave edits unstaged; never commit.\n\n' +
|
||||
`iter_id: ${iter_id}\nshipped: ${completed.map((c) => `#${c.id} ${c.title}`).join('; ')}`,
|
||||
{ agentType: 'tester', model: 'sonnet', label: 'e2e', phase: 'E2E coverage', schema: E2E_SCHEMA },
|
||||
{ agentType: 'tester', model: 'sonnet', effort: 'high', label: 'e2e', phase: 'E2E coverage', schema: E2E_SCHEMA },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -582,7 +587,7 @@ const fin = await agent(
|
||||
'and `## Files touched` (from `git status --porcelain`).\n') +
|
||||
'Report the stats path and whether you wrote BLOCKED.md. Do NOT commit anything.\n\nAGGREGATE:\n' +
|
||||
JSON.stringify(aggregate, null, 2),
|
||||
{ model: 'sonnet', label: 'finalize', phase: 'Report', schema: FINALIZE_SCHEMA },
|
||||
{ model: 'sonnet', effort: 'medium', label: 'finalize', phase: 'Report', schema: FINALIZE_SCHEMA },
|
||||
)
|
||||
|
||||
// The no-op verdict was already taken before finalize (the tree-check / mini-verify
|
||||
|
||||
Reference in New Issue
Block a user