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
@@ -218,6 +218,31 @@ function mockSlash() {
this.json.options.push({ type: 5, name: o.name })
return this
}
s.addIntegerOption = function (ofn) {
const o = {
name: '',
setName(n) {
this.name = n
return this
},
setDescription() {
return this
},
setRequired() {
return this
},
setMinValue() {
return this
},
setMaxValue() {
return this
}
}
ofn(o)
this.json.options = this.json.options || []
this.json.options.push({ type: 4, name: o.name })
return this
}
fn(s)
this.json.options.push({
type: 1,
@@ -1692,6 +1717,24 @@ test('Discord /agent is gated on config and runs the guest agent', async (t) =>
t.ok(agentCmd, 'slash /agent exists')
const subs = (agentCmd.options || []).filter((o) => o.type === 1).map((o) => o.name)
t.ok(subs.indexOf('ask') >= 0 && subs.indexOf('reset') >= 0 && subs.indexOf('status') >= 0)
for (const name of [
'skills',
'todos',
'plan',
'hooks',
'history',
'recap',
'undo',
'rewind',
'compact',
'export',
'remember',
'models',
'config',
'stop'
]) {
t.ok(subs.indexOf(name) >= 0, '/agent has ' + name)
}
const askSub = (agentCmd.options || []).find((o) => o.type === 1 && o.name === 'ask')
const askPrompt = ((askSub && askSub.options) || []).find((o) => o.name === 'prompt')
t.ok(askPrompt && askPrompt.required, '/agent ask prompt is required')
@@ -1700,6 +1743,31 @@ test('Discord /agent is gated on config and runs the guest agent', async (t) =>
((askSub && askSub.options) || []).some((o) => o.name === 'new' && o.type === 5),
'/agent ask has optional new flag'
)
t.ok(
((askSub && askSub.options) || []).some((o) => o.name === 'auto' && o.type === 5),
'/agent ask has optional auto flag'
)
t.ok(
((askSub && askSub.options) || []).some((o) => o.name === 'compact' && o.type === 5),
'/agent ask has optional compact flag'
)
t.ok(
((askSub && askSub.options) || []).some((o) => o.name === 'max_turns' && o.type === 4),
'/agent ask has max_turns'
)
const rememberSub = (agentCmd.options || []).find((o) => o.type === 1 && o.name === 'remember')
t.ok(
((rememberSub && rememberSub.options) || []).some((o) => o.name === 'text' && o.required),
'/agent remember text is required'
)
t.ok(
cmds.settingsSpecs.some((s) => s.jsonKey === 'emergency_stop_mutations'),
'settings expose emergency_stop_mutations'
)
t.ok(
cmds.settingsSpecs.some((s) => s.jsonKey === 'todo_nudge_enabled'),
'settings expose todo_nudge_enabled'
)
const views = []
const tree = {}
@@ -1793,6 +1861,18 @@ test('Discord /agent is gated on config and runs the guest agent', async (t) =>
),
'Models button on HUD'
)
t.ok(
(ready.components || []).some((row) =>
(row.components || []).some((c) => c.custom_id === 'agent:inspect')
),
'Inspect menu on HUD'
)
t.ok(
(ready.components || []).some((row) =>
(row.components || []).some((c) => c.custom_id === 'agent:recap')
),
'Recap button on HUD'
)
t.ok(cmds.qvacChatModels.some((m) => m.id === 'QWEN3_8B_INST_Q4_K_M'))
const parsed = cmds.parseOpenAiModels({
data: [{ id: 'grok-4' }, { id: 'text-embedding-3-small' }]
@@ -1892,8 +1972,10 @@ test('Discord /agent is gated on config and runs the guest agent', async (t) =>
commandName: 'agent',
options: {
getSubcommand: () => 'ask',
getString: (k) => (k === 'prompt' ? 'list /bin' : ''),
getBoolean: (k) => (k === 'new' ? true : k === 'plan' ? true : null)
getString: (k) => (k === 'prompt' ? 'list /bin' : k === 'model' ? 'QWEN3_4B_INST_Q4_K_M' : ''),
getBoolean: (k) =>
k === 'new' || k === 'plan' || k === 'auto' || k === 'compact' ? true : null,
getInteger: (k) => (k === 'max_turns' ? 12 : null)
},
user: { id: 'ag1' },
deferReply: async () => {
@@ -1908,8 +1990,17 @@ test('Discord /agent is gated on config and runs the guest agent', async (t) =>
})
t.ok(lines.some((l) => /agent /.test(l.line) && /list \/bin/.test(l.line)))
t.ok(
lines.some((l) => /--new/.test(l.line) && /--plan/.test(l.line) && /list \/bin/.test(l.line)),
'new + plan flags are passed through to /bin/agent'
lines.some(
(l) =>
/--new/.test(l.line) &&
/--plan/.test(l.line) &&
/--auto/.test(l.line) &&
/--compact/.test(l.line) &&
/--max-turns 12/.test(l.line) &&
/--model/.test(l.line) &&
/list \/bin/.test(l.line)
),
'new + plan + auto + compact + max-turns + model are passed through to /bin/agent'
)
t.ok(lines.some((l) => l.timeoutMs > 60000), 'agent timeout longer than /r')
t.ok(
@@ -1957,6 +2048,68 @@ test('Discord /agent is gated on config and runs the guest agent', async (t) =>
}
)
t.ok(denied[0] && /Access denied/.test(denied[0].content), 'whitelist still gates /agent')
const inspectLines = []
ctx.execLine = async (line) => {
inspectLines.push(String(line))
ctx.console.log('## skills\n| id | name |\n| --- | --- |\n| demo | Demo |')
ctx.exitCode = 0
}
const inspectViews = []
await cmds.dispatchInteraction(ctx, {
isChatInputCommand: () => true,
commandName: 'agent',
options: { getSubcommand: () => 'skills', getString: () => '' },
user: { id: 'ag1' },
reply: async (p) => {
inspectViews.push(p)
}
})
t.ok(
inspectLines.some((l) => /^agent skills$/.test(l)),
'skills inspect calls guest /bin/agent'
)
t.ok(/demo/.test(JSON.stringify(inspectViews[0] || {})), 'skills inspect shows output')
await cmds.dispatchInteraction(ctx, {
isChatInputCommand: () => true,
commandName: 'agent',
options: {
getSubcommand: () => 'remember',
getString: (k) => (k === 'text' ? 'holesail keys live in ~/.holesail' : '')
},
user: { id: 'ag1' },
reply: async (p) => {
inspectViews.push(p)
}
})
t.ok(
inspectLines.some((l) => /agent remember/.test(l) && /holesail keys/.test(l)),
'remember inspect quotes the fact'
)
await cmds.dispatchInteraction(ctx, {
isChatInputCommand: () => true,
commandName: 'agent',
options: {
getSubcommand: () => 'rewind',
getString: () => '',
getInteger: (k) => (k === 'steps' ? 2 : null)
},
user: { id: 'ag1' },
reply: async (p) => {
inspectViews.push(p)
}
})
t.ok(
inspectLines.some((l) => /agent rewind/.test(l) && /['"]?2['"]?/.test(l)),
'rewind passes steps'
)
tree['~/.agent/config.json'] =
'{"backend":"rest","provider":"groq","rest_api_key":"gsk","model":"x"}\n'
const restNoUrl = cmds.agentReady(ctx, JSON.parse(tree['~/.agent/config.json']))
t.absent(restNoUrl.ok, 'REST without base URL is not ready')
})
test('Discord /run captures stdout, raw writes, and writeScreen (no TTY leak)', async (t) => {