@visorcraft/idlehands 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bot/commands.js +149 -0
- package/dist/bot/commands.js.map +1 -1
- package/dist/bot/discord.js +686 -15
- package/dist/bot/discord.js.map +1 -1
- package/dist/bot/session-manager.js +131 -2
- package/dist/bot/session-manager.js.map +1 -1
- package/dist/bot/telegram.js +250 -6
- package/dist/bot/telegram.js.map +1 -1
- package/dist/cli/commands/model.js +99 -0
- package/dist/cli/commands/model.js.map +1 -1
- package/dist/cli/commands/session.js +1 -1
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/client.js +7 -0
- package/dist/client.js.map +1 -1
- package/dist/upgrade.js +41 -3
- package/dist/upgrade.js.map +1 -1
- package/package.json +1 -1
package/dist/bot/discord.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Client, Events, GatewayIntentBits, Partials, } from 'discord.js';
|
|
1
|
+
import { Client, Events, GatewayIntentBits, Partials, REST, Routes, SlashCommandBuilder, } from 'discord.js';
|
|
2
2
|
import { createSession } from '../agent.js';
|
|
3
3
|
import { DiscordConfirmProvider } from './confirm-discord.js';
|
|
4
4
|
import { sanitizeBotOutputText } from './format.js';
|
|
@@ -40,13 +40,150 @@ function safeContent(text) {
|
|
|
40
40
|
const t = sanitizeBotOutputText(text).trim();
|
|
41
41
|
return t.length ? t : '(empty response)';
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Check if the model response contains an escalation request.
|
|
45
|
+
* Returns { escalate: true, reason: string } if escalation marker found at start of response.
|
|
46
|
+
*/
|
|
47
|
+
function detectEscalation(text) {
|
|
48
|
+
const trimmed = text.trim();
|
|
49
|
+
const match = trimmed.match(/^\[ESCALATE:\s*([^\]]+)\]/i);
|
|
50
|
+
if (match) {
|
|
51
|
+
return { escalate: true, reason: match[1].trim() };
|
|
52
|
+
}
|
|
53
|
+
return { escalate: false };
|
|
54
|
+
}
|
|
55
|
+
/** Keyword presets for common escalation triggers */
|
|
56
|
+
const KEYWORD_PRESETS = {
|
|
57
|
+
coding: ['build', 'implement', 'create', 'develop', 'architect', 'refactor', 'debug', 'fix', 'code', 'program', 'write'],
|
|
58
|
+
planning: ['plan', 'design', 'roadmap', 'strategy', 'analyze', 'research', 'evaluate', 'compare'],
|
|
59
|
+
complex: ['full', 'complete', 'comprehensive', 'multi-step', 'integrate', 'migration', 'overhaul', 'entire', 'whole'],
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Check if text matches a set of keywords.
|
|
63
|
+
* Returns matched keywords or empty array if none match.
|
|
64
|
+
*/
|
|
65
|
+
function matchKeywords(text, keywords, presets) {
|
|
66
|
+
const allKeywords = [...keywords];
|
|
67
|
+
// Add preset keywords
|
|
68
|
+
if (presets) {
|
|
69
|
+
for (const preset of presets) {
|
|
70
|
+
const presetWords = KEYWORD_PRESETS[preset];
|
|
71
|
+
if (presetWords)
|
|
72
|
+
allKeywords.push(...presetWords);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (allKeywords.length === 0)
|
|
76
|
+
return [];
|
|
77
|
+
const lowerText = text.toLowerCase();
|
|
78
|
+
const matched = [];
|
|
79
|
+
for (const kw of allKeywords) {
|
|
80
|
+
if (kw.startsWith('re:')) {
|
|
81
|
+
// Regex pattern
|
|
82
|
+
try {
|
|
83
|
+
const regex = new RegExp(kw.slice(3), 'i');
|
|
84
|
+
if (regex.test(text))
|
|
85
|
+
matched.push(kw);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Invalid regex, skip
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Word boundary match (case-insensitive)
|
|
93
|
+
const wordRegex = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
|
94
|
+
if (wordRegex.test(lowerText))
|
|
95
|
+
matched.push(kw);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return matched;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if user message matches keyword escalation triggers.
|
|
102
|
+
* Returns { escalate: true, tier: number, reason: string } if keywords match.
|
|
103
|
+
* Tier indicates which model index to escalate to (highest matching tier wins).
|
|
104
|
+
*/
|
|
105
|
+
function checkKeywordEscalation(text, escalation) {
|
|
106
|
+
if (!escalation)
|
|
107
|
+
return { escalate: false };
|
|
108
|
+
// Tiered keyword escalation
|
|
109
|
+
if (escalation.tiers && escalation.tiers.length > 0) {
|
|
110
|
+
let highestTier = -1;
|
|
111
|
+
let highestReason = '';
|
|
112
|
+
// Check each tier, highest matching tier wins
|
|
113
|
+
for (let i = 0; i < escalation.tiers.length; i++) {
|
|
114
|
+
const tier = escalation.tiers[i];
|
|
115
|
+
const matched = matchKeywords(text, tier.keywords || [], tier.keyword_presets);
|
|
116
|
+
if (matched.length > 0 && i > highestTier) {
|
|
117
|
+
highestTier = i;
|
|
118
|
+
highestReason = `tier ${i} keyword match: ${matched.slice(0, 3).join(', ')}${matched.length > 3 ? '...' : ''}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (highestTier >= 0) {
|
|
122
|
+
return { escalate: true, tier: highestTier, reason: highestReason };
|
|
123
|
+
}
|
|
124
|
+
return { escalate: false };
|
|
125
|
+
}
|
|
126
|
+
// Legacy flat keywords (treated as tier 0)
|
|
127
|
+
const matched = matchKeywords(text, escalation.keywords || [], escalation.keyword_presets);
|
|
128
|
+
if (matched.length > 0) {
|
|
129
|
+
return {
|
|
130
|
+
escalate: true,
|
|
131
|
+
tier: 0,
|
|
132
|
+
reason: `keyword match: ${matched.slice(0, 3).join(', ')}${matched.length > 3 ? '...' : ''}`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { escalate: false };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolve which agent persona should handle a message.
|
|
139
|
+
* Priority: user > channel > guild > default > first agent > null
|
|
140
|
+
*/
|
|
141
|
+
function resolveAgentForMessage(msg, agents, routing) {
|
|
142
|
+
const agentMap = agents ?? {};
|
|
143
|
+
const agentIds = Object.keys(agentMap);
|
|
144
|
+
// No agents configured — return null persona (use global config)
|
|
145
|
+
if (agentIds.length === 0) {
|
|
146
|
+
return { agentId: '_default', persona: null };
|
|
147
|
+
}
|
|
148
|
+
const route = routing ?? {};
|
|
149
|
+
let resolvedId;
|
|
150
|
+
// Priority 1: User-specific routing
|
|
151
|
+
if (route.users && route.users[msg.author.id]) {
|
|
152
|
+
resolvedId = route.users[msg.author.id];
|
|
153
|
+
}
|
|
154
|
+
// Priority 2: Channel-specific routing
|
|
155
|
+
else if (route.channels && route.channels[msg.channelId]) {
|
|
156
|
+
resolvedId = route.channels[msg.channelId];
|
|
157
|
+
}
|
|
158
|
+
// Priority 3: Guild-specific routing
|
|
159
|
+
else if (msg.guildId && route.guilds && route.guilds[msg.guildId]) {
|
|
160
|
+
resolvedId = route.guilds[msg.guildId];
|
|
161
|
+
}
|
|
162
|
+
// Priority 4: Default agent
|
|
163
|
+
else if (route.default) {
|
|
164
|
+
resolvedId = route.default;
|
|
165
|
+
}
|
|
166
|
+
// Priority 5: First defined agent
|
|
167
|
+
else {
|
|
168
|
+
resolvedId = agentIds[0];
|
|
169
|
+
}
|
|
170
|
+
// Validate the resolved agent exists
|
|
171
|
+
const persona = agentMap[resolvedId];
|
|
172
|
+
if (!persona) {
|
|
173
|
+
// Fallback to first agent if routing points to non-existent agent
|
|
174
|
+
const fallbackId = agentIds[0];
|
|
175
|
+
return { agentId: fallbackId, persona: agentMap[fallbackId] ?? null };
|
|
176
|
+
}
|
|
177
|
+
return { agentId: resolvedId, persona };
|
|
178
|
+
}
|
|
179
|
+
function sessionKeyForMessage(msg, allowGuilds, agentId) {
|
|
180
|
+
// Include agentId in session key so switching agents creates a new session
|
|
44
181
|
if (allowGuilds) {
|
|
45
|
-
// Per-channel+user session in guilds
|
|
46
|
-
return `${msg.channelId}:${msg.author.id}`;
|
|
182
|
+
// Per-agent+channel+user session in guilds
|
|
183
|
+
return `${agentId}:${msg.channelId}:${msg.author.id}`;
|
|
47
184
|
}
|
|
48
|
-
// DM-only mode
|
|
49
|
-
return msg.author.id
|
|
185
|
+
// DM-only mode: per-agent+user session
|
|
186
|
+
return `${agentId}:${msg.author.id}`;
|
|
50
187
|
}
|
|
51
188
|
export async function startDiscordBot(config, botConfig) {
|
|
52
189
|
const token = process.env.IDLEHANDS_DISCORD_TOKEN || botConfig.token;
|
|
@@ -83,7 +220,9 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
83
220
|
return await msg.channel.send(content);
|
|
84
221
|
};
|
|
85
222
|
async function getOrCreate(msg) {
|
|
86
|
-
|
|
223
|
+
// Resolve which agent should handle this message
|
|
224
|
+
const { agentId, persona } = resolveAgentForMessage(msg, botConfig.agents, botConfig.routing);
|
|
225
|
+
const key = sessionKeyForMessage(msg, allowGuilds, agentId);
|
|
87
226
|
const existing = sessions.get(key);
|
|
88
227
|
if (existing) {
|
|
89
228
|
existing.lastActivity = Date.now();
|
|
@@ -92,11 +231,44 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
92
231
|
if (sessions.size >= maxSessions) {
|
|
93
232
|
return null;
|
|
94
233
|
}
|
|
234
|
+
// Build config with agent-specific overrides
|
|
235
|
+
const agentDir = persona?.default_dir || persona?.allowed_dirs?.[0] || defaultDir;
|
|
236
|
+
const agentApproval = persona?.approval_mode
|
|
237
|
+
? normalizeApprovalMode(persona.approval_mode, approvalMode)
|
|
238
|
+
: approvalMode;
|
|
239
|
+
// Build system prompt with escalation instructions if configured
|
|
240
|
+
let systemPrompt = persona?.system_prompt;
|
|
241
|
+
if (persona?.escalation?.models?.length && persona?.escalation?.auto !== false) {
|
|
242
|
+
const escalationModels = persona.escalation.models.join(', ');
|
|
243
|
+
const escalationInstructions = `
|
|
244
|
+
|
|
245
|
+
[AUTO-ESCALATION]
|
|
246
|
+
You have access to more powerful models when needed: ${escalationModels}
|
|
247
|
+
If you encounter a task that is too complex, requires deeper reasoning, or you're struggling to solve,
|
|
248
|
+
you can escalate by including this exact marker at the START of your response:
|
|
249
|
+
[ESCALATE: brief reason]
|
|
250
|
+
|
|
251
|
+
Examples:
|
|
252
|
+
- [ESCALATE: complex algorithm requiring multi-step reasoning]
|
|
253
|
+
- [ESCALATE: need larger context window for this codebase analysis]
|
|
254
|
+
- [ESCALATE: struggling with this optimization problem]
|
|
255
|
+
|
|
256
|
+
Only escalate when genuinely needed. Most tasks should be handled by your current model.
|
|
257
|
+
When you escalate, your request will be re-run on a more capable model.`;
|
|
258
|
+
systemPrompt = (systemPrompt || '') + escalationInstructions;
|
|
259
|
+
}
|
|
95
260
|
const cfg = {
|
|
96
261
|
...config,
|
|
97
|
-
dir:
|
|
98
|
-
approval_mode:
|
|
99
|
-
no_confirm:
|
|
262
|
+
dir: agentDir,
|
|
263
|
+
approval_mode: agentApproval,
|
|
264
|
+
no_confirm: agentApproval === 'yolo',
|
|
265
|
+
// Agent-specific overrides
|
|
266
|
+
...(persona?.model && { model: persona.model }),
|
|
267
|
+
...(persona?.endpoint && { endpoint: persona.endpoint }),
|
|
268
|
+
...(systemPrompt && { system_prompt_override: systemPrompt }),
|
|
269
|
+
...(persona?.max_tokens && { max_tokens: persona.max_tokens }),
|
|
270
|
+
...(persona?.temperature !== undefined && { temperature: persona.temperature }),
|
|
271
|
+
...(persona?.top_p !== undefined && { top_p: persona.top_p }),
|
|
100
272
|
};
|
|
101
273
|
const confirmProvider = new DiscordConfirmProvider(msg.channel, msg.author.id, botConfig.confirm_timeout_sec ?? 300);
|
|
102
274
|
const session = await createSession({
|
|
@@ -107,6 +279,8 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
107
279
|
const managed = {
|
|
108
280
|
key,
|
|
109
281
|
userId: msg.author.id,
|
|
282
|
+
agentId,
|
|
283
|
+
agentPersona: persona,
|
|
110
284
|
channel: msg.channel,
|
|
111
285
|
session,
|
|
112
286
|
confirmProvider,
|
|
@@ -122,8 +296,15 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
122
296
|
antonAbortSignal: null,
|
|
123
297
|
antonLastResult: null,
|
|
124
298
|
antonProgress: null,
|
|
299
|
+
currentModelIndex: 0,
|
|
300
|
+
escalationCount: 0,
|
|
301
|
+
pendingEscalation: null,
|
|
125
302
|
};
|
|
126
303
|
sessions.set(key, managed);
|
|
304
|
+
// Log agent assignment for debugging
|
|
305
|
+
if (persona) {
|
|
306
|
+
console.error(`[bot:discord] ${msg.author.id} → agent:${agentId} (${persona.display_name || agentId})`);
|
|
307
|
+
}
|
|
127
308
|
return managed;
|
|
128
309
|
}
|
|
129
310
|
function destroySession(key) {
|
|
@@ -196,10 +377,83 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
196
377
|
return { ok: true, message: '⏹ Cancel requested. Stopping current turn...' };
|
|
197
378
|
}
|
|
198
379
|
async function processMessage(managed, msg) {
|
|
199
|
-
|
|
380
|
+
let turn = beginTurn(managed);
|
|
200
381
|
if (!turn)
|
|
201
382
|
return;
|
|
202
|
-
|
|
383
|
+
let turnId = turn.turnId;
|
|
384
|
+
// Handle pending escalation - switch model before processing
|
|
385
|
+
if (managed.pendingEscalation) {
|
|
386
|
+
const targetModel = managed.pendingEscalation;
|
|
387
|
+
managed.pendingEscalation = null;
|
|
388
|
+
// Find the model index in escalation chain
|
|
389
|
+
const escalation = managed.agentPersona?.escalation;
|
|
390
|
+
if (escalation?.models) {
|
|
391
|
+
const idx = escalation.models.indexOf(targetModel);
|
|
392
|
+
if (idx !== -1) {
|
|
393
|
+
managed.currentModelIndex = idx + 1; // +1 because 0 is base model
|
|
394
|
+
managed.escalationCount += 1;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Recreate session with escalated model
|
|
398
|
+
const cfg = {
|
|
399
|
+
...managed.config,
|
|
400
|
+
model: targetModel,
|
|
401
|
+
};
|
|
402
|
+
try {
|
|
403
|
+
await recreateSession(managed, cfg);
|
|
404
|
+
console.error(`[bot:discord] ${managed.userId} escalated to ${targetModel}`);
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
console.error(`[bot:discord] escalation failed: ${e?.message ?? e}`);
|
|
408
|
+
// Continue with current model if escalation fails
|
|
409
|
+
}
|
|
410
|
+
// Re-acquire turn after recreation - must update turnId!
|
|
411
|
+
const newTurn = beginTurn(managed);
|
|
412
|
+
if (!newTurn)
|
|
413
|
+
return;
|
|
414
|
+
turn = newTurn;
|
|
415
|
+
turnId = newTurn.turnId;
|
|
416
|
+
}
|
|
417
|
+
// Check for keyword-based escalation BEFORE calling the model
|
|
418
|
+
// Allow escalation to higher tiers even if already escalated
|
|
419
|
+
const escalation = managed.agentPersona?.escalation;
|
|
420
|
+
if (escalation?.models?.length) {
|
|
421
|
+
const kwResult = checkKeywordEscalation(msg.content, escalation);
|
|
422
|
+
if (kwResult.escalate && kwResult.tier !== undefined) {
|
|
423
|
+
// Use the tier to select the target model (tier 0 → models[0], tier 1 → models[1], etc.)
|
|
424
|
+
const targetModelIndex = Math.min(kwResult.tier, escalation.models.length - 1);
|
|
425
|
+
// Only escalate if target tier is higher than current (currentModelIndex 0 = base, 1 = models[0], etc.)
|
|
426
|
+
const currentTier = managed.currentModelIndex - 1; // -1 because 0 is base model
|
|
427
|
+
if (targetModelIndex > currentTier) {
|
|
428
|
+
const targetModel = escalation.models[targetModelIndex];
|
|
429
|
+
// Get endpoint from tier if defined
|
|
430
|
+
const tierEndpoint = escalation.tiers?.[targetModelIndex]?.endpoint;
|
|
431
|
+
console.error(`[bot:discord] ${managed.userId} keyword escalation: ${kwResult.reason} → ${targetModel}${tierEndpoint ? ` @ ${tierEndpoint}` : ''}`);
|
|
432
|
+
// Set up escalation
|
|
433
|
+
managed.currentModelIndex = targetModelIndex + 1; // +1 because 0 is base model
|
|
434
|
+
managed.escalationCount += 1;
|
|
435
|
+
// Recreate session with escalated model and optional endpoint override
|
|
436
|
+
const cfg = {
|
|
437
|
+
...managed.config,
|
|
438
|
+
model: targetModel,
|
|
439
|
+
...(tierEndpoint && { endpoint: tierEndpoint }),
|
|
440
|
+
};
|
|
441
|
+
try {
|
|
442
|
+
await recreateSession(managed, cfg);
|
|
443
|
+
// Re-acquire turn after recreation - must update turnId!
|
|
444
|
+
const newTurn = beginTurn(managed);
|
|
445
|
+
if (!newTurn)
|
|
446
|
+
return;
|
|
447
|
+
turn = newTurn;
|
|
448
|
+
turnId = newTurn.turnId;
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
console.error(`[bot:discord] keyword escalation failed: ${e?.message ?? e}`);
|
|
452
|
+
// Continue with current model if escalation fails
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
203
457
|
const placeholder = await sendUserVisible(msg, '⏳ Thinking...').catch(() => null);
|
|
204
458
|
let streamed = '';
|
|
205
459
|
const hooks = {
|
|
@@ -225,6 +479,42 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
225
479
|
return;
|
|
226
480
|
markProgress(managed, turnId);
|
|
227
481
|
const finalText = safeContent(streamed || result.text);
|
|
482
|
+
// Check for auto-escalation request in response
|
|
483
|
+
const escalation = managed.agentPersona?.escalation;
|
|
484
|
+
const autoEscalate = escalation?.auto !== false && escalation?.models?.length;
|
|
485
|
+
const maxEscalations = escalation?.max_escalations ?? 2;
|
|
486
|
+
if (autoEscalate && managed.escalationCount < maxEscalations) {
|
|
487
|
+
const escResult = detectEscalation(finalText);
|
|
488
|
+
if (escResult.escalate) {
|
|
489
|
+
// Determine next model in escalation chain
|
|
490
|
+
const nextIndex = Math.min(managed.currentModelIndex, escalation.models.length - 1);
|
|
491
|
+
const targetModel = escalation.models[nextIndex];
|
|
492
|
+
// Get endpoint from tier if defined
|
|
493
|
+
const tierEndpoint = escalation.tiers?.[nextIndex]?.endpoint;
|
|
494
|
+
console.error(`[bot:discord] ${managed.userId} auto-escalation requested: ${escResult.reason}${tierEndpoint ? ` @ ${tierEndpoint}` : ''}`);
|
|
495
|
+
// Update placeholder with escalation notice
|
|
496
|
+
if (placeholder) {
|
|
497
|
+
await placeholder.edit(`⚡ Escalating to \`${targetModel}\` (${escResult.reason})...`).catch(() => { });
|
|
498
|
+
}
|
|
499
|
+
// Set up escalation for re-run
|
|
500
|
+
managed.pendingEscalation = targetModel;
|
|
501
|
+
managed.currentModelIndex = nextIndex + 1;
|
|
502
|
+
managed.escalationCount += 1;
|
|
503
|
+
// Recreate session with escalated model and optional endpoint override
|
|
504
|
+
const cfg = {
|
|
505
|
+
...managed.config,
|
|
506
|
+
model: targetModel,
|
|
507
|
+
...(tierEndpoint && { endpoint: tierEndpoint }),
|
|
508
|
+
};
|
|
509
|
+
await recreateSession(managed, cfg);
|
|
510
|
+
// Finish this turn and re-run with escalated model
|
|
511
|
+
clearInterval(watchdog);
|
|
512
|
+
finishTurn(managed, turnId);
|
|
513
|
+
// Re-process the original message with the escalated model
|
|
514
|
+
await processMessage(managed, msg);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
228
518
|
const chunks = splitDiscord(finalText);
|
|
229
519
|
if (placeholder) {
|
|
230
520
|
await placeholder.edit(chunks[0]).catch(() => { });
|
|
@@ -264,6 +554,23 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
264
554
|
finally {
|
|
265
555
|
clearInterval(watchdog);
|
|
266
556
|
finishTurn(managed, turnId);
|
|
557
|
+
// Auto-deescalate back to base model after each request
|
|
558
|
+
if (managed.currentModelIndex > 0 && managed.agentPersona?.escalation) {
|
|
559
|
+
const baseModel = managed.agentPersona.model || config.model || 'default';
|
|
560
|
+
managed.currentModelIndex = 0;
|
|
561
|
+
managed.escalationCount = 0;
|
|
562
|
+
const cfg = {
|
|
563
|
+
...managed.config,
|
|
564
|
+
model: baseModel,
|
|
565
|
+
};
|
|
566
|
+
try {
|
|
567
|
+
await recreateSession(managed, cfg);
|
|
568
|
+
console.error(`[bot:discord] ${managed.userId} auto-deescalated to ${baseModel}`);
|
|
569
|
+
}
|
|
570
|
+
catch (e) {
|
|
571
|
+
console.error(`[bot:discord] auto-deescalation failed: ${e?.message ?? e}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
267
574
|
const next = managed.pendingQueue.shift();
|
|
268
575
|
if (next && managed.state === 'idle' && !managed.inFlight) {
|
|
269
576
|
setTimeout(() => {
|
|
@@ -281,6 +588,8 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
281
588
|
managed.activeAbortController?.abort();
|
|
282
589
|
}
|
|
283
590
|
catch { }
|
|
591
|
+
// Preserve conversation history before destroying the old session
|
|
592
|
+
const oldMessages = managed.session.messages.slice();
|
|
284
593
|
try {
|
|
285
594
|
managed.session.cancel();
|
|
286
595
|
}
|
|
@@ -290,6 +599,15 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
290
599
|
confirmProvider: managed.confirmProvider,
|
|
291
600
|
confirm: async () => true,
|
|
292
601
|
});
|
|
602
|
+
// Restore conversation history to the new session
|
|
603
|
+
if (oldMessages.length > 0) {
|
|
604
|
+
try {
|
|
605
|
+
session.restore(oldMessages);
|
|
606
|
+
}
|
|
607
|
+
catch (e) {
|
|
608
|
+
console.error(`[bot:discord] Failed to restore ${oldMessages.length} messages after escalation:`, e);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
293
611
|
managed.session = session;
|
|
294
612
|
managed.config = cfg;
|
|
295
613
|
managed.inFlight = false;
|
|
@@ -298,7 +616,7 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
298
616
|
managed.lastProgressAt = 0;
|
|
299
617
|
managed.lastActivity = Date.now();
|
|
300
618
|
}
|
|
301
|
-
client.on(Events.ClientReady, () => {
|
|
619
|
+
client.on(Events.ClientReady, async () => {
|
|
302
620
|
console.error(`[bot:discord] Connected as ${client.user?.tag ?? 'unknown'}`);
|
|
303
621
|
console.error(`[bot:discord] Allowed users: [${[...allowedUsers].join(', ')}]`);
|
|
304
622
|
console.error(`[bot:discord] Default dir: ${defaultDir}`);
|
|
@@ -306,6 +624,233 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
306
624
|
if (allowGuilds) {
|
|
307
625
|
console.error(`[bot:discord] Guild mode enabled${guildId ? ` (guild ${guildId})` : ''}`);
|
|
308
626
|
}
|
|
627
|
+
// Log multi-agent config
|
|
628
|
+
const agents = botConfig.agents;
|
|
629
|
+
if (agents && Object.keys(agents).length > 0) {
|
|
630
|
+
const agentIds = Object.keys(agents);
|
|
631
|
+
console.error(`[bot:discord] Multi-agent mode: ${agentIds.length} agents configured [${agentIds.join(', ')}]`);
|
|
632
|
+
const routing = botConfig.routing;
|
|
633
|
+
if (routing?.default) {
|
|
634
|
+
console.error(`[bot:discord] Default agent: ${routing.default}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Register slash commands
|
|
638
|
+
try {
|
|
639
|
+
const commands = [
|
|
640
|
+
new SlashCommandBuilder().setName('help').setDescription('Show available commands'),
|
|
641
|
+
new SlashCommandBuilder().setName('new').setDescription('Start a new session'),
|
|
642
|
+
new SlashCommandBuilder().setName('status').setDescription('Show session statistics'),
|
|
643
|
+
new SlashCommandBuilder().setName('agent').setDescription('Show current agent info'),
|
|
644
|
+
new SlashCommandBuilder().setName('agents').setDescription('List all configured agents'),
|
|
645
|
+
new SlashCommandBuilder().setName('cancel').setDescription('Cancel the current operation'),
|
|
646
|
+
new SlashCommandBuilder().setName('reset').setDescription('Reset the session'),
|
|
647
|
+
new SlashCommandBuilder().setName('escalate').setDescription('Escalate to a larger model')
|
|
648
|
+
.addStringOption(option => option.setName('model').setDescription('Model name or "next"').setRequired(false)),
|
|
649
|
+
new SlashCommandBuilder().setName('deescalate').setDescription('Return to base model'),
|
|
650
|
+
].map(cmd => cmd.toJSON());
|
|
651
|
+
const rest = new REST({ version: '10' }).setToken(token);
|
|
652
|
+
// Register globally (takes up to 1 hour to propagate) or per-guild (instant)
|
|
653
|
+
if (guildId) {
|
|
654
|
+
await rest.put(Routes.applicationGuildCommands(client.user.id, guildId), { body: commands });
|
|
655
|
+
console.error(`[bot:discord] Registered ${commands.length} slash commands for guild ${guildId}`);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
await rest.put(Routes.applicationCommands(client.user.id), { body: commands });
|
|
659
|
+
console.error(`[bot:discord] Registered ${commands.length} global slash commands`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch (e) {
|
|
663
|
+
console.error(`[bot:discord] Failed to register slash commands: ${e?.message ?? e}`);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
// Handle slash command interactions
|
|
667
|
+
client.on(Events.InteractionCreate, async (interaction) => {
|
|
668
|
+
if (!interaction.isChatInputCommand())
|
|
669
|
+
return;
|
|
670
|
+
if (!allowedUsers.has(interaction.user.id)) {
|
|
671
|
+
await interaction.reply({ content: '⚠️ You are not authorized to use this bot.', ephemeral: true });
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const cmd = interaction.commandName;
|
|
675
|
+
// Create a fake message object with enough properties to work with existing handlers
|
|
676
|
+
const fakeMsg = {
|
|
677
|
+
author: interaction.user,
|
|
678
|
+
channel: interaction.channel,
|
|
679
|
+
channelId: interaction.channelId,
|
|
680
|
+
guildId: interaction.guildId,
|
|
681
|
+
content: `/${cmd}`,
|
|
682
|
+
reply: async (content) => {
|
|
683
|
+
if (interaction.replied || interaction.deferred) {
|
|
684
|
+
return await interaction.followUp(content);
|
|
685
|
+
}
|
|
686
|
+
return await interaction.reply(content);
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
// Defer reply for commands that might take a while
|
|
690
|
+
if (cmd === 'status' || cmd === 'agent' || cmd === 'agents') {
|
|
691
|
+
await interaction.deferReply();
|
|
692
|
+
}
|
|
693
|
+
// Resolve agent for this interaction
|
|
694
|
+
const { agentId, persona } = resolveAgentForMessage(fakeMsg, botConfig.agents, botConfig.routing);
|
|
695
|
+
const key = sessionKeyForMessage(fakeMsg, allowGuilds, agentId);
|
|
696
|
+
switch (cmd) {
|
|
697
|
+
case 'help': {
|
|
698
|
+
const lines = [
|
|
699
|
+
'**IdleHands Commands**',
|
|
700
|
+
'',
|
|
701
|
+
'/help — This message',
|
|
702
|
+
'/new — Start fresh session',
|
|
703
|
+
'/status — Session stats',
|
|
704
|
+
'/agent — Show current agent',
|
|
705
|
+
'/agents — List all configured agents',
|
|
706
|
+
'/cancel — Abort running task',
|
|
707
|
+
'/reset — Full session reset',
|
|
708
|
+
];
|
|
709
|
+
await interaction.reply(lines.join('\n'));
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
case 'new': {
|
|
713
|
+
destroySession(key);
|
|
714
|
+
const agentName = persona?.display_name || agentId;
|
|
715
|
+
const agentMsg = persona ? ` (agent: ${agentName})` : '';
|
|
716
|
+
await interaction.reply(`✨ New session started${agentMsg}. Send a message to begin.`);
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
case 'status': {
|
|
720
|
+
const managed = sessions.get(key);
|
|
721
|
+
if (!managed) {
|
|
722
|
+
await interaction.editReply('No active session.');
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
const lines = [
|
|
726
|
+
`**Session:** ${managed.key}`,
|
|
727
|
+
`**Agent:** ${managed.agentPersona?.display_name || managed.agentId}`,
|
|
728
|
+
`**Model:** ${managed.config.model ?? 'default'}`,
|
|
729
|
+
`**State:** ${managed.state}`,
|
|
730
|
+
`**Turns:** ${managed.session.messages.length}`,
|
|
731
|
+
];
|
|
732
|
+
await interaction.editReply(lines.join('\n'));
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case 'agent': {
|
|
737
|
+
const agentName = persona?.display_name || agentId;
|
|
738
|
+
if (persona) {
|
|
739
|
+
const lines = [
|
|
740
|
+
`**Current Agent:** ${agentName}`,
|
|
741
|
+
persona.model ? `**Model:** ${persona.model}` : null,
|
|
742
|
+
persona.system_prompt ? `**System Prompt:** ${persona.system_prompt.slice(0, 100)}...` : null,
|
|
743
|
+
].filter(Boolean);
|
|
744
|
+
await interaction.editReply(lines.join('\n'));
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
await interaction.editReply(`**Current Agent:** Default (no persona configured)`);
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
case 'agents': {
|
|
752
|
+
const agentsConfig = botConfig.agents;
|
|
753
|
+
if (!agentsConfig || Object.keys(agentsConfig).length === 0) {
|
|
754
|
+
await interaction.editReply('No agents configured.');
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
const lines = ['**Configured Agents:**', ''];
|
|
758
|
+
for (const [id, agent] of Object.entries(agentsConfig)) {
|
|
759
|
+
const name = agent.display_name || id;
|
|
760
|
+
const model = agent.model ? ` (${agent.model})` : '';
|
|
761
|
+
lines.push(`• **${name}**${model}`);
|
|
762
|
+
}
|
|
763
|
+
await interaction.editReply(lines.join('\n'));
|
|
764
|
+
}
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
case 'cancel': {
|
|
768
|
+
const managed = sessions.get(key);
|
|
769
|
+
if (!managed) {
|
|
770
|
+
await interaction.reply('No active session.');
|
|
771
|
+
}
|
|
772
|
+
else if (managed.state !== 'running') {
|
|
773
|
+
await interaction.reply('Nothing to cancel.');
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
cancelActive(managed);
|
|
777
|
+
await interaction.reply('🛑 Cancelling...');
|
|
778
|
+
}
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
case 'reset': {
|
|
782
|
+
destroySession(key);
|
|
783
|
+
await interaction.reply('🔄 Session reset.');
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
case 'escalate': {
|
|
787
|
+
const managed = sessions.get(key);
|
|
788
|
+
const escalation = persona?.escalation;
|
|
789
|
+
if (!escalation || !escalation.models?.length) {
|
|
790
|
+
await interaction.reply('❌ No escalation models configured for this agent.');
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
const arg = interaction.options.getString('model');
|
|
794
|
+
// No arg: show available models and current state
|
|
795
|
+
if (!arg) {
|
|
796
|
+
const currentModel = managed?.config.model || config.model || 'default';
|
|
797
|
+
const lines = [
|
|
798
|
+
`**Current model:** \`${currentModel}\``,
|
|
799
|
+
`**Escalation models:** ${escalation.models.map(m => `\`${m}\``).join(', ')}`,
|
|
800
|
+
'',
|
|
801
|
+
'Usage: `/escalate model:<name>` or `/escalate model:next`',
|
|
802
|
+
];
|
|
803
|
+
if (managed?.pendingEscalation) {
|
|
804
|
+
lines.push('', `⚡ **Pending escalation:** \`${managed.pendingEscalation}\``);
|
|
805
|
+
}
|
|
806
|
+
await interaction.reply(lines.join('\n'));
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
if (!managed) {
|
|
810
|
+
await interaction.reply('No active session. Send a message first.');
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
// Handle 'next' - escalate to next model in chain
|
|
814
|
+
let targetModel;
|
|
815
|
+
if (arg.toLowerCase() === 'next') {
|
|
816
|
+
const nextIndex = Math.min(managed.currentModelIndex, escalation.models.length - 1);
|
|
817
|
+
targetModel = escalation.models[nextIndex];
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
// Specific model requested
|
|
821
|
+
if (!escalation.models.includes(arg)) {
|
|
822
|
+
await interaction.reply(`❌ Model \`${arg}\` not in escalation chain. Available: ${escalation.models.map(m => `\`${m}\``).join(', ')}`);
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
targetModel = arg;
|
|
826
|
+
}
|
|
827
|
+
managed.pendingEscalation = targetModel;
|
|
828
|
+
await interaction.reply(`⚡ Next message will use \`${targetModel}\`. Send your request now.`);
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case 'deescalate': {
|
|
832
|
+
const managed = sessions.get(key);
|
|
833
|
+
if (!managed) {
|
|
834
|
+
await interaction.reply('No active session.');
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
if (managed.currentModelIndex === 0 && !managed.pendingEscalation) {
|
|
838
|
+
await interaction.reply('Already using base model.');
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
const baseModel = persona?.model || config.model || 'default';
|
|
842
|
+
managed.pendingEscalation = null;
|
|
843
|
+
managed.currentModelIndex = 0;
|
|
844
|
+
// Recreate session with base model
|
|
845
|
+
const cfg = {
|
|
846
|
+
...managed.config,
|
|
847
|
+
model: baseModel,
|
|
848
|
+
};
|
|
849
|
+
await recreateSession(managed, cfg);
|
|
850
|
+
await interaction.reply(`✅ Returned to base model: \`${baseModel}\``);
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
309
854
|
});
|
|
310
855
|
client.on(Events.MessageCreate, async (msg) => {
|
|
311
856
|
if (msg.author.bot)
|
|
@@ -319,10 +864,14 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
319
864
|
const content = msg.content?.trim();
|
|
320
865
|
if (!content)
|
|
321
866
|
return;
|
|
322
|
-
|
|
867
|
+
// Resolve agent for this message to get the correct session key
|
|
868
|
+
const { agentId, persona } = resolveAgentForMessage(msg, botConfig.agents, botConfig.routing);
|
|
869
|
+
const key = sessionKeyForMessage(msg, allowGuilds, agentId);
|
|
323
870
|
if (content === '/new') {
|
|
324
871
|
destroySession(key);
|
|
325
|
-
|
|
872
|
+
const agentName = persona?.display_name || agentId;
|
|
873
|
+
const agentMsg = persona ? ` (agent: ${agentName})` : '';
|
|
874
|
+
await sendUserVisible(msg, `✨ New session started${agentMsg}. Send a message to begin.`).catch(() => { });
|
|
326
875
|
return;
|
|
327
876
|
}
|
|
328
877
|
const managed = await getOrCreate(msg);
|
|
@@ -336,9 +885,13 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
336
885
|
return;
|
|
337
886
|
}
|
|
338
887
|
if (content === '/start') {
|
|
888
|
+
const agentLine = managed.agentPersona
|
|
889
|
+
? `Agent: **${managed.agentPersona.display_name || managed.agentId}**`
|
|
890
|
+
: null;
|
|
339
891
|
const lines = [
|
|
340
892
|
'🔧 Idle Hands — Local-first coding agent',
|
|
341
893
|
'',
|
|
894
|
+
...(agentLine ? [agentLine] : []),
|
|
342
895
|
`Model: \`${managed.session.model}\``,
|
|
343
896
|
`Endpoint: \`${managed.config.endpoint || '?'}\``,
|
|
344
897
|
`Default dir: \`${managed.config.dir || defaultDir}\``,
|
|
@@ -356,6 +909,10 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
356
909
|
'/new — Start a new session',
|
|
357
910
|
'/cancel — Abort current generation',
|
|
358
911
|
'/status — Session stats',
|
|
912
|
+
'/agent — Show current agent',
|
|
913
|
+
'/agents — List all configured agents',
|
|
914
|
+
'/escalate [model] — Use larger model for next message',
|
|
915
|
+
'/deescalate — Return to base model',
|
|
359
916
|
'/dir [path] — Get/set working directory',
|
|
360
917
|
'/model — Show current model',
|
|
361
918
|
'/approval [mode] — Get/set approval mode',
|
|
@@ -527,7 +1084,11 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
527
1084
|
const pct = managed.session.contextWindow > 0
|
|
528
1085
|
? ((used / managed.session.contextWindow) * 100).toFixed(1)
|
|
529
1086
|
: '?';
|
|
1087
|
+
const agentLine = managed.agentPersona
|
|
1088
|
+
? `Agent: ${managed.agentPersona.display_name || managed.agentId}`
|
|
1089
|
+
: null;
|
|
530
1090
|
await sendUserVisible(msg, [
|
|
1091
|
+
...(agentLine ? [agentLine] : []),
|
|
531
1092
|
`Mode: ${managed.config.mode ?? 'code'}`,
|
|
532
1093
|
`Approval: ${managed.config.approval_mode}`,
|
|
533
1094
|
`Model: ${managed.session.model}`,
|
|
@@ -538,6 +1099,116 @@ export async function startDiscordBot(config, botConfig) {
|
|
|
538
1099
|
].join('\n')).catch(() => { });
|
|
539
1100
|
return;
|
|
540
1101
|
}
|
|
1102
|
+
// /agent - show current agent info
|
|
1103
|
+
if (content === '/agent') {
|
|
1104
|
+
if (!managed.agentPersona) {
|
|
1105
|
+
await sendUserVisible(msg, 'No agent configured. Using global config.').catch(() => { });
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const p = managed.agentPersona;
|
|
1109
|
+
const lines = [
|
|
1110
|
+
`**Agent: ${p.display_name || managed.agentId}** (\`${managed.agentId}\`)`,
|
|
1111
|
+
...(p.model ? [`Model: \`${p.model}\``] : []),
|
|
1112
|
+
...(p.endpoint ? [`Endpoint: \`${p.endpoint}\``] : []),
|
|
1113
|
+
...(p.approval_mode ? [`Approval: \`${p.approval_mode}\``] : []),
|
|
1114
|
+
...(p.default_dir ? [`Default dir: \`${p.default_dir}\``] : []),
|
|
1115
|
+
...(p.allowed_dirs?.length ? [`Allowed dirs: ${p.allowed_dirs.map(d => `\`${d}\``).join(', ')}`] : []),
|
|
1116
|
+
];
|
|
1117
|
+
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
// /agents - list all configured agents
|
|
1121
|
+
if (content === '/agents') {
|
|
1122
|
+
const agents = botConfig.agents;
|
|
1123
|
+
if (!agents || Object.keys(agents).length === 0) {
|
|
1124
|
+
await sendUserVisible(msg, 'No agents configured. Using global config.').catch(() => { });
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const lines = ['**Configured Agents:**'];
|
|
1128
|
+
for (const [id, p] of Object.entries(agents)) {
|
|
1129
|
+
const current = id === managed.agentId ? ' ← current' : '';
|
|
1130
|
+
const model = p.model ? ` (${p.model})` : '';
|
|
1131
|
+
lines.push(`• **${p.display_name || id}** (\`${id}\`)${model}${current}`);
|
|
1132
|
+
}
|
|
1133
|
+
// Show routing rules
|
|
1134
|
+
const routing = botConfig.routing;
|
|
1135
|
+
if (routing) {
|
|
1136
|
+
lines.push('', '**Routing:**');
|
|
1137
|
+
if (routing.default)
|
|
1138
|
+
lines.push(`Default: \`${routing.default}\``);
|
|
1139
|
+
if (routing.users && Object.keys(routing.users).length > 0) {
|
|
1140
|
+
lines.push(`Users: ${Object.entries(routing.users).map(([u, a]) => `${u}→${a}`).join(', ')}`);
|
|
1141
|
+
}
|
|
1142
|
+
if (routing.channels && Object.keys(routing.channels).length > 0) {
|
|
1143
|
+
lines.push(`Channels: ${Object.entries(routing.channels).map(([c, a]) => `${c}→${a}`).join(', ')}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (routing.guilds && Object.keys(routing.guilds).length > 0) {
|
|
1146
|
+
lines.push(`Guilds: ${Object.entries(routing.guilds).map(([g, a]) => `${g}→${a}`).join(', ')}`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
// /escalate - explicitly escalate to a larger model for next message
|
|
1153
|
+
if (content === '/escalate' || content.startsWith('/escalate ')) {
|
|
1154
|
+
const escalation = managed.agentPersona?.escalation;
|
|
1155
|
+
if (!escalation || !escalation.models?.length) {
|
|
1156
|
+
await sendUserVisible(msg, '❌ No escalation models configured for this agent.').catch(() => { });
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const arg = content.slice('/escalate'.length).trim();
|
|
1160
|
+
// No arg: show available models and current state
|
|
1161
|
+
if (!arg) {
|
|
1162
|
+
const currentModel = managed.config.model || config.model || 'default';
|
|
1163
|
+
const lines = [
|
|
1164
|
+
`**Current model:** \`${currentModel}\``,
|
|
1165
|
+
`**Escalation models:** ${escalation.models.map(m => `\`${m}\``).join(', ')}`,
|
|
1166
|
+
'',
|
|
1167
|
+
'Usage: `/escalate <model>` or `/escalate next`',
|
|
1168
|
+
'Then send your message - it will use the escalated model.',
|
|
1169
|
+
];
|
|
1170
|
+
if (managed.pendingEscalation) {
|
|
1171
|
+
lines.push('', `⚡ **Pending escalation:** \`${managed.pendingEscalation}\` (next message will use this)`);
|
|
1172
|
+
}
|
|
1173
|
+
await sendUserVisible(msg, lines.join('\n')).catch(() => { });
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
// Handle 'next' - escalate to next model in chain
|
|
1177
|
+
let targetModel;
|
|
1178
|
+
if (arg.toLowerCase() === 'next') {
|
|
1179
|
+
const nextIndex = Math.min(managed.currentModelIndex, escalation.models.length - 1);
|
|
1180
|
+
targetModel = escalation.models[nextIndex];
|
|
1181
|
+
}
|
|
1182
|
+
else {
|
|
1183
|
+
// Specific model requested
|
|
1184
|
+
if (!escalation.models.includes(arg)) {
|
|
1185
|
+
await sendUserVisible(msg, `❌ Model \`${arg}\` not in escalation chain. Available: ${escalation.models.map(m => `\`${m}\``).join(', ')}`).catch(() => { });
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
targetModel = arg;
|
|
1189
|
+
}
|
|
1190
|
+
managed.pendingEscalation = targetModel;
|
|
1191
|
+
await sendUserVisible(msg, `⚡ Next message will use \`${targetModel}\`. Send your request now.`).catch(() => { });
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
// /deescalate - return to base model
|
|
1195
|
+
if (content === '/deescalate') {
|
|
1196
|
+
if (managed.currentModelIndex === 0 && !managed.pendingEscalation) {
|
|
1197
|
+
await sendUserVisible(msg, 'Already using base model.').catch(() => { });
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const baseModel = managed.agentPersona?.model || config.model || 'default';
|
|
1201
|
+
managed.pendingEscalation = null;
|
|
1202
|
+
managed.currentModelIndex = 0;
|
|
1203
|
+
// Recreate session with base model
|
|
1204
|
+
const cfg = {
|
|
1205
|
+
...managed.config,
|
|
1206
|
+
model: baseModel,
|
|
1207
|
+
};
|
|
1208
|
+
await recreateSession(managed, cfg);
|
|
1209
|
+
await sendUserVisible(msg, `✅ Returned to base model: \`${baseModel}\``).catch(() => { });
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
541
1212
|
if (content === '/hosts') {
|
|
542
1213
|
try {
|
|
543
1214
|
const { loadRuntimes, redactConfig } = await import('../runtime/store.js');
|