openvibers/openvibe.codes Commits
The OpenVibe.AI agent carries the run's effort and advisor option to AI (#361)
Changes
@@@ -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 noedits-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;@@@ -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);@@@ -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 } : {}),@@@ -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, tmsay({ 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@@@ -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 }));
@@ -24,6 +24,11 @@ const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your term
@@ -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,
--agent <id> skip routing: claude-code, claude-other, codex, opencode, command-code,
aider, minimax-code, deepseek, openai-compatible
aider, minimax-code, deepseek, openai-compatible
--model <name> the first agent's model
--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);
--permission read|edit|full read: look and answer; edit: change files (default);
full: anything, unattended. OpenCode and Command Code have no
full: anything, unattended. OpenCode and Command Code have no
edits-only mode: here they need full (on a paired machine's
edits-only mode: here they need full (on a paired machine's
@@ -52,6 +57,7 @@ function parse(argv) {
@@ -52,6 +57,7 @@ function parse(argv) {
for (let i = 0; i < argv.length; i++) {
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const a = argv[i];
if (a === '--json') opts.json = true;
if (a === '--json') opts.json = true;
+ else if (a === '--advisor') opts.advisor = true;
else if (a === '--no-handoff') opts.handoff = false;
else if (a === '--no-handoff') opts.handoff = false;
else if (a === '--help' || a === '-h') opts.help = true;
else if (a === '--help' || a === '-h') opts.help = true;
else if (a.startsWith('--')) { const k = a.slice(2); opts[k] = argv[++i]; }
else if (a.startsWith('--')) { const k = a.slice(2); opts[k] = argv[++i]; }
@@ -156,6 +162,7 @@ async function main() {
@@ -156,6 +162,7 @@ async function main() {
const run = harness.run({
const run = harness.run({
prompt, task: o.task || (resume ? 'resume' : 'edit'), agent, model: o.model || null, cwd, resume,
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,
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}`));
if (!o.json) out(dim(`run ${run.runId} · ${cwd}`));
let last = null;
let last = null;
@@ -234,7 +234,11 @@ function apiAgent(spec) {
@@ -234,7 +234,11 @@ function apiAgent(spec) {
const res = await doFetch(`${base}/chat/completions`, {
const res = await doFetch(`${base}/chat/completions`, {
method: 'POST',
method: 'POST',
headers: { 'content-type': 'application/json', ...(bearer ? { authorization: `Bearer ${bearer}` } : {}), ...extraHeaders(env) },
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,
signal: controller.signal,
});
});
const body = await res.json().catch(() => null);
const body = await res.json().catch(() => null);
@@ -165,7 +165,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,
@@ -165,7 +165,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,
return decision;
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 (!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();
@@ -213,7 +213,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,
@@ -213,7 +213,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, 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
// 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 } : {}),
@@ -182,6 +182,10 @@ function parseArgs(raw, env = process.env) {
@@ -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');
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 EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
const effort = EFFORTS.includes(a.effort) ? a.effort : null;
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 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`);
@@ -251,6 +255,7 @@ function parseArgs(raw, env = process.env) {
@@ -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,
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 } : {}),
...(effort ? { effort } : {}),
+ ...(advisor ? { advisor } : {}),
// Know K5: the lessons and facts earlier runs learned, from the server's own store and already a short block.
// 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.
// 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) } : {}),
...(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
@@ -765,7 +770,8 @@ 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: 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,
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
// 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
// what the job was given); the Link sets OPENVIBE_JOB_ID there. Only inside it may an agent with no
@@ -212,6 +212,21 @@ const toolResults = (events) => events.filter((e) => e.type === 'user').flatMap(
@@ -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');
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 () => {
await check('an HTTP error ends the run with the server\'s message', async () => {
api.set([{ status: 429, error: 'rate limited, slow down' }]);
api.set([{ status: 429, error: 'rate limited, slow down' }]);
const events = await collect(h.run({ prompt: 'x', agent: 'openai-compatible', cwd: work, handoff: false }));
const events = await collect(h.run({ prompt: 'x', agent: 'openai-compatible', cwd: work, handoff: false }));