openvibers/openvibe.codes Commits

The OpenVibe.AI agent carries the run's effort and advisor option to AI (#361)

OP OpenVibers committed 3 days ago · 4f297a4e56 · 5 files, +36 −4

Changes

ddiff --git a/bin/openvibe-codes.js b/bin/openvibe-codes.js
iindex 9b7eb23..d4fdf70 100755
---- a/bin/openvibe-codes.js
++++ b/bin/openvibe-codes.js
@@@ -24,6 +24,11 @@ const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your term
--agent <id> skip routing: claude-code, claude-other, codex, opencode, command-code,
aider, minimax-code, deepseek, openai-compatible
--model <name> the first agent's model
++ --effort <level> reasoning effort for agents that take one (low|medium|high|max);
++ the OpenVibe.AI agent passes it to AI's run
++ --advisor let the OpenVibe.AI agent ask a stronger model for advice
++ mid-run (at most 3 uses; AI puts each use on the run's receipt)
++ --advisor-model <provider/model> the advisor model (default: AI's reviewer role, then its strongest)
--permission read|edit|full read: look and answer; edit: change files (default);
full: anything, unattended. OpenCode and Command Code have no
edits-only mode: here they need full (on a paired machine's
@@@ -52,6 +57,7 @@ function parse(argv) {
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--json') opts.json = true;
++ else if (a === '--advisor') opts.advisor = true;
else if (a === '--no-handoff') opts.handoff = false;
else if (a === '--help' || a === '-h') opts.help = true;
else if (a.startsWith('--')) { const k = a.slice(2); opts[k] = argv[++i]; }
@@@ -156,6 +162,7 @@ async function main() {
const run = harness.run({
prompt, task: o.task || (resume ? 'resume' : 'edit'), agent, model: o.model || null, cwd, resume,
permission: o.permission || 'edit', handoff: o.handoff !== false && !resume, maxAttempts: Number(o.attempts) || 3, signal: controller.signal,
++ effort: o.effort || null, advisor: o.advisor ? (o['advisor-model'] ? { model: o['advisor-model'] } : true) : null,
});
if (!o.json) out(dim(`run ${run.runId} · ${cwd}`));
let last = null;
ddiff --git a/harness/adapters/api.js b/harness/adapters/api.js
iindex 5b55661..ed1839c 100644
---- a/harness/adapters/api.js
++++ b/harness/adapters/api.js
@@@ -234,7 +234,11 @@ function apiAgent(spec) {
const res = await doFetch(`${base}/chat/completions`, {
method: 'POST',
headers: { 'content-type': 'application/json', ...(bearer ? { authorization: `Bearer ${bearer}` } : {}), ...extraHeaders(env) },
-- body: JSON.stringify({ ...(spec.extra ? spec.extra(env) : {}), model, messages, tools, tool_choice: 'auto' }),
++ body: JSON.stringify({ ...(spec.extra ? spec.extra(env) : {}), model, messages, tools, tool_choice: 'auto',
++ // Codes L31/AI AL3: the reasoning level the caller asked for, and the option to ask
++ // the AI router's advisor for guidance. AI ignores both when its own request does not
++ // support them; a harness that has no setting never sees them.
++ ...(t.effort ? { effort: t.effort } : {}), ...(t.advisor ? { advisor: t.advisor } : {}) }),
signal: controller.signal,
});
const body = await res.json().catch(() => null);
ddiff --git a/harness/index.js b/harness/index.js
iindex fadad35..bb331f2 100644
---- a/harness/index.js
++++ b/harness/index.js
@@@ -165,7 +165,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,
return decision;
}
-- 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, sandboxed = false } = {}) {
++ function run({ prompt, task = 'edit', agent = null, model = null, effort = null, advisor = null, cwd = process.cwd(), permission = 'edit', resume = null, handoff = true, maxAttempts = 3, stallMs = 10 * 60_000, timeoutMs, maxTurns, requirements = {}, signal, adapterOptions = null, sandboxed = false } = {}) {
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();
@@@ -213,7 +213,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, effort, resume: attempt === 1 ? resume : null, permission, sandboxed: sandboxed === true, maxTurns, timeoutMs, signal: controller.signal,
++ prompt: nextPrompt, cwd, model: modelFor, effort, advisor, resume: attempt === 1 ? resume : null, permission, sandboxed: sandboxed === true, 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 } : {}),
ddiff --git a/harness/job.js b/harness/job.js
iindex 89b7645..4813799 100644
---- a/harness/job.js
++++ b/harness/job.js
@@@ -182,6 +182,10 @@ function parseArgs(raw, env = process.env) {
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;
++ // AL3: a job may ask the OpenVibe.AI agent for an advisor; the caps and the model choice belong to AI.
++ const advisor = a.advisor === true ? true
++ : a.advisor && typeof a.advisor === 'object' && !Array.isArray(a.advisor) && (a.advisor.model == null || /^[A-Za-z0-9][A-Za-z0-9._:-]*\/[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,199}$/.test(String(a.advisor.model)))
++ ? (a.advisor.model ? { model: String(a.advisor.model) } : {}) : 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`);
@@@ -251,6 +255,7 @@ function parseArgs(raw, env = process.env) {
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 } : {}),
++ ...(advisor ? { advisor } : {}),
// Know K5: the lessons and facts earlier runs learned, from the server's own store and already a short block.
// It is prepended to the prompt in runJob, so every harness (CLI or API) gets it the same way.
...(typeof a.knowledge === 'string' && a.knowledge.trim() ? { knowledge: a.knowledge.trim().slice(0, 4000) } : {}),
@@@ -765,7 +770,8 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
say({ t: 'step', text: 'Preparing an empty benchmark workspace' });
}
const run = harness.run({
-- prompt: agentPrompt(job), task: 'edit', agent: job.agent, model: job.model, effort: job.effort || null, cwd: work,
++ prompt: agentPrompt(job), task: 'edit', agent: job.agent, model: job.model, effort: job.effort || null,
++ advisor: job.advisor || null, cwd: work,
permission: job.permission, handoff: !job.agent, maxAttempts: job.kind === 'conductor' ? 1 : 3,
// A job on the person's Node runs in its sandbox (own namespaces, a read-only root, no home folder, only
// what the job was given); the Link sets OPENVIBE_JOB_ID there. Only inside it may an agent with no
ddiff --git a/test/harness-api.test.js b/test/harness-api.test.js
iindex d76104a..5e9c43b 100644
---- a/test/harness-api.test.js
++++ b/test/harness-api.test.js
@@@ -212,6 +212,21 @@ const toolResults = (events) => events.filter((e) => e.type === 'user').flatMap(
assert.deepStrictEqual([input.file_path, input.content], ['note.txt', 'hello'], 'a new file shows what it holds');
});
++ await check('OpenVibe.AI: the run\'s effort and advisor are forwarded on every turn', async () => {
++ const forwardEnv = { ...env, OPENVIBE_AI_URL: api.url, OPENVIBE_AI_TOKEN: 'ov-task-token' };
++ const forward = createHarness({ env: forwardEnv, store: createStore({ env: forwardEnv }) });
++ api.set([{ content: 'done' }]);
++ const events = await collect(forward.run({ prompt: 'x', agent: 'openvibe-ai', cwd: work, permission: 'read', handoff: false, effort: 'low', advisor: { model: 'deepseek/deepseek-v4-pro' } }));
++ assert.strictEqual(events.find((e) => e.type === 'result').is_error, false);
++ assert.strictEqual(api.requests[0].body.effort, 'low');
++ assert.deepStrictEqual(api.requests[0].body.advisor, { model: 'deepseek/deepseek-v4-pro' });
++ assert.strictEqual(api.requests[0].body.model, 'openvibe');
++ api.set([{ content: 'done' }]);
++ await collect(forward.run({ prompt: 'x', agent: 'openvibe-ai', cwd: work, permission: 'read', handoff: false, advisor: true }));
++ assert.strictEqual(api.requests[0].body.advisor, true);
++ assert.ok(!('effort' in api.requests[0].body), 'no effort names no level');
++ });
++
await check('an HTTP error ends the run with the server\'s message', async () => {
api.set([{ status: 429, error: 'rate limited, slow down' }]);
const events = await collect(h.run({ prompt: 'x', agent: 'openai-compatible', cwd: work, handoff: false }));