Update Model Selection
Release rolling / release (push) Successful in 15m21s

This commit is contained in:
2026-08-18 18:59:14 -04:00
parent db6dfa22e3
commit 4eb767d65e
12 changed files with 312 additions and 39 deletions
@@ -124,6 +124,67 @@ function bareAgentParseOpenAiModels(json) {
return out
}
/**
* Keep an explicit QVAC model id (Discord / `--model` / picker).
* Profile defaults only apply when no model is set.
* @param {Record<string, unknown>} config
* @param {{ chatModel?: string }} [profile]
* @returns {Record<string, unknown>}
*/
function bareAgentHonorExplicitQvacModel(config, profile) {
const out = config && typeof config === 'object' ? config : {}
const explicit = String(out.qvac_model || out.model || '').trim()
if (explicit) {
out.qvac_model = explicit
out.model = explicit
const card =
typeof bareAgentQvacFindChatModel === 'function'
? bareAgentQvacFindChatModel(explicit)
: null
if (card && card.profile) out.qvac_profile = card.profile
return out
}
const fallback = String((profile && profile.chatModel) || '').trim()
if (fallback) {
out.qvac_model = fallback
out.model = fallback
}
return out
}
/**
* Persist a live model switch for the current backend.
* @param {Record<string, unknown>} config
* @param {string} modelId
* @returns {Record<string, unknown>}
*/
function bareAgentApplyLiveModel(config, modelId) {
const out = { ...(config && typeof config === 'object' ? config : {}) }
const id = String(modelId || '').trim()
if (!id) return out
let backend = 'qvac'
if (typeof bareAgentResolveBackend === 'function') {
backend = bareAgentResolveBackend(out)
} else {
const b = String(out.backend || '').trim().toLowerCase()
const p = String(out.provider || '').trim().toLowerCase()
if (b === 'rest' || b === 'openai' || b === 'http') backend = 'rest'
else if (b === 'qvac') backend = 'qvac'
else if (p === 'groq' || p === 'xai' || p === 'openai' || p === 'custom') backend = 'rest'
else if (out.rest_api_key && String(out.rest_api_key).trim()) backend = 'rest'
}
if (backend === 'qvac') {
out.backend = 'qvac'
out.provider = 'qvac'
out.qvac_model = id
out.model = id
return bareAgentHonorExplicitQvacModel(out, null)
}
out.backend = 'rest'
out.model = id
return out
}
/**
* @param {string} [provider]
*/
@@ -1036,7 +1036,11 @@ async function bareAgentDispatchTool(o) {
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
return bareAgentJsonResult({ ok: false, error: 'bad patch' })
}
const merged = bareAgentMergeConfigPatch(configRef.current, patch)
let merged = bareAgentMergeConfigPatch(configRef.current, patch)
const liveId = String(patch.qvac_model || patch.model || '').trim()
if (liveId && typeof bareAgentApplyLiveModel === 'function') {
merged = bareAgentApplyLiveModel(merged, liveId)
}
configRef.current = merged
await bareAgentSaveConfigFromTools(ctx, paths, merged)
if (
@@ -455,9 +455,13 @@ function bareAgentApplyProviderProfile(cfg) {
if (!profileId) profileId = 'recommended'
const profile = bareAgentQvacGetProfile(profileId)
out.qvac_profile = profile.id
out.qvac_model = profile.chatModel
out.model = profile.chatModel
out.provider = 'qvac'
if (typeof bareAgentHonorExplicitQvacModel === 'function') {
bareAgentHonorExplicitQvacModel(out, profile)
} else {
out.qvac_model = profile.chatModel
out.model = profile.chatModel
}
const rawCtx = Number(out.qvac_ctx_size)
if (!Number.isFinite(rawCtx) || rawCtx < profile.ctxSize) {
out.qvac_ctx_size = 0
@@ -1220,9 +1224,14 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
if (runOpts && runOpts.modelOverride) {
const m = String(runOpts.modelOverride).trim()
if (m) {
const backendNow = bareAgentResolveBackend(config)
if (backendNow === 'qvac') config.qvac_model = m
else config.model = m
config =
typeof bareAgentApplyLiveModel === 'function'
? bareAgentApplyLiveModel(config, m)
: Object.assign(config, {
qvac_model:
bareAgentResolveBackend(config) === 'qvac' ? m : config.qvac_model,
model: m
})
}
}
@@ -1260,6 +1269,24 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
})
config = bareAgentApplyProviderProfile(config)
}
if (runOpts && runOpts.modelOverride) {
const m = String(runOpts.modelOverride).trim()
if (m) {
config =
typeof bareAgentApplyLiveModel === 'function'
? bareAgentApplyLiveModel(config, m)
: Object.assign(config, {
qvac_model:
bareAgentResolveBackend(config) === 'qvac' ? m : config.qvac_model,
model: m
})
try {
await bareAgentSaveConfig(ctx, paths, config)
} catch {
/* persist best-effort so the next ask uses the same model */
}
}
}
const backendReady = bareAgentResolveBackend(config)
if (backendReady === 'rest') {
@@ -1883,7 +1910,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
(providerNow || 'unknown') +
' model=' +
String(
configRef.current.model || configRef.current.qvac_model || ''
configRef.current.qvac_model || configRef.current.model || ''
) +
EDIT_ANSI_RESET +
'\n'
@@ -8,9 +8,9 @@
"agent --status",
"agent --plan REQUEST...",
"agent --auto GOAL...",
"agent skills | todos | plan | compact | reset | undo | hooks | history | rewind | export | remember | recap"
"agent skills | todos | plan | compact | reset | undo | hooks | history | rewind | export | remember | recap | models [ID]"
],
"description": "Runs the in-guest ReAct coding agent. Default backend is QVAC (local ctx.bareOsQvac*); REST (OpenAI-compatible) is optional. Full guest admin by default (denylist): the read-only base system is /bin /etc /boot /lib /usr /share /proc /dev /sys /run. Tools include grep (output_mode), apply_patch, web_search, git helpers, find_symbol, copy_path, diff_files, create_skill, session rewind/export, todos, plan mode, and walk-up project skills. History lives in ~/.agent/history.json and is shared with Discord /agent. Inspect subcommands (skills, todos, plan, undo, hooks, history, rewind, export, remember, recap) do not call the model.",
"description": "Runs the in-guest ReAct coding agent. Default backend is QVAC (local ctx.bareOsQvac*); REST (OpenAI-compatible) is optional. Full guest admin by default (denylist): the read-only base system is /bin /etc /boot /lib /usr /share /proc /dev /sys /run. Tools include grep (output_mode), apply_patch, web_search, git helpers, find_symbol, copy_path, diff_files, create_skill, session rewind/export, todos, plan mode, and walk-up project skills. History lives in ~/.agent/history.json and is shared with Discord /agent. Model switches persist: --model, agent models ID, Discord /agent models, and Settings → Agent. The next turn loads that id (QVAC unloads the previous GGUF). Inspect subcommands (skills, todos, plan, undo, hooks, history, rewind, export, remember, recap, models) do not call the model unless you pass models ID.",
"options": [
{
"flag": "--setup, --config",
@@ -42,7 +42,7 @@
},
{
"flag": "--model NAME",
"meaning": "Override QVAC or REST model for this run"
"meaning": "Switch QVAC or REST model now and persist to ~/.agent/config.json"
},
{
"flag": "--system FILE",
+1 -1
View File
@@ -7,7 +7,7 @@
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
"scripts": {
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
"test": "node ./test/clear-sequence.test.mjs && node ./test/init-discord.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/edit-tui-sdk.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-tui-sdk.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.test.mjs && node ./test/irc-parse.test.mjs && node ./test/irc-client.test.mjs && node ./test/irc-dial.test.mjs && node ./test/irc-commands.test.mjs && node ./test/irc-tui-sdk.test.mjs && node ./test/summon-engine.test.mjs && node ./test/summon-tui-sdk.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-test-bracket-argv.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/hardening.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-tools-run-command.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/agent-grok-port.test.mjs && node ./test/agent-trim-ctx.test.mjs && node ./test/agent-qvac.test.mjs && node ./test/agent-models.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/p2p-suite-tui-sdk.test.mjs && node ./test/chat-tui-sdk.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
"test": "node ./test/clear-sequence.test.mjs && node ./test/init-discord.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/edit-tui-sdk.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-tui-sdk.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.test.mjs && node ./test/irc-parse.test.mjs && node ./test/irc-client.test.mjs && node ./test/irc-dial.test.mjs && node ./test/irc-commands.test.mjs && node ./test/irc-tui-sdk.test.mjs && node ./test/summon-engine.test.mjs && node ./test/summon-tui-sdk.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-test-bracket-argv.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/hardening.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-tools-run-command.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/agent-grok-port.test.mjs && node ./test/agent-trim-ctx.test.mjs && node ./test/agent-qvac.test.mjs && node ./test/agent-models.test.mjs && node ./test/agent-live-model.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/p2p-suite-tui-sdk.test.mjs && node ./test/chat-tui-sdk.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
"perf:baretop": "node ./test/baretop-perf-baseline.test.mjs"
}
}
+61 -4
View File
@@ -65,11 +65,13 @@ async function run(ctx, argv) {
sub === 'rewind' ||
sub === 'export' ||
sub === 'remember' ||
sub === 'recap')) ||
sub === 'recap' ||
sub === 'models')) ||
(sub === 'history' && rest[0] === 'history') ||
(sub === 'rewind' && rest[0] === 'rewind') ||
(sub === 'export' && rest[0] === 'export') ||
(sub === 'remember' && rest[0] === 'remember'))
(sub === 'remember' && rest[0] === 'remember') ||
(sub === 'models' && rest[0] === 'models'))
) {
if (sub === 'status') statusFlag = true
else if (sub === 'reset') resetFlag = true
@@ -189,7 +191,7 @@ function bareAgentCliUsage(argv0) {
' --compact\n' +
' ' +
argv0 +
' skills | todos | plan | undo | hooks | history | rewind | export | remember | recap\n' +
' skills | todos | plan | undo | hooks | history | rewind | export | remember | recap | models [ID]\n' +
'\n' +
'Production coding/OS agent. Default backend is QVAC (local); or any OpenAI-compatible REST API.\n' +
'Config: ~/.agent/config.json. Full guest admin (denylist). NEVER ASK — it just works.\n' +
@@ -202,7 +204,7 @@ function bareAgentCliUsage(argv0) {
' --continue, -c Resume history (default)\n' +
' --compact Compact history now (or compact then run if a task follows)\n' +
' --max-turns N Cap ReAct iterations for this run\n' +
' --model NAME Override qvac_model / REST model for this run\n' +
' --model NAME Switch QVAC / REST model now and persist to ~/.agent/config.json\n' +
' --system FILE Append extra instructions from an absolute guest path\n' +
' --status Print backend, compaction, plan mode, and autonomous run state\n' +
'\n' +
@@ -217,6 +219,7 @@ function bareAgentCliUsage(argv0) {
' export [path] Write a Markdown transcript (default ~/.agent/export.md)\n' +
' remember TEXT Append a FACT to MEMORY.md\n' +
' recap Print the last user + assistant pair\n' +
' models [ID] List catalog / current, or persist a live model switch\n' +
'\n' +
'Examples:\n' +
' ' +
@@ -467,6 +470,60 @@ async function bareAgentRunInspectSubcommand(ctx, argv0, sub, extra) {
ctx.exitCode = 0
return
}
if (sub === 'models') {
const loaded = await bareAgentLoadOrCreateConfig(ctx, paths)
let cfg = loaded && loaded.config ? loaded.config : loaded || {}
const want = String(extra || '').trim()
if (want) {
cfg =
typeof bareAgentApplyLiveModel === 'function'
? bareAgentApplyLiveModel(cfg, want)
: Object.assign(cfg, { qvac_model: want, model: want })
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
cfg = bareAgentSanitizeConfigForBackend(cfg)
}
await bareAgentSaveConfig(ctx, paths, cfg)
const backend =
typeof bareAgentResolveBackend === 'function'
? bareAgentResolveBackend(cfg)
: String(cfg.backend || 'qvac')
const used =
backend === 'qvac'
? String(cfg.qvac_model || cfg.model || want)
: String(cfg.model || want)
ctx.console.log(argv0 + ': now using ' + backend + ' · ' + used)
ctx.exitCode = 0
return
}
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
cfg = bareAgentSanitizeConfigForBackend(cfg)
}
const backend =
typeof bareAgentResolveBackend === 'function'
? bareAgentResolveBackend(cfg)
: String(cfg.backend || 'qvac')
const current =
backend === 'qvac'
? String(cfg.qvac_model || cfg.model || '')
: String(cfg.model || '')
const lines = [
'backend: ' + backend,
'current: ' + (current || '(unset)')
]
if (backend === 'qvac' && typeof BARE_AGENT_QVAC_CHAT_MODELS !== 'undefined') {
lines.push('catalog:')
for (let i = 0; i < BARE_AGENT_QVAC_CHAT_MODELS.length; i++) {
const m = BARE_AGENT_QVAC_CHAT_MODELS[i]
const mark = m.id === current ? '* ' : ' '
lines.push(mark + m.id + (m.label ? ' — ' + m.label : ''))
}
} else {
lines.push('set with: ' + argv0 + ' models <id> or ' + argv0 + ' --model <id>')
}
ctx.console.log(lines.join('\n'))
ctx.exitCode = 0
return
}
} catch (e) {
const msg =
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
@@ -240,8 +240,13 @@ test('agent CLI exposes production flags and inspect subcommands', async (t) =>
t.ok(CLI.includes("sub === 'export'"))
t.ok(CLI.includes("sub === 'remember'"))
t.ok(CLI.includes("sub === 'recap'"))
t.ok(CLI.includes("sub === 'models'"))
t.ok(CLI.includes("sub === 'models' && rest[0] === 'models'"))
t.ok(CLI.includes('bareAgentApplyLiveModel'))
t.ok(TUI.includes('planMode'))
t.ok(TUI.includes('modelOverride'))
t.ok(TUI.includes('bareAgentHonorExplicitQvacModel'))
t.ok(TUI.includes('bareAgentSaveConfig'))
t.ok(TUI.includes('extraSystemFile'))
})
@@ -0,0 +1,74 @@
import test from 'brittle'
import { readFileSync } from 'node:fs'
import vm from 'node:vm'
const QVAC_SRC = readFileSync(new URL('../lib/agent/agent-qvac.js', import.meta.url), 'utf8')
const MODELS_SRC = readFileSync(
new URL('../lib/agent/agent-models.js', import.meta.url),
'utf8'
)
const TUI_SRC = readFileSync(new URL('../lib/agent/agent-tui.js', import.meta.url), 'utf8')
function load() {
const sandbox = { console }
vm.createContext(sandbox)
vm.runInContext(
QVAC_SRC +
'\n' +
MODELS_SRC +
'\n' +
TUI_SRC +
'\n;this.__exports = { bareAgentHonorExplicitQvacModel, bareAgentApplyLiveModel, bareAgentApplyProviderProfile, bareAgentQvacGetProfile, bareAgentQvacFindChatModel }',
sandbox
)
return sandbox.__exports
}
test('explicit QVAC model survives profile apply (live Discord/CLI switch)', async (t) => {
const { bareAgentApplyProviderProfile } = load()
const next = bareAgentApplyProviderProfile({
backend: 'qvac',
qvac_profile: 'recommended',
qvac_model: 'QWEN3_8B_INST_Q4_K_M',
model: 'QWEN3_8B_INST_Q4_K_M'
})
t.is(next.qvac_model, 'QWEN3_8B_INST_Q4_K_M')
t.is(next.model, 'QWEN3_8B_INST_Q4_K_M')
})
test('empty QVAC model fills from profile', async (t) => {
const { bareAgentApplyProviderProfile, bareAgentQvacGetProfile } = load()
const next = bareAgentApplyProviderProfile({
backend: 'qvac',
qvac_profile: 'strong'
})
t.is(next.qvac_model, bareAgentQvacGetProfile('strong').chatModel)
})
test('apply live model on QVAC sets both keys and profile when catalog-known', async (t) => {
const { bareAgentApplyLiveModel } = load()
const next = bareAgentApplyLiveModel(
{ backend: 'qvac', qvac_profile: 'recommended', qvac_model: 'QWEN3_1_7B_INST_Q4' },
'QWEN3_600M_INST_Q4'
)
t.is(next.qvac_model, 'QWEN3_600M_INST_Q4')
t.is(next.model, 'QWEN3_600M_INST_Q4')
t.is(next.qvac_profile, 'lite')
})
test('apply live model on REST only changes model', async (t) => {
const { bareAgentApplyLiveModel } = load()
const next = bareAgentApplyLiveModel(
{
backend: 'rest',
provider: 'groq',
rest_api_key: 'gsk',
rest_base_url: 'https://api.groq.com/openai/v1',
model: 'old'
},
'llama-3.1-8b-instant'
)
t.is(next.backend, 'rest')
t.is(next.model, 'llama-3.1-8b-instant')
t.absent(next.qvac_model)
})
@@ -56,3 +56,40 @@ test('fetch REST models uses /models and falls back on empty', async (t) => {
)
t.ok(s.bareAgentRestModelsFallback('groq').some((m) => m.id === 'llama-3.3-70b-versatile'))
})
test('honor explicit QVAC model does not fall back to profile default', async (t) => {
const s = load()
const next = s.bareAgentHonorExplicitQvacModel(
{
qvac_profile: 'recommended',
qvac_model: 'QWEN3_8B_INST_Q4_K_M',
model: 'QWEN3_8B_INST_Q4_K_M'
},
{ chatModel: 'QWEN3_1_7B_INST_Q4' }
)
t.is(next.qvac_model, 'QWEN3_8B_INST_Q4_K_M')
t.is(next.model, 'QWEN3_8B_INST_Q4_K_M')
})
test('apply live model switches QVAC and REST independently', async (t) => {
const s = load()
const qvac = s.bareAgentApplyLiveModel(
{ backend: 'qvac', qvac_profile: 'recommended', qvac_model: 'QWEN3_1_7B_INST_Q4' },
'QWEN3_600M_INST_Q4'
)
t.is(qvac.qvac_model, 'QWEN3_600M_INST_Q4')
t.is(qvac.qvac_profile, 'lite')
const rest = s.bareAgentApplyLiveModel(
{
backend: 'rest',
provider: 'groq',
rest_api_key: 'gsk',
rest_base_url: 'https://api.groq.com/openai/v1',
model: 'old'
},
'llama-3.1-8b-instant'
)
t.is(rest.backend, 'rest')
t.is(rest.model, 'llama-3.1-8b-instant')
})