openvibers/openvibe.codes Commits
Each turn can run at the effort a person picks, in each agent's own words (L31 CH2) (#204)
Changes
@@@ -64,6 +64,7 @@ const EFFORT_LEVELS = {codex: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'],'command-code': ['off', 'low', 'high', 'max'],opencode: [],++ aider: ['low', 'medium', 'high'],'minimax-code': ['low', 'medium', 'high', 'xhigh', 'max'],};@@@ -5,7 +5,8 @@* and how to read what it prints into the shared events (harness/events.js):** { id, name, kind: 'cli', bin, start(task, ctx) } ctx: { env } (the harness's environment: PATH finds the CLI)-- * task: { prompt, cwd, model, resume, permission: 'read' | 'edit' | 'full', maxTurns, timeoutMs, signal }++ * task: { prompt, cwd, model, effort, resume, permission: 'read' | 'edit' | 'full', maxTurns, timeoutMs, signal }++ * effort is the shared ladder (low … max); each CLI gets the nearest level it accepts, or none (OpenCode has no flag)** Permission levels mean the same for every agent, mapped onto what each CLI offers:* read look and answer: no file is changed and no command that changes anything runs@@@ -62,6 +63,7 @@ const claudeCode = {const mode = o.permissionMode || { read: 'plan', edit: 'acceptEdits', full: 'bypassPermissions' }[t.permission] || 'plan';const args = ['-p', t.prompt, '--output-format', 'stream-json', '--verbose', '--permission-mode', mode];if (t.model) args.push('--model', t.model);++ if (t.effort) args.push('--effort', effortFor('claude-code', t.effort));if (t.resume) args.push('--resume', t.resume);if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));// MCP servers this session may use (the conductor's Codes tools): a config file, and the tools it may call@@@ -95,6 +97,7 @@ const codex = {? ['exec', 'resume', t.resume, '--json', '--skip-git-repo-check']: ['exec', '--json', '--skip-git-repo-check', '-C', t.cwd, '-s', sandbox];if (t.model) args.push('-m', t.model);++ if (t.effort) args.push('-c', `model_reasoning_effort="${effortFor('codex', t.effort)}"`);args.push(t.prompt);const startedAt = Date.now();const parser = jsonLines(codex, (e, emit, st) => {@@@ -191,6 +194,7 @@ const commandCode = {const args = [`--print=${t.prompt}`, '--output-format', 'json', '--skip-onboarding', '--no-auto-update', '-t'];if (t.permission !== 'read') args.push('--yolo');if (t.model) args.push('-m', t.model);++ if (t.effort) args.push('--effort', effortFor('command-code', t.effort));if (t.resume) args.push('--resume', t.resume);if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));const startedAt = Date.now();@@@ -227,6 +231,7 @@ const aider = {const args = ['--message', t.prompt, '--yes-always', '--no-pretty', '--no-stream', '--no-check-update', '--no-show-release-notes'];if (t.permission === 'read') args.push('--dry-run');if (t.model) args.push('--model', t.model);++ if (t.effort) args.push('--reasoning-effort', effortFor('aider', t.effort));const startedAt = Date.now();const lines = [];let started = false;@@@ -154,7 +154,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,return decision;}-- function run({ prompt, task = 'edit', agent = null, model = null, cwd = process.cwd(), permission = 'edit', resume = null, handoff = true, maxAttempts = 3, stallMs = 10 * 60_000, timeoutMs, maxTurns, requirements = {}, signal, adapterOptions = null } = {}) {++ function run({ prompt, task = 'edit', agent = null, model = null, effort = null, cwd = process.cwd(), permission = 'edit', resume = null, handoff = true, maxAttempts = 3, stallMs = 10 * 60_000, timeoutMs, maxTurns, requirements = {}, signal, adapterOptions = null } = {}) {if (!prompt || !String(prompt).trim()) throw new Error('a run needs a prompt');if (!['read', 'edit', 'full'].includes(permission)) throw new Error('permission is read, edit or full');const runId = store.newRunId();@@@ -200,7 +200,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,const controller = new AbortController();if (signal) signal.addEventListener('abort', () => controller.abort(), { once: true });current = adapter.start({-- prompt: nextPrompt, cwd, model: modelFor, resume: attempt === 1 ? resume : null, permission, maxTurns, timeoutMs, signal: controller.signal,++ prompt: nextPrompt, cwd, model: modelFor, effort, resume: attempt === 1 ? resume : null, permission, maxTurns, timeoutMs, signal: controller.signal,// Extra flags a caller needs (the conductor's MCP config and permission rules); the adapter decides// which of them its CLI understands. Nothing here can replace the fields above....(adapterOptions && typeof adapterOptions === 'object' ? { options: adapterOptions } : {}),@@@ -100,6 +100,8 @@ function parseArgs(raw, env = process.env) {let a;try { a = JSON.parse(String(raw || '').trim() || '{}'); } catch { throw new Error('the job input is not JSON'); }if (!a || typeof a !== 'object' || Array.isArray(a)) throw new Error('the job input must be an object');++ const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];++ const effort = EFFORTS.includes(a.effort) ? a.effort : null;const kind = a.kind === 'bench' ? 'bench' : a.kind === 'conductor' ? 'conductor' : 'code';const prompt = typeof a.prompt === 'string' ? a.prompt.trim() : '';if (!prompt || prompt.length > MAX_PROMPT) throw new Error(`prompt must be 1-${MAX_PROMPT} characters`);@@@ -124,6 +126,7 @@ function parseArgs(raw, env = process.env) {folder, safety, mcp: a.mcp === true, codesUrl,taskId: typeof a.task_id === 'string' && /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/.test(a.task_id) ? a.task_id : null,model: typeof a.model === 'string' && a.model.length <= 100 ? a.model : null,++ ...(effort ? { effort } : {}),agent: 'claude-code',...(files.length ? { attachments: files } : {}),...(resume ? { resume } : {}),@@@ -165,6 +168,7 @@ function parseArgs(raw, env = process.env) {...(Object.keys(wanted).length ? { env: wanted } : {}),agent: typeof a.agent === 'string' && /^[a-z0-9-]{1,40}$/.test(a.agent) ? a.agent : null,model: typeof a.model === 'string' && a.model.length <= 100 ? a.model : null,++ ...(effort ? { effort } : {}),};}@@@ -490,7 +494,7 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tmsay({ t: 'step', text: 'Preparing an empty benchmark workspace' });}const run = harness.run({-- prompt: job.prompt, task: 'edit', agent: job.agent, model: job.model, cwd: work,++ prompt: job.prompt, task: 'edit', agent: job.agent, model: job.model, effort: job.effort || null, cwd: work,permission: job.permission, handoff: !job.agent, maxAttempts: job.kind === 'conductor' ? 1 : 3,// A task carried on in the same conversation resumes its session (RESUME in the environment, or the job's// own resume field).nnew file mode 100644@@@ -0,0 +1,5 @@++-- phase: expand++-- Codes L31 CH2: the reasoning effort a person chose for a turn (low, medium, high, xhigh or max on the shared ladder;++-- each agent gets the nearest level it accepts). Kept on the attempt beside its model, so the conversation can say++-- what each turn ran with. NULL is the agent's own default. No prompt, key or path is kept here.++ALTER TABLE codes_attempts ADD COLUMN IF NOT EXISTS effort text COLLATE "C";@@@ -150,18 +150,18 @@ function createAttempts({ store, config = {}, log = console, harnesses = null })* One attempt. The fence: follow_up_of is unique, so an ending that starts a follow-up twice answers the same* attempt the second time and starts no second job. created tells the caller which of the two it was.*/-- async function create({ task, agent = null, model = null, machine = null, session = null, why = null, kind = 'first', followUpOf = null, state = 'queued', budget = null }) {++ async function create({ task, agent = null, model = null, effort = null, machine = null, session = null, why = null, kind = 'first', followUpOf = null, state = 'queued', budget = null }) {if (!task || !task.id) throw new Error('an attempt belongs to a task');for (let tries = 0; tries < 5; tries++) {const n = await db.one('SELECT COALESCE(MAX(n), 0) + 1 AS n FROM codes_attempts WHERE task_id = $1', [task.id]);const id = store.newId('att');const inserted = await db.maybe(-- `INSERT INTO codes_attempts (id, task_id, n, agent, model, machine, session, state, why, kind, follow_up_of, budget_usd, budget_tokens, created_at)-- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::timestamptz)++ `INSERT INTO codes_attempts (id, task_id, n, agent, model, machine, session, state, why, kind, follow_up_of, budget_usd, budget_tokens, created_at, effort)++ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::timestamptz, $15)ON CONFLICT DO NOTHING RETURNING *`,[id, task.id, n.n, agent, model, machine, session, state, why, kind, followUpOf,budget && Number(budget.usd) > 0 ? Number(budget.usd) : null,-- budget && Number(budget.tokens) > 0 ? Math.round(Number(budget.tokens)) : null, store.iso()],++ budget && Number(budget.tokens) > 0 ? Math.round(Number(budget.tokens)) : null, store.iso(), effort || null],);if (inserted) {await db.exec('UPDATE run_tasks SET attempts_count = $2 WHERE id = $1', [task.id, n.n]);@@@ -558,7 +558,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou// The budget of the attempt: the task's limits (a benchmark's per-task limits) or the person's default.const budget = budgetFor(runArgs.limits);const first = await attemptStore.create({-- task: inserted, agent: agent || null, model: model || null, why,++ task: inserted, agent: agent || null, model: model || null, effort: runArgs.effort || null, why,kind: 'first', state: 'queued', budget,});let job;@@@ -796,7 +796,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou* continue after a crash, a wrap-up, another manual press — that attempt is returned, and no second one is started* beside it. Two sessions on one worktree would fight over the same files.*/-- async function continueTask(row, { note = null, agent = null } = {}) {++ async function continueTask(row, { note = null, agent = null, model = null, effort = null } = {}) {if (!row || !row.id) return { error: 'no such task' };const attempts = await attemptStore.list(row.id);const ending = attempts[attempts.length - 1];@@@ -806,12 +806,12 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ouconst text = note ? String(note).trim().slice(0, 4000) : '';const plan = agent? {-- kind: 'switch', agent, model: null, session: null,++ kind: 'switch', agent, model: model || null, effort: effort || null, session: null,prompt: `${row.prompt}\n\n---\n## Hand-over (written by openvibe.codes)\n\n${attemptStore.nameOf(agent)} takes over from ${attemptStore.nameOf(ending.agent)}, which ${attemptStore.wordsOf(ending.reason || 'error')}. Keep the work that was done, do not redo it, and finish the task.\n`,why: `${attemptStore.nameOf(agent)} took over at the owner's request`,}: {-- kind: 'manual', agent: ending.agent, model: ending.model, session: null,++ kind: 'manual', agent: ending.agent, model: model || ending.model, effort: effort || ending.effort || null, session: null,prompt: `${row.prompt}${text ? `\n\n---\n## A note from the owner\n\n${text}\n` : ''}`,why: text ? `carried on by the owner: ${text.slice(0, 200)}` : 'carried on by the owner',};@@@ -835,7 +835,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ouif (!TERMINAL.has(current.state)) return { error: 'the task is still running' };if (current.outcome === 'stopped') return { error: 'the task was stopped on purpose' };const made = await attemptStore.create({-- task: current, agent: plan.agent, model: plan.model, session: null, why: plan.why, kind: plan.kind, state: 'queued', budget: left.left,++ task: current, agent: plan.agent, model: plan.model, effort: plan.effort || null, session: null, why: plan.why, kind: plan.kind, state: 'queued', budget: left.left,});attempt = made.attempt;await db.one(`UPDATE run_tasks SET state = 'queued', outcome = NULL, outcome_note = NULL, error = NULL, job_id = NULL, finished_at = NULL, updated_at = $2 WHERE id = $1 RETURNING *`,@@@ -869,6 +869,8 @@ function createRunTasks({ store, run, config, log = console, sessions = null, oureturn parseArgs(JSON.stringify({repo: row.repo, ref: row.ref || null, prompt: plan.prompt || row.prompt,agent: plan.agent || null, permission: 'edit',++ ...(plan.model ? { model: plan.model } : {}),++ ...(plan.effort ? { effort: plan.effort } : {}),...(row.push ? { push: { pull_request: !!row.pull_request } } : {}),...(plan.session ? { resume: plan.session } : {}),}));@@@ -202,7 +202,8 @@ ${rows.length ? '' : '<p class="ch-empty-rail">Your conversations appear here.</const answer = (done && done.text) || said[said.length - 1] || '';const along = said.slice(0, answer && said[said.length - 1] === answer ? -1 : undefined);const alongSteps = along.map((text) => ({ name: 'Said', what: firstLine(text, 140), kind: 'sub', ok: null }));-- const who = `<div class="ch-who">${avatar(a)}<b>${esc(a.name)}</b>${attempt && attempt.model ? `<span>${esc(attempt.model)}</span>` : ''}</div>`;++ const ran = attempt ? [attempt.model, attempt.effort ? effortWord(attempt.effort) : null].filter(Boolean).join(' · ') : '';++ const who = `<div class="ch-who">${avatar(a)}<b>${esc(a.name)}</b>${ran ? `<span>${esc(ran)}</span>` : ''}</div>`;const body = answer ? `<div class="ch-md">${markdown(answer)}</div>` : live ? '' : `<p class="ch-quiet">${esc(attempt && attempt.end_why ? attempt.end_why : 'It ended without a word.')}</p>`;const working = live ? `<div class="ch-working" data-ch-working><span class="ch-dots" aria-hidden="true"><i></i><i></i><i></i></span><span data-ch-working-text>${steps.length ? esc(`${steps[steps.length - 1].name} ${steps[steps.length - 1].what || ''}`.trim()) : 'Starting…'}</span></div>` : '';return `<div class="ch-turn ch-agent${live ? ' is-live' : ''}"${attempt ? ` data-attempt="${Number(attempt.n)}"` : ''}>${who}${stepsHtml([...alongSteps, ...steps], ms, live)}${body}${working}</div>`;@@@ -295,6 +296,12 @@ ${rows.length ? '' : '<p class="ch-empty-rail">Your conversations appear here.</const list = harnesses?.list?.() || [];return `<label class="ch-pick" title="${esc(label)}"><span class="ch-sr">${esc(label)}</span><select name="agent" data-ch-agent><option value="">${esc(auto)}</option>${list.map((a) => `<option value="${esc(a.id)}"${selected === a.id ? ' selected' : ''}>${esc(a.name)}</option>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;}++ // The effort in plain words (the shared ladder underneath; each agent gets the nearest level it accepts).++ const EFFORT = [['', 'Effort: auto'], ['low', 'Quick'], ['medium', 'Balanced'], ['high', 'Thorough'], ['max', 'Deepest']];++ const effortWord = (v) => (EFFORT.find(([k]) => k === v) || [null, v])[1];++ function effortPicker(selected = '') {++ return `<label class="ch-pick" title="How hard it thinks"><span class="ch-sr">Effort</span>${ico('sub')}<select name="effort" data-ch-effort>${EFFORT.map(([v, l]) => `<option value="${v}"${selected === v ? ' selected' : ''}>${l}</option>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;++ }function composer({ action, api = '', placeholder, pickers = '', live = false, stopAction = '', hidden = '', note = '' }) {return `<form class="ch-composer" method="post" action="${esc(action)}"${api ? ` data-ch-api="${esc(api)}"` : ''} data-ch-composer>${hidden}<label class="ch-sr" for="ch-text">${esc(placeholder)}</label>@@@ -349,7 +356,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${fconst error = String(req.query.error || '').slice(0, 200);const name = String(req.viewer.displayName || req.viewer.username || '').split(' ')[0];const ideas = ['Fix the failing tests', 'Explain how this project is laid out', 'Make the home page faster on phones', 'Find and fix one bug'];-- const pickers = `<label class="ch-pick ch-pick-repo" title="Repository"><span class="ch-sr">Repository</span>${ico('repo')}<input name="repo" type="url" value="${esc(repo)}" placeholder="Repository URL" list="ch-repos" autocomplete="url" required data-ch-repo data-ch-repo-url="/api/v1/me/repos"><datalist id="ch-repos">${repos.map((x) => `<option value="${esc(x.url)}">${esc(x.full_name)}</option>`).join('')}</datalist></label>${agentPicker(settings.default_agent || '')}`;++ const pickers = `<label class="ch-pick ch-pick-repo" title="Repository"><span class="ch-sr">Repository</span>${ico('repo')}<input name="repo" type="url" value="${esc(repo)}" placeholder="Repository URL" list="ch-repos" autocomplete="url" required data-ch-repo data-ch-repo-url="/api/v1/me/repos"><datalist id="ch-repos">${repos.map((x) => `<option value="${esc(x.url)}">${esc(x.full_name)}</option>`).join('')}</datalist></label>${agentPicker(settings.default_agent || '')}${effortPicker('')}`;const thread = `<div class="ch-hello"><span class="ov-mark" data-size="56" aria-hidden="true"></span><h1>${name ? `What should we build, ${esc(name)}?` : 'What should we build?'}</h1><p>Say it the way you would to a colleague. Codes picks the agent, runs it on your machine, and shows you what changed.</p>${error ? `<p class="ch-error" role="alert">${esc(error)}</p>` : ''}<div class="ch-ideas" aria-label="Ideas">${ideas.map((t) => `<button class="ch-chip" type="button" data-ch-idea="${esc(t)}">${esc(t)}</button>`).join('')}</div>${repos.length > 1 ? `<div class="ch-ideas ch-recent" aria-label="Your recent repositories">${repos.map((x) => `<button class="ch-chip" type="button" data-ch-saved-repo="${esc(x.url)}">${ico('repo')}${esc(x.full_name)}</button>`).join('')}</div>` : ''}</div>`;return frame(req, res, {title: 'New chat', empty: true, head: '<h1 class="ch-title">New chat</h1>', thread,@@@ -370,7 +377,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${flet args;try {const { parseArgs } = require('../../harness/job');-- args = parseArgs(JSON.stringify({ repo: String(b.repo || '').trim(), prompt: text, agent: agent || null, permission: 'edit' }));++ args = parseArgs(JSON.stringify({ repo: String(b.repo || '').trim(), prompt: text, agent: agent || null, permission: 'edit', ...(b.effort ? { effort: String(b.effort) } : {}) }));} catch (e) { return back(`Check the repository: ${e.message}.`); }let project = null;try { project = (await tasks.latestProject?.(req.viewer.subject)) || await conductor.projectFor(req.viewer.token || null, req.viewer.subject); } catch { project = null; }@@@ -405,7 +412,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f// The stream starts after the last line this page already shows.const seqRow = live ? await db.maybe('SELECT max(seq) AS seq FROM codes_session_lines WHERE task_id = $1', [row.id]) : null;const lastSeq = seqRow && seqRow.seq != null ? Number(seqRow.seq) : 0;-- const pickers = agentPicker('', { label: 'Who carries on', auto: `Same agent (${agentOf(row.agent, agentNames()).name})` });++ const pickers = agentPicker('', { label: 'Who carries on', auto: `Same agent (${agentOf(row.agent, agentNames()).name})` }) + effortPicker('');return frame(req, res, {currentId: row.id, title: titleOf(row), head, side: await sideHtml(row),thread: (error ? `<p class="ch-error" role="alert">${esc(error)}</p>` : '') + await taskThread(row),@@@ -428,7 +435,8 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${fconst b = req.body || {};const agent = String(b.agent || '').trim() || null;if (agent && !(harnesses?.list?.() || []).some((a) => a.id === agent)) return res.redirect(303, `/tasks/${encodeURIComponent(row.id)}?error=${encodeURIComponent('Choose an agent from the list.')}`);-- const out = await tasks.continueTask(row, { note: String(b.text || '').slice(0, 4000), agent });++ const effort = ['low', 'medium', 'high', 'xhigh', 'max'].includes(b.effort) ? b.effort : null;++ const out = await tasks.continueTask(row, { note: String(b.text || '').slice(0, 4000), agent, effort });return res.redirect(303, `/tasks/${encodeURIComponent(row.id)}${out.error ? `?error=${encodeURIComponent(out.error)}` : ''}`);});@@@ -28,7 +28,7 @@ function createTaskEvents({ store, config = {}, log = console, attempts = null }const frame = (event, data, id = null) => (id == null ? '' : `id: ${id}\n`) + `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;const started = (a) => ({-- type: 'attempt_started', attempt_n: Number(a.n), id: a.id, agent: a.agent || null, model: a.model || null,++ type: 'attempt_started', attempt_n: Number(a.n), id: a.id, agent: a.agent || null, model: a.model || null, effort: a.effort || null,why: a.why || null, state: 'queued', at: a.created_at ? new Date(a.created_at).toISOString() : null,});const ended = (a) => ({@@@ -326,7 +326,10 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, taskconst note = typeof b.note === 'string' ? b.note.slice(0, 4000) : null;const agent = typeof b.agent === 'string' && b.agent ? b.agent : null;if (agent && !harnesses.list().some((a) => a.id === agent)) return problem(req, res, 422, 'codes.agent_unknown', 'agent is one of GET /api/v1/harnesses');-- const out = await taskStore.continueTask(row, { note, agent });++ const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];++ const model = typeof b.model === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:\/#-]{0,99}$/.test(b.model) ? b.model : null;++ const effort = EFFORTS.includes(b.effort) ? b.effort : null;++ const out = await taskStore.continueTask(row, { note: note || (typeof b.text === 'string' ? b.text.slice(0, 4000) : null), agent, model, effort });if (out.error) return problem(req, res, 409, 'codes.task_conflict', out.error);// One continuation at a time: an attempt that is already live answers this press, and no second one starts.if (!out.live) log.info(`[Codes] task ${row.id} carried on (${agent ? `with ${agent}` : 'by the owner'})`);@@@ -68,6 +68,16 @@ const { stepKind } = require('../server/http/chat');assert.ok(row && row.prompt === 'Make the footer quieter' && row.subject === ada.subject, 'the brief is the person’s own task');});++ await check('the effort a person picks rides on the first attempt, and the thread says it in words', async () => {++ const made = await t.get('/chat', { as: ada, form: { text: 'Think hard about the cache', repo: 'https://github.com/OpenVibers/OpenVibe.Live', effort: 'high' } });++ assert.strictEqual(made.status, 303);++ const id = made.headers.get('location').split('/').pop();++ const att = await db.maybe('SELECT effort FROM codes_attempts WHERE task_id = $1 ORDER BY n LIMIT 1', [id]);++ assert.ok(att && att.effort === 'high', `the attempt keeps it (${att && att.effort})`);++ const home = await t.get('/', { as: ada });++ assert.ok(home.text.includes('name="effort"') && home.text.includes('>Thorough</option>'), 'the composer offers it in plain words');++ });++await check('a visitor who is not signed in gets the product’s home, not the app', async () => {const page = await t.get('/');assert.strictEqual(page.status, 200);nnew file mode 100644@@@ -0,0 +1,63 @@++'use strict';++/**++ * The reasoning effort a person picks for a turn (plan L31 CH2) reaches each CLI in its own words: Claude Code and++ * Command Code take --effort, Codex its model_reasoning_effort setting, Aider --reasoning-effort, and OpenCode has no++ * such flag. The level is fitted to what each one accepts (engine/adapters/base.js effortFor: Command Code has no++ * "medium", Aider no "max"), and a turn without an effort passes none. The job input keeps only a level on the ladder.++ */++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { check, done } = require('./helpers/boot');++const cli = require('../harness/adapters/cli');++const { parseArgs } = require('../harness/job');++++// One stand-in for every CLI: it records its argv and says nothing (the adapter then reports a failed run).++const FAKE = `++const fs = require('fs');++if (process.argv.includes('--version')) { console.log('9.9.9'); process.exit(0); }++fs.writeFileSync(process.env.FAKE_RECORD, JSON.stringify({ argv: process.argv.slice(2) }));++`;++++(async () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-effort-'));++ const bin = path.join(dir, 'bin');++ fs.mkdirSync(bin);++ for (const name of ['claude', 'codex', 'cmd', 'aider', 'opencode']) fs.writeFileSync(path.join(bin, name), `#!${process.execPath}\n'use strict';\n${FAKE}\n`, { mode: 0o755 });++ const record = path.join(dir, 'record.json');++ const argvOf = async (adapter, effort) => {++ fs.rmSync(record, { force: true });++ for await (const e of adapter.start({ prompt: 'tidy the module', cwd: dir, permission: 'edit', ...(effort ? { effort } : {}) }, { env: { PATH: bin, FAKE_RECORD: record } })) void e;++ return JSON.parse(fs.readFileSync(record, 'utf8')).argv;++ };++ const after = (argv, flag) => { const i = argv.indexOf(flag); return i < 0 ? null : argv[i + 1]; };++ try {++ await check('each CLI gets the effort in its own words', async () => {++ assert.strictEqual(after(await argvOf(cli.claudeCode, 'high'), '--effort'), 'high', 'Claude Code');++ const codex = await argvOf(cli.codex, 'high');++ assert.ok(codex.includes('model_reasoning_effort="high"') && after(codex, '-c') === 'model_reasoning_effort="high"', `Codex: ${codex.join(' ')}`);++ assert.strictEqual(after(await argvOf(cli.commandCode, 'high'), '--effort'), 'high', 'Command Code');++ assert.strictEqual(after(await argvOf(cli.aider, 'high'), '--reasoning-effort'), 'high', 'Aider');++ });++++ await check('a level a CLI does not have becomes the nearest one it does', async () => {++ assert.strictEqual(after(await argvOf(cli.commandCode, 'medium'), '--effort'), 'high', 'Command Code has no medium: a tie goes up');++ assert.strictEqual(after(await argvOf(cli.aider, 'max'), '--reasoning-effort'), 'high', 'Aider tops out at high');++ });++++ await check('no effort, no flag; and OpenCode, which has none, is never given one', async () => {++ const plain = await argvOf(cli.claudeCode, null);++ assert.ok(!plain.includes('--effort'), 'Claude Code at its default');++ assert.ok(!(await argvOf(cli.codex, null)).some((a) => /reasoning_effort/.test(a)), 'Codex at its default');++ assert.ok(!(await argvOf(cli.opencode, 'max')).some((a) => /effort/.test(a)), 'OpenCode');++ });++++ await check('the job input keeps an effort on the ladder and drops anything else', () => {++ assert.strictEqual(parseArgs(JSON.stringify({ repo: 'https://github.com/a/b', prompt: 'x', effort: 'max' })).effort, 'max');++ assert.strictEqual(parseArgs(JSON.stringify({ repo: 'https://github.com/a/b', prompt: 'x', effort: 'ludicrous' })).effort, undefined);++ assert.strictEqual(parseArgs(JSON.stringify({ kind: 'conductor', folder: '/home/x', prompt: 'x', effort: 'low' })).effort, 'low');++ });++ } finally { fs.rmSync(dir, { recursive: true, force: true }); }++ done();++})().catch((e) => { console.error(e); process.exit(1); });
@@ -64,6 +64,7 @@ const EFFORT_LEVELS = {
@@ -64,6 +64,7 @@ const EFFORT_LEVELS = {
codex: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
codex: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
'command-code': ['off', 'low', 'high', 'max'],
'command-code': ['off', 'low', 'high', 'max'],
opencode: [],
opencode: [],
+ aider: ['low', 'medium', 'high'],
'minimax-code': ['low', 'medium', 'high', 'xhigh', 'max'],
'minimax-code': ['low', 'medium', 'high', 'xhigh', 'max'],
};
};
@@ -5,7 +5,8 @@
@@ -5,7 +5,8 @@
* and how to read what it prints into the shared events (harness/events.js):
* and how to read what it prints into the shared events (harness/events.js):
*
*
* { id, name, kind: 'cli', bin, start(task, ctx) } ctx: { env } (the harness's environment: PATH finds the CLI)
* { id, name, kind: 'cli', bin, start(task, ctx) } ctx: { env } (the harness's environment: PATH finds the CLI)
- * task: { prompt, cwd, model, resume, permission: 'read' | 'edit' | 'full', maxTurns, timeoutMs, signal }
+ * task: { prompt, cwd, model, effort, resume, permission: 'read' | 'edit' | 'full', maxTurns, timeoutMs, signal }
+ * effort is the shared ladder (low … max); each CLI gets the nearest level it accepts, or none (OpenCode has no flag)
*
*
* Permission levels mean the same for every agent, mapped onto what each CLI offers:
* Permission levels mean the same for every agent, mapped onto what each CLI offers:
* read look and answer: no file is changed and no command that changes anything runs
* read look and answer: no file is changed and no command that changes anything runs
@@ -62,6 +63,7 @@ const claudeCode = {
@@ -62,6 +63,7 @@ const claudeCode = {
const mode = o.permissionMode || { read: 'plan', edit: 'acceptEdits', full: 'bypassPermissions' }[t.permission] || 'plan';
const mode = o.permissionMode || { read: 'plan', edit: 'acceptEdits', full: 'bypassPermissions' }[t.permission] || 'plan';
const args = ['-p', t.prompt, '--output-format', 'stream-json', '--verbose', '--permission-mode', mode];
const args = ['-p', t.prompt, '--output-format', 'stream-json', '--verbose', '--permission-mode', mode];
if (t.model) args.push('--model', t.model);
if (t.model) args.push('--model', t.model);
+ if (t.effort) args.push('--effort', effortFor('claude-code', t.effort));
if (t.resume) args.push('--resume', t.resume);
if (t.resume) args.push('--resume', t.resume);
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
// MCP servers this session may use (the conductor's Codes tools): a config file, and the tools it may call
// MCP servers this session may use (the conductor's Codes tools): a config file, and the tools it may call
@@ -95,6 +97,7 @@ const codex = {
@@ -95,6 +97,7 @@ const codex = {
? ['exec', 'resume', t.resume, '--json', '--skip-git-repo-check']
? ['exec', 'resume', t.resume, '--json', '--skip-git-repo-check']
: ['exec', '--json', '--skip-git-repo-check', '-C', t.cwd, '-s', sandbox];
: ['exec', '--json', '--skip-git-repo-check', '-C', t.cwd, '-s', sandbox];
if (t.model) args.push('-m', t.model);
if (t.model) args.push('-m', t.model);
+ if (t.effort) args.push('-c', `model_reasoning_effort="${effortFor('codex', t.effort)}"`);
args.push(t.prompt);
args.push(t.prompt);
const startedAt = Date.now();
const startedAt = Date.now();
const parser = jsonLines(codex, (e, emit, st) => {
const parser = jsonLines(codex, (e, emit, st) => {
@@ -191,6 +194,7 @@ const commandCode = {
@@ -191,6 +194,7 @@ const commandCode = {
const args = [`--print=${t.prompt}`, '--output-format', 'json', '--skip-onboarding', '--no-auto-update', '-t'];
const args = [`--print=${t.prompt}`, '--output-format', 'json', '--skip-onboarding', '--no-auto-update', '-t'];
if (t.permission !== 'read') args.push('--yolo');
if (t.permission !== 'read') args.push('--yolo');
if (t.model) args.push('-m', t.model);
if (t.model) args.push('-m', t.model);
+ if (t.effort) args.push('--effort', effortFor('command-code', t.effort));
if (t.resume) args.push('--resume', t.resume);
if (t.resume) args.push('--resume', t.resume);
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
const startedAt = Date.now();
const startedAt = Date.now();
@@ -227,6 +231,7 @@ const aider = {
@@ -227,6 +231,7 @@ const aider = {
const args = ['--message', t.prompt, '--yes-always', '--no-pretty', '--no-stream', '--no-check-update', '--no-show-release-notes'];
const args = ['--message', t.prompt, '--yes-always', '--no-pretty', '--no-stream', '--no-check-update', '--no-show-release-notes'];
if (t.permission === 'read') args.push('--dry-run');
if (t.permission === 'read') args.push('--dry-run');
if (t.model) args.push('--model', t.model);
if (t.model) args.push('--model', t.model);
+ if (t.effort) args.push('--reasoning-effort', effortFor('aider', t.effort));
const startedAt = Date.now();
const startedAt = Date.now();
const lines = [];
const lines = [];
let started = false;
let started = false;
@@ -154,7 +154,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,
@@ -154,7 +154,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,
return decision;
return decision;
}
}
- function run({ prompt, task = 'edit', agent = null, model = null, cwd = process.cwd(), permission = 'edit', resume = null, handoff = true, maxAttempts = 3, stallMs = 10 * 60_000, timeoutMs, maxTurns, requirements = {}, signal, adapterOptions = null } = {}) {
+ function run({ prompt, task = 'edit', agent = null, model = null, effort = null, cwd = process.cwd(), permission = 'edit', resume = null, handoff = true, maxAttempts = 3, stallMs = 10 * 60_000, timeoutMs, maxTurns, requirements = {}, signal, adapterOptions = null } = {}) {
if (!prompt || !String(prompt).trim()) throw new Error('a run needs a prompt');
if (!prompt || !String(prompt).trim()) throw new Error('a run needs a prompt');
if (!['read', 'edit', 'full'].includes(permission)) throw new Error('permission is read, edit or full');
if (!['read', 'edit', 'full'].includes(permission)) throw new Error('permission is read, edit or full');
const runId = store.newRunId();
const runId = store.newRunId();
@@ -200,7 +200,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,
@@ -200,7 +200,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,
const controller = new AbortController();
const controller = new AbortController();
if (signal) signal.addEventListener('abort', () => controller.abort(), { once: true });
if (signal) signal.addEventListener('abort', () => controller.abort(), { once: true });
current = adapter.start({
current = adapter.start({
- prompt: nextPrompt, cwd, model: modelFor, resume: attempt === 1 ? resume : null, permission, maxTurns, timeoutMs, signal: controller.signal,
+ prompt: nextPrompt, cwd, model: modelFor, effort, resume: attempt === 1 ? resume : null, permission, maxTurns, timeoutMs, signal: controller.signal,
// Extra flags a caller needs (the conductor's MCP config and permission rules); the adapter decides
// Extra flags a caller needs (the conductor's MCP config and permission rules); the adapter decides
// which of them its CLI understands. Nothing here can replace the fields above.
// which of them its CLI understands. Nothing here can replace the fields above.
...(adapterOptions && typeof adapterOptions === 'object' ? { options: adapterOptions } : {}),
...(adapterOptions && typeof adapterOptions === 'object' ? { options: adapterOptions } : {}),
@@ -100,6 +100,8 @@ function parseArgs(raw, env = process.env) {
@@ -100,6 +100,8 @@ function parseArgs(raw, env = process.env) {
let a;
let a;
try { a = JSON.parse(String(raw || '').trim() || '{}'); } catch { throw new Error('the job input is not JSON'); }
try { a = JSON.parse(String(raw || '').trim() || '{}'); } catch { throw new Error('the job input is not JSON'); }
if (!a || typeof a !== 'object' || Array.isArray(a)) throw new Error('the job input must be an object');
if (!a || typeof a !== 'object' || Array.isArray(a)) throw new Error('the job input must be an object');
+ const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
+ const effort = EFFORTS.includes(a.effort) ? a.effort : null;
const kind = a.kind === 'bench' ? 'bench' : a.kind === 'conductor' ? 'conductor' : 'code';
const kind = a.kind === 'bench' ? 'bench' : a.kind === 'conductor' ? 'conductor' : 'code';
const prompt = typeof a.prompt === 'string' ? a.prompt.trim() : '';
const prompt = typeof a.prompt === 'string' ? a.prompt.trim() : '';
if (!prompt || prompt.length > MAX_PROMPT) throw new Error(`prompt must be 1-${MAX_PROMPT} characters`);
if (!prompt || prompt.length > MAX_PROMPT) throw new Error(`prompt must be 1-${MAX_PROMPT} characters`);
@@ -124,6 +126,7 @@ function parseArgs(raw, env = process.env) {
@@ -124,6 +126,7 @@ function parseArgs(raw, env = process.env) {
folder, safety, mcp: a.mcp === true, codesUrl,
folder, safety, mcp: a.mcp === true, codesUrl,
taskId: typeof a.task_id === 'string' && /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/.test(a.task_id) ? a.task_id : null,
taskId: typeof a.task_id === 'string' && /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/.test(a.task_id) ? a.task_id : null,
model: typeof a.model === 'string' && a.model.length <= 100 ? a.model : null,
model: typeof a.model === 'string' && a.model.length <= 100 ? a.model : null,
+ ...(effort ? { effort } : {}),
agent: 'claude-code',
agent: 'claude-code',
...(files.length ? { attachments: files } : {}),
...(files.length ? { attachments: files } : {}),
...(resume ? { resume } : {}),
...(resume ? { resume } : {}),
@@ -165,6 +168,7 @@ function parseArgs(raw, env = process.env) {
@@ -165,6 +168,7 @@ function parseArgs(raw, env = process.env) {
...(Object.keys(wanted).length ? { env: wanted } : {}),
...(Object.keys(wanted).length ? { env: wanted } : {}),
agent: typeof a.agent === 'string' && /^[a-z0-9-]{1,40}$/.test(a.agent) ? a.agent : null,
agent: typeof a.agent === 'string' && /^[a-z0-9-]{1,40}$/.test(a.agent) ? a.agent : null,
model: typeof a.model === 'string' && a.model.length <= 100 ? a.model : null,
model: typeof a.model === 'string' && a.model.length <= 100 ? a.model : null,
+ ...(effort ? { effort } : {}),
};
};
}
}
@@ -490,7 +494,7 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
@@ -490,7 +494,7 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
say({ t: 'step', text: 'Preparing an empty benchmark workspace' });
say({ t: 'step', text: 'Preparing an empty benchmark workspace' });
}
}
const run = harness.run({
const run = harness.run({
- prompt: job.prompt, task: 'edit', agent: job.agent, model: job.model, cwd: work,
+ prompt: job.prompt, task: 'edit', agent: job.agent, model: job.model, effort: job.effort || null, cwd: work,
permission: job.permission, handoff: !job.agent, maxAttempts: job.kind === 'conductor' ? 1 : 3,
permission: job.permission, handoff: !job.agent, maxAttempts: job.kind === 'conductor' ? 1 : 3,
// A task carried on in the same conversation resumes its session (RESUME in the environment, or the job's
// A task carried on in the same conversation resumes its session (RESUME in the environment, or the job's
// own resume field).
// own resume field).
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+-- phase: expand
+-- Codes L31 CH2: the reasoning effort a person chose for a turn (low, medium, high, xhigh or max on the shared ladder;
+-- each agent gets the nearest level it accepts). Kept on the attempt beside its model, so the conversation can say
+-- what each turn ran with. NULL is the agent's own default. No prompt, key or path is kept here.
+ALTER TABLE codes_attempts ADD COLUMN IF NOT EXISTS effort text COLLATE "C";
@@ -150,18 +150,18 @@ function createAttempts({ store, config = {}, log = console, harnesses = null })
@@ -150,18 +150,18 @@ function createAttempts({ store, config = {}, log = console, harnesses = null })
* One attempt. The fence: follow_up_of is unique, so an ending that starts a follow-up twice answers the same
* One attempt. The fence: follow_up_of is unique, so an ending that starts a follow-up twice answers the same
* attempt the second time and starts no second job. created tells the caller which of the two it was.
* attempt the second time and starts no second job. created tells the caller which of the two it was.
*/
*/
- async function create({ task, agent = null, model = null, machine = null, session = null, why = null, kind = 'first', followUpOf = null, state = 'queued', budget = null }) {
+ async function create({ task, agent = null, model = null, effort = null, machine = null, session = null, why = null, kind = 'first', followUpOf = null, state = 'queued', budget = null }) {
if (!task || !task.id) throw new Error('an attempt belongs to a task');
if (!task || !task.id) throw new Error('an attempt belongs to a task');
for (let tries = 0; tries < 5; tries++) {
for (let tries = 0; tries < 5; tries++) {
const n = await db.one('SELECT COALESCE(MAX(n), 0) + 1 AS n FROM codes_attempts WHERE task_id = $1', [task.id]);
const n = await db.one('SELECT COALESCE(MAX(n), 0) + 1 AS n FROM codes_attempts WHERE task_id = $1', [task.id]);
const id = store.newId('att');
const id = store.newId('att');
const inserted = await db.maybe(
const inserted = await db.maybe(
- `INSERT INTO codes_attempts (id, task_id, n, agent, model, machine, session, state, why, kind, follow_up_of, budget_usd, budget_tokens, created_at)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::timestamptz)
+ `INSERT INTO codes_attempts (id, task_id, n, agent, model, machine, session, state, why, kind, follow_up_of, budget_usd, budget_tokens, created_at, effort)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::timestamptz, $15)
ON CONFLICT DO NOTHING RETURNING *`,
ON CONFLICT DO NOTHING RETURNING *`,
[id, task.id, n.n, agent, model, machine, session, state, why, kind, followUpOf,
[id, task.id, n.n, agent, model, machine, session, state, why, kind, followUpOf,
budget && Number(budget.usd) > 0 ? Number(budget.usd) : null,
budget && Number(budget.usd) > 0 ? Number(budget.usd) : null,
- budget && Number(budget.tokens) > 0 ? Math.round(Number(budget.tokens)) : null, store.iso()],
+ budget && Number(budget.tokens) > 0 ? Math.round(Number(budget.tokens)) : null, store.iso(), effort || null],
);
);
if (inserted) {
if (inserted) {
await db.exec('UPDATE run_tasks SET attempts_count = $2 WHERE id = $1', [task.id, n.n]);
await db.exec('UPDATE run_tasks SET attempts_count = $2 WHERE id = $1', [task.id, n.n]);
@@ -558,7 +558,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
@@ -558,7 +558,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
// The budget of the attempt: the task's limits (a benchmark's per-task limits) or the person's default.
// The budget of the attempt: the task's limits (a benchmark's per-task limits) or the person's default.
const budget = budgetFor(runArgs.limits);
const budget = budgetFor(runArgs.limits);
const first = await attemptStore.create({
const first = await attemptStore.create({
- task: inserted, agent: agent || null, model: model || null, why,
+ task: inserted, agent: agent || null, model: model || null, effort: runArgs.effort || null, why,
kind: 'first', state: 'queued', budget,
kind: 'first', state: 'queued', budget,
});
});
let job;
let job;
@@ -796,7 +796,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
@@ -796,7 +796,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
* continue after a crash, a wrap-up, another manual press — that attempt is returned, and no second one is started
* continue after a crash, a wrap-up, another manual press — that attempt is returned, and no second one is started
* beside it. Two sessions on one worktree would fight over the same files.
* beside it. Two sessions on one worktree would fight over the same files.
*/
*/
- async function continueTask(row, { note = null, agent = null } = {}) {
+ async function continueTask(row, { note = null, agent = null, model = null, effort = null } = {}) {
if (!row || !row.id) return { error: 'no such task' };
if (!row || !row.id) return { error: 'no such task' };
const attempts = await attemptStore.list(row.id);
const attempts = await attemptStore.list(row.id);
const ending = attempts[attempts.length - 1];
const ending = attempts[attempts.length - 1];
@@ -806,12 +806,12 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
@@ -806,12 +806,12 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
const text = note ? String(note).trim().slice(0, 4000) : '';
const text = note ? String(note).trim().slice(0, 4000) : '';
const plan = agent
const plan = agent
? {
? {
- kind: 'switch', agent, model: null, session: null,
+ kind: 'switch', agent, model: model || null, effort: effort || null, session: null,
prompt: `${row.prompt}\n\n---\n## Hand-over (written by openvibe.codes)\n\n${attemptStore.nameOf(agent)} takes over from ${attemptStore.nameOf(ending.agent)}, which ${attemptStore.wordsOf(ending.reason || 'error')}. Keep the work that was done, do not redo it, and finish the task.\n`,
prompt: `${row.prompt}\n\n---\n## Hand-over (written by openvibe.codes)\n\n${attemptStore.nameOf(agent)} takes over from ${attemptStore.nameOf(ending.agent)}, which ${attemptStore.wordsOf(ending.reason || 'error')}. Keep the work that was done, do not redo it, and finish the task.\n`,
why: `${attemptStore.nameOf(agent)} took over at the owner's request`,
why: `${attemptStore.nameOf(agent)} took over at the owner's request`,
}
}
: {
: {
- kind: 'manual', agent: ending.agent, model: ending.model, session: null,
+ kind: 'manual', agent: ending.agent, model: model || ending.model, effort: effort || ending.effort || null, session: null,
prompt: `${row.prompt}${text ? `\n\n---\n## A note from the owner\n\n${text}\n` : ''}`,
prompt: `${row.prompt}${text ? `\n\n---\n## A note from the owner\n\n${text}\n` : ''}`,
why: text ? `carried on by the owner: ${text.slice(0, 200)}` : 'carried on by the owner',
why: text ? `carried on by the owner: ${text.slice(0, 200)}` : 'carried on by the owner',
};
};
@@ -835,7 +835,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
@@ -835,7 +835,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
if (!TERMINAL.has(current.state)) return { error: 'the task is still running' };
if (!TERMINAL.has(current.state)) return { error: 'the task is still running' };
if (current.outcome === 'stopped') return { error: 'the task was stopped on purpose' };
if (current.outcome === 'stopped') return { error: 'the task was stopped on purpose' };
const made = await attemptStore.create({
const made = await attemptStore.create({
- task: current, agent: plan.agent, model: plan.model, session: null, why: plan.why, kind: plan.kind, state: 'queued', budget: left.left,
+ task: current, agent: plan.agent, model: plan.model, effort: plan.effort || null, session: null, why: plan.why, kind: plan.kind, state: 'queued', budget: left.left,
});
});
attempt = made.attempt;
attempt = made.attempt;
await db.one(`UPDATE run_tasks SET state = 'queued', outcome = NULL, outcome_note = NULL, error = NULL, job_id = NULL, finished_at = NULL, updated_at = $2 WHERE id = $1 RETURNING *`,
await db.one(`UPDATE run_tasks SET state = 'queued', outcome = NULL, outcome_note = NULL, error = NULL, job_id = NULL, finished_at = NULL, updated_at = $2 WHERE id = $1 RETURNING *`,
@@ -869,6 +869,8 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
@@ -869,6 +869,8 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
return parseArgs(JSON.stringify({
return parseArgs(JSON.stringify({
repo: row.repo, ref: row.ref || null, prompt: plan.prompt || row.prompt,
repo: row.repo, ref: row.ref || null, prompt: plan.prompt || row.prompt,
agent: plan.agent || null, permission: 'edit',
agent: plan.agent || null, permission: 'edit',
+ ...(plan.model ? { model: plan.model } : {}),
+ ...(plan.effort ? { effort: plan.effort } : {}),
...(row.push ? { push: { pull_request: !!row.pull_request } } : {}),
...(row.push ? { push: { pull_request: !!row.pull_request } } : {}),
...(plan.session ? { resume: plan.session } : {}),
...(plan.session ? { resume: plan.session } : {}),
}));
}));
@@ -202,7 +202,8 @@ ${rows.length ? '' : '<p class="ch-empty-rail">Your conversations appear here.</
@@ -202,7 +202,8 @@ ${rows.length ? '' : '<p class="ch-empty-rail">Your conversations appear here.</
const answer = (done && done.text) || said[said.length - 1] || '';
const answer = (done && done.text) || said[said.length - 1] || '';
const along = said.slice(0, answer && said[said.length - 1] === answer ? -1 : undefined);
const along = said.slice(0, answer && said[said.length - 1] === answer ? -1 : undefined);
const alongSteps = along.map((text) => ({ name: 'Said', what: firstLine(text, 140), kind: 'sub', ok: null }));
const alongSteps = along.map((text) => ({ name: 'Said', what: firstLine(text, 140), kind: 'sub', ok: null }));
- const who = `<div class="ch-who">${avatar(a)}<b>${esc(a.name)}</b>${attempt && attempt.model ? `<span>${esc(attempt.model)}</span>` : ''}</div>`;
+ const ran = attempt ? [attempt.model, attempt.effort ? effortWord(attempt.effort) : null].filter(Boolean).join(' · ') : '';
+ const who = `<div class="ch-who">${avatar(a)}<b>${esc(a.name)}</b>${ran ? `<span>${esc(ran)}</span>` : ''}</div>`;
const body = answer ? `<div class="ch-md">${markdown(answer)}</div>` : live ? '' : `<p class="ch-quiet">${esc(attempt && attempt.end_why ? attempt.end_why : 'It ended without a word.')}</p>`;
const body = answer ? `<div class="ch-md">${markdown(answer)}</div>` : live ? '' : `<p class="ch-quiet">${esc(attempt && attempt.end_why ? attempt.end_why : 'It ended without a word.')}</p>`;
const working = live ? `<div class="ch-working" data-ch-working><span class="ch-dots" aria-hidden="true"><i></i><i></i><i></i></span><span data-ch-working-text>${steps.length ? esc(`${steps[steps.length - 1].name} ${steps[steps.length - 1].what || ''}`.trim()) : 'Starting…'}</span></div>` : '';
const working = live ? `<div class="ch-working" data-ch-working><span class="ch-dots" aria-hidden="true"><i></i><i></i><i></i></span><span data-ch-working-text>${steps.length ? esc(`${steps[steps.length - 1].name} ${steps[steps.length - 1].what || ''}`.trim()) : 'Starting…'}</span></div>` : '';
return `<div class="ch-turn ch-agent${live ? ' is-live' : ''}"${attempt ? ` data-attempt="${Number(attempt.n)}"` : ''}>${who}${stepsHtml([...alongSteps, ...steps], ms, live)}${body}${working}</div>`;
return `<div class="ch-turn ch-agent${live ? ' is-live' : ''}"${attempt ? ` data-attempt="${Number(attempt.n)}"` : ''}>${who}${stepsHtml([...alongSteps, ...steps], ms, live)}${body}${working}</div>`;
@@ -295,6 +296,12 @@ ${rows.length ? '' : '<p class="ch-empty-rail">Your conversations appear here.</
@@ -295,6 +296,12 @@ ${rows.length ? '' : '<p class="ch-empty-rail">Your conversations appear here.</
const list = harnesses?.list?.() || [];
const list = harnesses?.list?.() || [];
return `<label class="ch-pick" title="${esc(label)}"><span class="ch-sr">${esc(label)}</span><select name="agent" data-ch-agent><option value="">${esc(auto)}</option>${list.map((a) => `<option value="${esc(a.id)}"${selected === a.id ? ' selected' : ''}>${esc(a.name)}</option>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
return `<label class="ch-pick" title="${esc(label)}"><span class="ch-sr">${esc(label)}</span><select name="agent" data-ch-agent><option value="">${esc(auto)}</option>${list.map((a) => `<option value="${esc(a.id)}"${selected === a.id ? ' selected' : ''}>${esc(a.name)}</option>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
}
}
+ // The effort in plain words (the shared ladder underneath; each agent gets the nearest level it accepts).
+ const EFFORT = [['', 'Effort: auto'], ['low', 'Quick'], ['medium', 'Balanced'], ['high', 'Thorough'], ['max', 'Deepest']];
+ const effortWord = (v) => (EFFORT.find(([k]) => k === v) || [null, v])[1];
+ function effortPicker(selected = '') {
+ return `<label class="ch-pick" title="How hard it thinks"><span class="ch-sr">Effort</span>${ico('sub')}<select name="effort" data-ch-effort>${EFFORT.map(([v, l]) => `<option value="${v}"${selected === v ? ' selected' : ''}>${l}</option>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
+ }
function composer({ action, api = '', placeholder, pickers = '', live = false, stopAction = '', hidden = '', note = '' }) {
function composer({ action, api = '', placeholder, pickers = '', live = false, stopAction = '', hidden = '', note = '' }) {
return `<form class="ch-composer" method="post" action="${esc(action)}"${api ? ` data-ch-api="${esc(api)}"` : ''} data-ch-composer>${hidden}
return `<form class="ch-composer" method="post" action="${esc(action)}"${api ? ` data-ch-api="${esc(api)}"` : ''} data-ch-composer>${hidden}
<label class="ch-sr" for="ch-text">${esc(placeholder)}</label>
<label class="ch-sr" for="ch-text">${esc(placeholder)}</label>
@@ -349,7 +356,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
@@ -349,7 +356,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
const error = String(req.query.error || '').slice(0, 200);
const error = String(req.query.error || '').slice(0, 200);
const name = String(req.viewer.displayName || req.viewer.username || '').split(' ')[0];
const name = String(req.viewer.displayName || req.viewer.username || '').split(' ')[0];
const ideas = ['Fix the failing tests', 'Explain how this project is laid out', 'Make the home page faster on phones', 'Find and fix one bug'];
const ideas = ['Fix the failing tests', 'Explain how this project is laid out', 'Make the home page faster on phones', 'Find and fix one bug'];
- const pickers = `<label class="ch-pick ch-pick-repo" title="Repository"><span class="ch-sr">Repository</span>${ico('repo')}<input name="repo" type="url" value="${esc(repo)}" placeholder="Repository URL" list="ch-repos" autocomplete="url" required data-ch-repo data-ch-repo-url="/api/v1/me/repos"><datalist id="ch-repos">${repos.map((x) => `<option value="${esc(x.url)}">${esc(x.full_name)}</option>`).join('')}</datalist></label>${agentPicker(settings.default_agent || '')}`;
+ const pickers = `<label class="ch-pick ch-pick-repo" title="Repository"><span class="ch-sr">Repository</span>${ico('repo')}<input name="repo" type="url" value="${esc(repo)}" placeholder="Repository URL" list="ch-repos" autocomplete="url" required data-ch-repo data-ch-repo-url="/api/v1/me/repos"><datalist id="ch-repos">${repos.map((x) => `<option value="${esc(x.url)}">${esc(x.full_name)}</option>`).join('')}</datalist></label>${agentPicker(settings.default_agent || '')}${effortPicker('')}`;
const thread = `<div class="ch-hello"><span class="ov-mark" data-size="56" aria-hidden="true"></span><h1>${name ? `What should we build, ${esc(name)}?` : 'What should we build?'}</h1><p>Say it the way you would to a colleague. Codes picks the agent, runs it on your machine, and shows you what changed.</p>${error ? `<p class="ch-error" role="alert">${esc(error)}</p>` : ''}<div class="ch-ideas" aria-label="Ideas">${ideas.map((t) => `<button class="ch-chip" type="button" data-ch-idea="${esc(t)}">${esc(t)}</button>`).join('')}</div>${repos.length > 1 ? `<div class="ch-ideas ch-recent" aria-label="Your recent repositories">${repos.map((x) => `<button class="ch-chip" type="button" data-ch-saved-repo="${esc(x.url)}">${ico('repo')}${esc(x.full_name)}</button>`).join('')}</div>` : ''}</div>`;
const thread = `<div class="ch-hello"><span class="ov-mark" data-size="56" aria-hidden="true"></span><h1>${name ? `What should we build, ${esc(name)}?` : 'What should we build?'}</h1><p>Say it the way you would to a colleague. Codes picks the agent, runs it on your machine, and shows you what changed.</p>${error ? `<p class="ch-error" role="alert">${esc(error)}</p>` : ''}<div class="ch-ideas" aria-label="Ideas">${ideas.map((t) => `<button class="ch-chip" type="button" data-ch-idea="${esc(t)}">${esc(t)}</button>`).join('')}</div>${repos.length > 1 ? `<div class="ch-ideas ch-recent" aria-label="Your recent repositories">${repos.map((x) => `<button class="ch-chip" type="button" data-ch-saved-repo="${esc(x.url)}">${ico('repo')}${esc(x.full_name)}</button>`).join('')}</div>` : ''}</div>`;
return frame(req, res, {
return frame(req, res, {
title: 'New chat', empty: true, head: '<h1 class="ch-title">New chat</h1>', thread,
title: 'New chat', empty: true, head: '<h1 class="ch-title">New chat</h1>', thread,
@@ -370,7 +377,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
@@ -370,7 +377,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
let args;
let args;
try {
try {
const { parseArgs } = require('../../harness/job');
const { parseArgs } = require('../../harness/job');
- args = parseArgs(JSON.stringify({ repo: String(b.repo || '').trim(), prompt: text, agent: agent || null, permission: 'edit' }));
+ args = parseArgs(JSON.stringify({ repo: String(b.repo || '').trim(), prompt: text, agent: agent || null, permission: 'edit', ...(b.effort ? { effort: String(b.effort) } : {}) }));
} catch (e) { return back(`Check the repository: ${e.message}.`); }
} catch (e) { return back(`Check the repository: ${e.message}.`); }
let project = null;
let project = null;
try { project = (await tasks.latestProject?.(req.viewer.subject)) || await conductor.projectFor(req.viewer.token || null, req.viewer.subject); } catch { project = null; }
try { project = (await tasks.latestProject?.(req.viewer.subject)) || await conductor.projectFor(req.viewer.token || null, req.viewer.subject); } catch { project = null; }
@@ -405,7 +412,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
@@ -405,7 +412,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
// The stream starts after the last line this page already shows.
// The stream starts after the last line this page already shows.
const seqRow = live ? await db.maybe('SELECT max(seq) AS seq FROM codes_session_lines WHERE task_id = $1', [row.id]) : null;
const seqRow = live ? await db.maybe('SELECT max(seq) AS seq FROM codes_session_lines WHERE task_id = $1', [row.id]) : null;
const lastSeq = seqRow && seqRow.seq != null ? Number(seqRow.seq) : 0;
const lastSeq = seqRow && seqRow.seq != null ? Number(seqRow.seq) : 0;
- const pickers = agentPicker('', { label: 'Who carries on', auto: `Same agent (${agentOf(row.agent, agentNames()).name})` });
+ const pickers = agentPicker('', { label: 'Who carries on', auto: `Same agent (${agentOf(row.agent, agentNames()).name})` }) + effortPicker('');
return frame(req, res, {
return frame(req, res, {
currentId: row.id, title: titleOf(row), head, side: await sideHtml(row),
currentId: row.id, title: titleOf(row), head, side: await sideHtml(row),
thread: (error ? `<p class="ch-error" role="alert">${esc(error)}</p>` : '') + await taskThread(row),
thread: (error ? `<p class="ch-error" role="alert">${esc(error)}</p>` : '') + await taskThread(row),
@@ -428,7 +435,8 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
@@ -428,7 +435,8 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
const b = req.body || {};
const b = req.body || {};
const agent = String(b.agent || '').trim() || null;
const agent = String(b.agent || '').trim() || null;
if (agent && !(harnesses?.list?.() || []).some((a) => a.id === agent)) return res.redirect(303, `/tasks/${encodeURIComponent(row.id)}?error=${encodeURIComponent('Choose an agent from the list.')}`);
if (agent && !(harnesses?.list?.() || []).some((a) => a.id === agent)) return res.redirect(303, `/tasks/${encodeURIComponent(row.id)}?error=${encodeURIComponent('Choose an agent from the list.')}`);
- const out = await tasks.continueTask(row, { note: String(b.text || '').slice(0, 4000), agent });
+ const effort = ['low', 'medium', 'high', 'xhigh', 'max'].includes(b.effort) ? b.effort : null;
+ const out = await tasks.continueTask(row, { note: String(b.text || '').slice(0, 4000), agent, effort });
return res.redirect(303, `/tasks/${encodeURIComponent(row.id)}${out.error ? `?error=${encodeURIComponent(out.error)}` : ''}`);
return res.redirect(303, `/tasks/${encodeURIComponent(row.id)}${out.error ? `?error=${encodeURIComponent(out.error)}` : ''}`);
});
});
@@ -28,7 +28,7 @@ function createTaskEvents({ store, config = {}, log = console, attempts = null }
@@ -28,7 +28,7 @@ function createTaskEvents({ store, config = {}, log = console, attempts = null }
const frame = (event, data, id = null) => (id == null ? '' : `id: ${id}\n`) + `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
const frame = (event, data, id = null) => (id == null ? '' : `id: ${id}\n`) + `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
const started = (a) => ({
const started = (a) => ({
- type: 'attempt_started', attempt_n: Number(a.n), id: a.id, agent: a.agent || null, model: a.model || null,
+ type: 'attempt_started', attempt_n: Number(a.n), id: a.id, agent: a.agent || null, model: a.model || null, effort: a.effort || null,
why: a.why || null, state: 'queued', at: a.created_at ? new Date(a.created_at).toISOString() : null,
why: a.why || null, state: 'queued', at: a.created_at ? new Date(a.created_at).toISOString() : null,
});
});
const ended = (a) => ({
const ended = (a) => ({
@@ -326,7 +326,10 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
@@ -326,7 +326,10 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
const note = typeof b.note === 'string' ? b.note.slice(0, 4000) : null;
const note = typeof b.note === 'string' ? b.note.slice(0, 4000) : null;
const agent = typeof b.agent === 'string' && b.agent ? b.agent : null;
const agent = typeof b.agent === 'string' && b.agent ? b.agent : null;
if (agent && !harnesses.list().some((a) => a.id === agent)) return problem(req, res, 422, 'codes.agent_unknown', 'agent is one of GET /api/v1/harnesses');
if (agent && !harnesses.list().some((a) => a.id === agent)) return problem(req, res, 422, 'codes.agent_unknown', 'agent is one of GET /api/v1/harnesses');
- const out = await taskStore.continueTask(row, { note, agent });
+ const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
+ const model = typeof b.model === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:\/#-]{0,99}$/.test(b.model) ? b.model : null;
+ const effort = EFFORTS.includes(b.effort) ? b.effort : null;
+ const out = await taskStore.continueTask(row, { note: note || (typeof b.text === 'string' ? b.text.slice(0, 4000) : null), agent, model, effort });
if (out.error) return problem(req, res, 409, 'codes.task_conflict', out.error);
if (out.error) return problem(req, res, 409, 'codes.task_conflict', out.error);
// One continuation at a time: an attempt that is already live answers this press, and no second one starts.
// One continuation at a time: an attempt that is already live answers this press, and no second one starts.
if (!out.live) log.info(`[Codes] task ${row.id} carried on (${agent ? `with ${agent}` : 'by the owner'})`);
if (!out.live) log.info(`[Codes] task ${row.id} carried on (${agent ? `with ${agent}` : 'by the owner'})`);
@@ -68,6 +68,16 @@ const { stepKind } = require('../server/http/chat');
@@ -68,6 +68,16 @@ const { stepKind } = require('../server/http/chat');
assert.ok(row && row.prompt === 'Make the footer quieter' && row.subject === ada.subject, 'the brief is the person’s own task');
assert.ok(row && row.prompt === 'Make the footer quieter' && row.subject === ada.subject, 'the brief is the person’s own task');
});
});
+ await check('the effort a person picks rides on the first attempt, and the thread says it in words', async () => {
+ const made = await t.get('/chat', { as: ada, form: { text: 'Think hard about the cache', repo: 'https://github.com/OpenVibers/OpenVibe.Live', effort: 'high' } });
+ assert.strictEqual(made.status, 303);
+ const id = made.headers.get('location').split('/').pop();
+ const att = await db.maybe('SELECT effort FROM codes_attempts WHERE task_id = $1 ORDER BY n LIMIT 1', [id]);
+ assert.ok(att && att.effort === 'high', `the attempt keeps it (${att && att.effort})`);
+ const home = await t.get('/', { as: ada });
+ assert.ok(home.text.includes('name="effort"') && home.text.includes('>Thorough</option>'), 'the composer offers it in plain words');
+ });
+
await check('a visitor who is not signed in gets the product’s home, not the app', async () => {
await check('a visitor who is not signed in gets the product’s home, not the app', async () => {
const page = await t.get('/');
const page = await t.get('/');
assert.strictEqual(page.status, 200);
assert.strictEqual(page.status, 200);
new file mode 100644
new file mode 100644
@@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
+'use strict';
+/**
+ * The reasoning effort a person picks for a turn (plan L31 CH2) reaches each CLI in its own words: Claude Code and
+ * Command Code take --effort, Codex its model_reasoning_effort setting, Aider --reasoning-effort, and OpenCode has no
+ * such flag. The level is fitted to what each one accepts (engine/adapters/base.js effortFor: Command Code has no
+ * "medium", Aider no "max"), and a turn without an effort passes none. The job input keeps only a level on the ladder.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { check, done } = require('./helpers/boot');
+const cli = require('../harness/adapters/cli');
+const { parseArgs } = require('../harness/job');
+
+// One stand-in for every CLI: it records its argv and says nothing (the adapter then reports a failed run).
+const FAKE = `
+const fs = require('fs');
+if (process.argv.includes('--version')) { console.log('9.9.9'); process.exit(0); }
+fs.writeFileSync(process.env.FAKE_RECORD, JSON.stringify({ argv: process.argv.slice(2) }));
+`;
+
+(async () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-effort-'));
+ const bin = path.join(dir, 'bin');
+ fs.mkdirSync(bin);
+ for (const name of ['claude', 'codex', 'cmd', 'aider', 'opencode']) fs.writeFileSync(path.join(bin, name), `#!${process.execPath}\n'use strict';\n${FAKE}\n`, { mode: 0o755 });
+ const record = path.join(dir, 'record.json');
+ const argvOf = async (adapter, effort) => {
+ fs.rmSync(record, { force: true });
+ for await (const e of adapter.start({ prompt: 'tidy the module', cwd: dir, permission: 'edit', ...(effort ? { effort } : {}) }, { env: { PATH: bin, FAKE_RECORD: record } })) void e;
+ return JSON.parse(fs.readFileSync(record, 'utf8')).argv;
+ };
+ const after = (argv, flag) => { const i = argv.indexOf(flag); return i < 0 ? null : argv[i + 1]; };
+ try {
+ await check('each CLI gets the effort in its own words', async () => {
+ assert.strictEqual(after(await argvOf(cli.claudeCode, 'high'), '--effort'), 'high', 'Claude Code');
+ const codex = await argvOf(cli.codex, 'high');
+ assert.ok(codex.includes('model_reasoning_effort="high"') && after(codex, '-c') === 'model_reasoning_effort="high"', `Codex: ${codex.join(' ')}`);
+ assert.strictEqual(after(await argvOf(cli.commandCode, 'high'), '--effort'), 'high', 'Command Code');
+ assert.strictEqual(after(await argvOf(cli.aider, 'high'), '--reasoning-effort'), 'high', 'Aider');
+ });
+
+ await check('a level a CLI does not have becomes the nearest one it does', async () => {
+ assert.strictEqual(after(await argvOf(cli.commandCode, 'medium'), '--effort'), 'high', 'Command Code has no medium: a tie goes up');
+ assert.strictEqual(after(await argvOf(cli.aider, 'max'), '--reasoning-effort'), 'high', 'Aider tops out at high');
+ });
+
+ await check('no effort, no flag; and OpenCode, which has none, is never given one', async () => {
+ const plain = await argvOf(cli.claudeCode, null);
+ assert.ok(!plain.includes('--effort'), 'Claude Code at its default');
+ assert.ok(!(await argvOf(cli.codex, null)).some((a) => /reasoning_effort/.test(a)), 'Codex at its default');
+ assert.ok(!(await argvOf(cli.opencode, 'max')).some((a) => /effort/.test(a)), 'OpenCode');
+ });
+
+ await check('the job input keeps an effort on the ladder and drops anything else', () => {
+ assert.strictEqual(parseArgs(JSON.stringify({ repo: 'https://github.com/a/b', prompt: 'x', effort: 'max' })).effort, 'max');
+ assert.strictEqual(parseArgs(JSON.stringify({ repo: 'https://github.com/a/b', prompt: 'x', effort: 'ludicrous' })).effort, undefined);
+ assert.strictEqual(parseArgs(JSON.stringify({ kind: 'conductor', folder: '/home/x', prompt: 'x', effort: 'low' })).effort, 'low');
+ });
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });