Agent Surface Changes

This commit is contained in:
2026-08-18 15:58:42 -04:00
parent 2ad8ae1666
commit 183073e9b8
12 changed files with 1225 additions and 37 deletions
+353 -8
View File
@@ -5541,6 +5541,15 @@ function discordAgentReady(ctx, cfg) {
reason: 'REST backend has no API key. Run `agent --setup` (not stored in Discord).'
}
}
if (!String(cfg.rest_base_url || '').trim()) {
return {
ok: false,
backend: backend,
model: String(cfg.model || ''),
reason:
'REST backend has no base URL. Set `rest_base_url` in **/settings → Agent** or run `agent --setup`.'
}
}
return {
ok: true,
backend: backend,
@@ -5769,6 +5778,17 @@ async function discordAgentHud(ctx, interaction, note) {
? '**working**' + (run.currentTool ? ' · `' + run.currentTool + '`' : '')
: 'idle',
true
),
discordField(
'Plan mode',
cfg && cfg.plan_mode_active ? '**on**' : 'off',
true
),
discordField('Compaction', String((cfg && cfg.context_compaction) || 'auto'), true),
discordField(
'Autonomous',
cfg && cfg.autonomous_mode_enabled === false ? 'off' : 'on',
true
)
]
if (qvac) fields.push(discordField('QVAC', qvac, true))
@@ -5778,7 +5798,7 @@ async function discordAgentHud(ctx, interaction, note) {
fields.push(discordField('Identity', (label || 'agent') + (owner ? ' · ' + owner : ''), true))
}
let desc = ready.ok
? 'Talk to the guest **agent** (`~/.agent`). Same session as `/bin/agent` — history is shared.'
? 'Talk to the guest **agent** (`~/.agent`). Same session as `/bin/agent` — history, skills, todos, plan, and memory are shared.'
: ready.reason
if (run && run.prompt) desc += '\nCurrent: _' + String(run.prompt).slice(0, 180) + '_'
if (note) desc += '\n' + note
@@ -5792,8 +5812,32 @@ async function discordAgentHud(ctx, interaction, note) {
btns.push({ id: 'agent:models', label: 'Models' })
btns.push({ id: 'agent:status', label: 'Status' })
btns.push({ id: 'agent:config', label: 'Config' })
if (ready.ok) {
btns.push({ id: 'agent:recap', label: 'Recap' })
btns.push({ id: 'agent:compact', label: 'Compact' })
}
btns.push({ id: 'agent:refresh', label: 'Refresh' })
const rows = [discordButtons(btns.slice(0, 5)), discordButtons(btns.slice(5))].filter(Boolean)
const rows = []
for (let i = 0; i < btns.length; i += 5) {
const row = discordButtons(btns.slice(i, i + 5))
if (row) rows.push(row)
}
if (ready.ok) {
const inspect = discordSelect('agent:inspect', 'Inspect / session…', [
{ label: 'Skills catalog', value: 'skills', description: 'agent skills' },
{ label: 'Todos', value: 'todos', description: 'agent todos' },
{ label: 'Plan.md', value: 'plan', description: 'agent plan' },
{ label: 'History search…', value: 'history', description: 'agent history [query]' },
{ label: 'Recap last turn', value: 'recap', description: 'agent recap' },
{ label: 'Hooks', value: 'hooks', description: 'agent hooks' },
{ label: 'Undo last edit', value: 'undo', description: 'agent undo' },
{ label: 'Rewind last user turn', value: 'rewind', description: 'agent rewind 1' },
{ label: 'Compact history', value: 'compact', description: 'agent compact' },
{ label: 'Export transcript', value: 'export', description: 'agent export' },
{ label: 'Remember a fact…', value: 'remember', description: 'agent remember TEXT' }
])
if (inspect) rows.push(inspect)
}
return discordResult(
discordEmbed({
title: 'Agent',
@@ -5877,6 +5921,78 @@ async function discordAgentStop(ctx, interaction) {
return discordAgentHud(ctx, interaction, 'Stop requested. The in-flight turn may take a moment to unwind.')
}
var BARE_OS_DISCORD_AGENT_INSPECT = {
skills: { title: 'Skills', fence: true },
todos: { title: 'Todos', fence: false },
plan: { title: 'Plan', fence: false },
hooks: { title: 'Hooks', fence: true },
history: { title: 'History', fence: true },
recap: { title: 'Recap', fence: false },
undo: { title: 'Undo', fence: false },
rewind: { title: 'Rewind', fence: false },
compact: { title: 'Compact', fence: false },
export: { title: 'Export', fence: false },
remember: { title: 'Remember', fence: false }
}
async function discordAgentInspect(ctx, interaction, sub, extra) {
const name = String(sub || '').trim()
const spec = BARE_OS_DISCORD_AGENT_INSPECT[name]
if (!spec) return discordAgentHud(ctx, interaction, 'Unknown inspect command.')
if (name === 'remember' && !String(extra || '').trim()) {
return { modal: 'agent:remember' }
}
if (name === 'history' && extra === undefined) {
return { modal: 'agent:history' }
}
if (typeof ctx.execLine !== 'function') {
return { text: 'execLine unavailable — cannot run `agent ' + name + '`.', ephemeral: true }
}
const extraArg = extra == null ? '' : String(extra).trim()
let line = 'agent ' + name
if (extraArg) line += ' ' + discordShQuote(extraArg)
let out = ''
let errNote = ''
try {
out = await discordCmdCapture(ctx, function () {
return ctx.execLine(line, { timeoutMs: 45000 })
})
} catch (err) {
errNote = String((err && err.message) || err)
out = (out ? out + '\n' : '') + errNote
}
const raw = discordStripAnsi(String(out || '')).replace(/^\s+|\s+$/g, '')
const body = discordAgentHeadingsToBold(discordCmdRedact(raw || '(empty)'))
const comps = [
discordButtons([
{ id: 'agent:home', label: 'Agent' },
{ id: 'agent:ask', label: 'Ask…', style: 1 }
])
]
if (spec.fence) {
return discordResult(
discordEmbed({
title: 'Agent · ' + spec.title,
desc: discordCmdFence(body, '', 3500),
color: errNote ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
footer: 'guest /bin/agent ' + name
}),
{ components: comps }
)
}
return {
more: {
title: 'Agent · ' + spec.title,
lead: '',
body: body,
fence: false,
color: errNote ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
footer: 'guest /bin/agent ' + name,
components: comps
}
}
}
async function discordAgentAsk(ctx, interaction, prompt, flags) {
const task = String(prompt || '').trim()
if (!task) {
@@ -5890,6 +6006,10 @@ async function discordAgentAsk(ctx, interaction, prompt, flags) {
}
const wantNew = Boolean(flags && (flags.fresh || flags.new))
const wantPlan = Boolean(flags && flags.plan)
const wantAuto = Boolean(flags && (flags.auto || flags.autonomous))
const wantCompact = Boolean(flags && flags.compact)
const maxTurns = Math.max(0, Math.floor(Number(flags && flags.maxTurns) || 0))
const modelOverride = flags && flags.model ? String(flags.model).trim() : ''
const cfg = await discordAgentLoadConfig(ctx)
const ready = discordAgentReady(ctx, cfg)
if (!ready.ok) {
@@ -5983,6 +6103,10 @@ async function discordAgentAsk(ctx, interaction, prompt, flags) {
let line = 'agent'
if (wantNew) line += ' --new'
if (wantPlan) line += ' --plan'
if (wantAuto) line += ' --auto'
if (wantCompact) line += ' --compact'
if (maxTurns > 0) line += ' --max-turns ' + String(maxTurns)
if (modelOverride) line += ' --model ' + discordShQuote(modelOverride)
line += ' ' + discordShQuote(task)
return ctx.execLine(line, {
timeoutMs: BARE_OS_DISCORD_AGENT_MS
@@ -6057,10 +6181,21 @@ async function discordCmdHandleAgent(ctx, interaction, sub, opt) {
'Hidden until configured (QVAC model + host bridge, or REST + API key).',
fields: [
discordField('/agent', 'Interactive manager (default)'),
discordField('/agent ask', '**prompt** (required) — run one turn. Optional `new` / `plan`.'),
discordField('/agent status', 'Ready / backend / model / QVAC'),
discordField(
'/agent ask',
'**prompt** (required). Optional `new` / `plan` / `auto` / `compact` / `max_turns` / `model`.'
),
discordField('/agent status', 'Ready / backend / model / QVAC / plan / autonomous'),
discordField('/agent config', 'Non-secret ~/.agent/config.json'),
discordField('/agent models', 'QVAC catalog or live REST /models picker'),
discordField(
'/agent skills | todos | plan | hooks | history | recap',
'Same inspect surface as `/bin/agent`'
),
discordField(
'/agent undo | rewind | compact | export | remember',
'Session edits, rewind, compaction, transcript, MEMORY.md'
),
discordField('/agent reset', 'Clear history.json (keeps config)'),
discordField('/agent stop', 'Abort the in-flight turn')
]
@@ -6075,9 +6210,21 @@ async function discordCmdHandleAgent(ctx, interaction, sub, opt) {
if (s === 'ask') {
return discordAgentAsk(ctx, interaction, opt('prompt') || opt('text') || '', {
fresh: opt('new') === 'true',
plan: opt('plan') === 'true'
plan: opt('plan') === 'true',
auto: opt('auto') === 'true' || opt('autonomous') === 'true',
compact: opt('compact') === 'true',
maxTurns: opt('max_turns') || opt('max-turns'),
model: opt('model')
})
}
if (BARE_OS_DISCORD_AGENT_INSPECT[s]) {
let extra = ''
if (s === 'remember') extra = opt('text') || opt('note') || ''
else if (s === 'history') extra = opt('query') || opt('q') || ''
else if (s === 'rewind') extra = opt('steps') || opt('n') || ''
else if (s === 'export') extra = opt('path') || ''
return discordAgentInspect(ctx, interaction, s, extra)
}
return discordAgentHud(ctx, interaction, '')
}
@@ -6097,6 +6244,30 @@ async function discordAgentAction(ctx, interaction, id) {
if (id === 'agent:stop') {
return discordSendResult(ctx, interaction, await discordAgentStop(ctx, interaction))
}
if (id === 'agent:recap' || id === 'agent:compact' || id === 'agent:undo') {
return discordSendResult(
ctx,
interaction,
await discordAgentInspect(ctx, interaction, id.slice('agent:'.length), '')
)
}
if (id === 'agent:inspect') {
const which = String((interaction.values && interaction.values[0]) || '')
if (which === 'remember') {
return discordSendResult(ctx, interaction, { modal: 'agent:remember' })
}
if (which === 'history') {
return discordSendResult(ctx, interaction, { modal: 'agent:history' })
}
if (BARE_OS_DISCORD_AGENT_INSPECT[which]) {
return discordSendResult(
ctx,
interaction,
await discordAgentInspect(ctx, interaction, which, which === 'rewind' ? '1' : '')
)
}
return discordSendResult(ctx, interaction, await discordAgentHud(ctx, interaction, ''))
}
if (
id === 'agent:models' ||
id === 'agent:mdl' ||
@@ -6196,6 +6367,22 @@ function discordCmdBool(s, name, desc) {
})
}
function discordCmdInt(s, name, desc, extra) {
if (!s || typeof s.addIntegerOption !== 'function') {
return discordCmdOpt(s, name, desc, false)
}
return s.addIntegerOption(function (o) {
o.setName(name).setDescription(desc)
if (extra && extra.min != null && typeof o.setMinValue === 'function') {
o.setMinValue(Math.floor(Number(extra.min)))
}
if (extra && extra.max != null && typeof o.setMaxValue === 'function') {
o.setMaxValue(Math.floor(Number(extra.max)))
}
return o
})
}
function discordCmdAtt(s, name, desc, required) {
if (!s || typeof s.addAttachmentOption !== 'function') return s
return s.addAttachmentOption(function (o) {
@@ -6866,7 +7053,7 @@ function discordBuildSlashCommands(dj, ctx) {
.setDescription('Holesail tunnels (list, add, edit, start/stop, service)')
const agent = new B2()
.setName('agent')
.setDescription('Guest AI agent (ask, status, reset — when configured)')
.setDescription('Guest AI agent — full /bin/agent harness (ask, inspect, session)')
const ok =
discordCmdAddSubs(bare, [
['ping', 'Latency / liveness'],
@@ -7021,7 +7208,7 @@ function discordBuildSlashCommands(dj, ctx) {
discordCmdAddSubs(agent, [
['list', 'Interactive agent manager'],
['help', 'Agent command map'],
['status', 'Ready / backend / model / QVAC'],
['status', 'Ready / backend / model / QVAC / plan / autonomous'],
['config', 'Non-secret ~/.agent/config.json'],
['models', 'Pick QVAC or REST model'],
['ask', 'Run one agent turn', function (s) {
@@ -7035,6 +7222,29 @@ function discordBuildSlashCommands(dj, ctx) {
)
discordCmdBool(s, 'new', 'Start a fresh chat (clear history first)')
discordCmdBool(s, 'plan', 'Start in plan mode (read-only until exit_plan_mode)')
discordCmdBool(s, 'auto', 'Autonomous until task_complete / timebox')
discordCmdBool(s, 'compact', 'Compact history before this turn')
discordCmdInt(s, 'max_turns', 'Cap ReAct iterations for this run', { min: 1, max: 200 })
discordCmdOpt(s, 'model', 'Override QVAC / REST model for this run', false, false)
}],
['skills', 'List discovered SKILL.md ids'],
['todos', 'Print session todos'],
['plan', 'Print ~/.agent/plan.md'],
['hooks', 'List ~/.agent/hooks/*.json'],
['history', 'Search or tail chat history', function (s) {
discordCmdOpt(s, 'query', 'Optional keyword filter', false, false)
}],
['recap', 'Last user + assistant pair'],
['undo', 'Restore the last file edit snapshot'],
['rewind', 'Drop the last N user turns', function (s) {
discordCmdInt(s, 'steps', 'How many user turns to drop (default 1)', { min: 1, max: 50 })
}],
['compact', 'Compact history now'],
['export', 'Write a Markdown transcript', function (s) {
discordCmdOpt(s, 'path', 'Dest path (default ~/.agent/export.md)', false, false)
}],
['remember', 'Append a FACT to MEMORY.md', function (s) {
discordCmdOpt(s, 'text', 'What to remember', true, false, { min: 1, max: 1000 })
}],
['reset', 'Clear chat history (keeps config)'],
['stop', 'Abort the in-flight turn']
@@ -7834,6 +8044,76 @@ var BARE_OS_DISCORD_SETTINGS = [
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_emerg',
group: 'agent',
label: 'Emergency stop mutations',
kind: 'bool',
jsonKey: 'emergency_stop_mutations',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_nudge',
group: 'agent',
label: 'Todo nudge',
kind: 'bool',
jsonKey: 'todo_nudge_enabled',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_parallel',
group: 'agent',
label: 'Tool parallelism',
kind: 'enum',
jsonKey: 'tool_parallelism',
values: ['1', '2', '4'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_timeout',
group: 'agent',
label: 'Request timeout ms',
kind: 'enum',
jsonKey: 'request_timeout_ms',
values: ['60000', '120000', '180000', '300000'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_keep',
group: 'agent',
label: 'Compaction keep recent',
kind: 'enum',
jsonKey: 'compaction_keep_recent',
values: ['4', '8', '12', '16'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_gpulayers',
group: 'agent',
label: 'QVAC GPU layers',
kind: 'enum',
jsonKey: 'qvac_gpu_layers',
values: ['-1', '0', '8', '16', '32', '64'],
persist: 'agent',
backend: 'qvac',
live: 'next agent'
},
{
id: 'ag_maingpu',
group: 'agent',
label: 'QVAC main GPU',
kind: 'enum',
jsonKey: 'qvac_main_gpu',
values: ['auto', '0', '1', '2'],
persist: 'agent',
backend: 'qvac',
live: 'next agent'
},
{
id: 'irc_nick',
group: 'irc',
@@ -8171,7 +8451,14 @@ async function discordSettingsApply(ctx, spec, value) {
const cfg = await discordReadJsonFile(ctx, '~/.agent/config.json')
if (spec.jsonKey === 'backend') cfg.backend = String(v) === 'rest' ? 'rest' : 'qvac'
else if (spec.jsonKey === 'qvac_ctx_size') cfg.qvac_ctx_size = Number(v) || 0
else if (spec.jsonKey === 'qvac_profile') {
else if (
spec.jsonKey === 'tool_parallelism' ||
spec.jsonKey === 'request_timeout_ms' ||
spec.jsonKey === 'compaction_keep_recent' ||
spec.jsonKey === 'qvac_gpu_layers'
) {
cfg[spec.jsonKey] = Number(v)
} else if (spec.jsonKey === 'qvac_profile') {
cfg.qvac_profile = String(v)
const mapped = discordQvacModelForProfile(v)
if (mapped && mapped.id) {
@@ -8915,6 +9202,40 @@ async function discordSendResult(ctx, interaction, result) {
})
return
}
if (result && result.modal === 'agent:remember') {
await discordShowForm(interaction, {
id: 'agent:remember',
title: 'Remember a fact',
fields: [
{
id: 'text',
label: 'Fact to store in MEMORY.md',
paragraph: true,
placeholder: 'e.g. Holesail keys live in ~/.holesail',
min: 1,
max: 1000,
required: true
}
]
})
return
}
if (result && result.modal === 'agent:history') {
await discordShowForm(interaction, {
id: 'agent:history',
title: 'Search agent history',
fields: [
{
id: 'query',
label: 'Keyword (empty = count only)',
placeholder: 'holesail',
max: 200,
required: false
}
]
})
return
}
if (result && result.modal === 'agent:mdltype') {
await discordShowForm(interaction, {
id: 'agent:mdltype',
@@ -9523,6 +9844,20 @@ async function discordDispatchModal(ctx, interaction) {
await discordAgentAsk(ctx, interaction, field('prompt') || value)
)
}
if (id === 'agent:remember') {
return discordSendResult(
ctx,
interaction,
await discordAgentInspect(ctx, interaction, 'remember', field('text') || value)
)
}
if (id === 'agent:history') {
return discordSendResult(
ctx,
interaction,
await discordAgentInspect(ctx, interaction, 'history', field('query') || value)
)
}
if (id === 'agent:mdltype') {
const mid = field('model') || value
try {
@@ -9842,6 +10177,14 @@ async function discordDispatchInteraction(ctx, interaction) {
/* not a boolean option */
}
}
if (typeof interaction.options.getInteger === 'function') {
try {
const n = interaction.options.getInteger(key)
if (n != null) return String(n)
} catch {
/* not an integer option */
}
}
if (typeof interaction.options.getString === 'function') {
const v = interaction.options.getString(key)
return v == null ? '' : String(v)
@@ -9927,6 +10270,8 @@ var bareOsDiscordCommands = {
parseAgentProgress: discordAgentParseProgress,
splitAgentStdout: discordAgentSplitStdout,
formatAgentProcess: discordAgentFormatProcess,
inspectAgent: discordAgentInspect,
agentReady: discordAgentReady,
markdownPages: discordMarkdownPages,
settingsApply: discordSettingsApply,
settingsCurrent: discordSettingsCurrent,