kimaki 0.26.0 → 0.28.0
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/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -13,20 +13,21 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import url from 'node:url';
|
|
15
15
|
import { describe, beforeAll, afterAll, test, expect, } from 'vitest';
|
|
16
|
-
import { ChannelType, Client, GatewayIntentBits, Partials, REST, Routes
|
|
16
|
+
import { ChannelType, Client, GatewayIntentBits, Partials, REST, Routes } from 'discord.js';
|
|
17
17
|
import { DigitalDiscord } from 'discord-digital-twin/src';
|
|
18
18
|
import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
|
|
19
19
|
import { setDataDir } from './config.js';
|
|
20
20
|
import { store } from './store.js';
|
|
21
21
|
import { startDiscordBot } from './discord-bot.js';
|
|
22
|
-
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, setChannelAgent, setChannelModel, getThreadSession, getSessionModel, getSessionAgent, getChannelAgent, setSessionModel, } from './database.js';
|
|
22
|
+
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, setChannelAgent, setChannelModel, getThreadSession, getSessionModel, getSessionAgent, getChannelAgent, getChannelModel, setSessionModel, } from './database.js';
|
|
23
23
|
import { getDb } from './db.js';
|
|
24
24
|
import * as orm from 'drizzle-orm';
|
|
25
25
|
import * as schema from './schema.js';
|
|
26
26
|
import { startHranaServer, stopHranaServer } from './hrana-server.js';
|
|
27
27
|
import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
|
|
28
|
-
import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
28
|
+
import { chooseLockPort, cleanupTestSessions, initTestGitRepo, isFooterMessage, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
29
29
|
import { buildQuickAgentCommandDescription } from './commands/agent.js';
|
|
30
|
+
import { buildQuickAgentSlashCommand } from './discord-command-registration.js';
|
|
30
31
|
const TEST_USER_ID = '200000000000000920';
|
|
31
32
|
const TEXT_CHANNEL_ID = '200000000000000921';
|
|
32
33
|
const AGENT_MODEL = 'agent-model-v2';
|
|
@@ -265,6 +266,9 @@ describe('agent model resolution', () => {
|
|
|
265
266
|
}
|
|
266
267
|
providerConfig.models[AGENT_MODEL] = { name: AGENT_MODEL };
|
|
267
268
|
providerConfig.models[PLAN_AGENT_MODEL] = { name: PLAN_AGENT_MODEL };
|
|
269
|
+
Object.assign(providerConfig.models[PLAN_AGENT_MODEL], {
|
|
270
|
+
variants: { high: {}, max: {} },
|
|
271
|
+
});
|
|
268
272
|
providerConfig.models[CHANNEL_MODEL] = { name: CHANNEL_MODEL };
|
|
269
273
|
fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
270
274
|
// Leading /command detection only rewrites when registeredUserCommands is set
|
|
@@ -316,18 +320,13 @@ describe('agent model resolution', () => {
|
|
|
316
320
|
// Register quick agent slash commands so /plan-agent and /test-agent-agent
|
|
317
321
|
// are resolvable by handleQuickAgentCommand via guild.commands.fetch().
|
|
318
322
|
const agentCommands = ['test-agent', 'plan', 'plain'].map((agentName) => {
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
.addStringOption((opt) => opt
|
|
327
|
-
.setName('prompt')
|
|
328
|
-
.setDescription('Send a prompt with this agent')
|
|
329
|
-
.setRequired(false))
|
|
330
|
-
.toJSON();
|
|
323
|
+
return buildQuickAgentSlashCommand({
|
|
324
|
+
commandName: `${agentName}-agent`,
|
|
325
|
+
description: buildQuickAgentCommandDescription({
|
|
326
|
+
agentName,
|
|
327
|
+
description: `Switch to ${agentName} agent`,
|
|
328
|
+
}),
|
|
329
|
+
}).toJSON();
|
|
331
330
|
});
|
|
332
331
|
const rest = new REST({ version: '10', api: discord.restUrl }).setToken(discord.botToken);
|
|
333
332
|
await rest.put(Routes.applicationGuildCommands(discord.botUserId, discord.guildId), { body: agentCommands });
|
|
@@ -394,15 +393,15 @@ describe('agent model resolution', () => {
|
|
|
394
393
|
// Find the footer message (starts with * italic)
|
|
395
394
|
const footerMessage = messages.find((message) => {
|
|
396
395
|
return (message.author.id === discord.botUserId &&
|
|
397
|
-
message.
|
|
396
|
+
isFooterMessage({ message, botUserId: discord.botUserId }));
|
|
398
397
|
});
|
|
399
398
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
400
399
|
"--- from: user (agent-model-tester)
|
|
401
400
|
Reply with exactly: agent-model-check
|
|
402
401
|
--- from: assistant (TestBot)
|
|
403
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
404
|
-
|
|
405
|
-
*project ⋅ main ⋅
|
|
402
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
403
|
+
> ok
|
|
404
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
|
|
406
405
|
`);
|
|
407
406
|
expect(footerMessage).toBeDefined();
|
|
408
407
|
if (!footerMessage) {
|
|
@@ -444,9 +443,9 @@ describe('agent model resolution', () => {
|
|
|
444
443
|
"--- from: user (agent-model-tester)
|
|
445
444
|
Reply with exactly: system-context-check
|
|
446
445
|
--- from: assistant (TestBot)
|
|
447
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
448
|
-
|
|
449
|
-
*project ⋅ main ⋅
|
|
446
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
447
|
+
> system-context-ok
|
|
448
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
|
|
450
449
|
`);
|
|
451
450
|
}, 15_000);
|
|
452
451
|
test('session.command path includes kimaki system prompt on first message', async () => {
|
|
@@ -514,7 +513,7 @@ describe('agent model resolution', () => {
|
|
|
514
513
|
const threadText = await discord.thread(thread.id).text();
|
|
515
514
|
expect(threadText).toContain('first message in thread');
|
|
516
515
|
expect(threadText).toContain('Reply with exactly: reply-context-check');
|
|
517
|
-
expect(threadText).toContain('
|
|
516
|
+
expect(threadText).toContain('ok');
|
|
518
517
|
}, 15_000);
|
|
519
518
|
test('new thread uses channel model when channel model preference is set', async () => {
|
|
520
519
|
// Clear channel agent so model resolution falls through to channel model
|
|
@@ -544,15 +543,15 @@ describe('agent model resolution', () => {
|
|
|
544
543
|
const messages = await discord.thread(thread.id).getMessages();
|
|
545
544
|
const footerMessage = messages.find((message) => {
|
|
546
545
|
return (message.author.id === discord.botUserId &&
|
|
547
|
-
message.
|
|
546
|
+
isFooterMessage({ message, botUserId: discord.botUserId }));
|
|
548
547
|
});
|
|
549
548
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
550
549
|
"--- from: user (agent-model-tester)
|
|
551
550
|
Reply with exactly: channel-model-check
|
|
552
551
|
--- from: assistant (TestBot)
|
|
553
|
-
*using deterministic-provider/channel-model-v2*
|
|
554
|
-
|
|
555
|
-
*project ⋅ main ⋅
|
|
552
|
+
> *using deterministic-provider/channel-model-v2*
|
|
553
|
+
> ok
|
|
554
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ channel-model-v2* <@200000000000000920>"
|
|
556
555
|
`);
|
|
557
556
|
expect(footerMessage).toBeDefined();
|
|
558
557
|
if (!footerMessage) {
|
|
@@ -565,59 +564,41 @@ describe('agent model resolution', () => {
|
|
|
565
564
|
expect(footerMessage.content).toContain(CHANNEL_MODEL);
|
|
566
565
|
expect(footerMessage.content).not.toContain(DEFAULT_MODEL);
|
|
567
566
|
}, 15_000);
|
|
568
|
-
test('channel model
|
|
569
|
-
// Clear channel agent so model resolution falls through to channel model
|
|
567
|
+
test('unsupported channel model variant falls back to the selected model', async () => {
|
|
570
568
|
const db = await getDb();
|
|
571
569
|
await db.delete(schema.channel_agents).where(orm.eq(schema.channel_agents.channel_id, TEXT_CHANNEL_ID));
|
|
572
|
-
// Set channel model with a variant (thinking level)
|
|
573
|
-
// The deterministic provider doesn't support thinking, so the variant
|
|
574
|
-
// is resolved but silently dropped (no matching thinking values).
|
|
575
|
-
// This test verifies the variant cascade code path runs without crashing
|
|
576
|
-
// and the correct model still appears in the footer.
|
|
577
570
|
await setChannelModel({
|
|
578
571
|
channelId: TEXT_CHANNEL_ID,
|
|
579
572
|
modelId: `${PROVIDER_NAME}/${CHANNEL_MODEL}`,
|
|
580
573
|
variant: 'high',
|
|
581
574
|
});
|
|
575
|
+
const prompt = 'Reply with exactly: variant-check';
|
|
582
576
|
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
583
|
-
content:
|
|
577
|
+
content: prompt,
|
|
584
578
|
});
|
|
585
579
|
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
586
580
|
timeout: 4_000,
|
|
587
|
-
predicate: (
|
|
588
|
-
return t.name === 'Reply with exactly: variant-check';
|
|
589
|
-
},
|
|
581
|
+
predicate: (candidate) => candidate.name === prompt,
|
|
590
582
|
});
|
|
591
|
-
await
|
|
583
|
+
const messages = await waitForFooterMessage({
|
|
592
584
|
discord,
|
|
593
585
|
threadId: thread.id,
|
|
594
|
-
userId: TEST_USER_ID,
|
|
595
|
-
text: '*project',
|
|
596
586
|
timeout: 4_000,
|
|
597
587
|
});
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
message.content.startsWith('*'));
|
|
588
|
+
const footer = messages.find((message) => {
|
|
589
|
+
return message.author.id === discord.botUserId
|
|
590
|
+
&& isFooterMessage({ message, botUserId: discord.botUserId });
|
|
602
591
|
});
|
|
603
592
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
604
593
|
"--- from: user (agent-model-tester)
|
|
605
594
|
Reply with exactly: variant-check
|
|
606
595
|
--- from: assistant (TestBot)
|
|
607
|
-
*using deterministic-provider/channel-model-v2*
|
|
608
|
-
|
|
609
|
-
*project ⋅ main ⋅
|
|
596
|
+
> *using deterministic-provider/channel-model-v2*
|
|
597
|
+
> ok
|
|
598
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ channel-model-v2* <@200000000000000920>"
|
|
610
599
|
`);
|
|
611
|
-
expect(
|
|
612
|
-
|
|
613
|
-
throw new Error(`Expected footer message but none found. Bot messages: ${messages
|
|
614
|
-
.filter((m) => m.author.id === discord.botUserId)
|
|
615
|
-
.map((m) => m.content.slice(0, 150))
|
|
616
|
-
.join(' | ')}`);
|
|
617
|
-
}
|
|
618
|
-
// Footer should still contain the channel model (variant doesn't crash)
|
|
619
|
-
expect(footerMessage.content).toContain(CHANNEL_MODEL);
|
|
620
|
-
expect(footerMessage.content).not.toContain(DEFAULT_MODEL);
|
|
600
|
+
expect(footer?.content).toContain(CHANNEL_MODEL);
|
|
601
|
+
expect(footer?.content).not.toContain(DEFAULT_MODEL);
|
|
621
602
|
}, 15_000);
|
|
622
603
|
test('/btw fork keeps source session model when channel model differs', async () => {
|
|
623
604
|
const db = await getDb();
|
|
@@ -680,8 +661,8 @@ describe('agent model resolution', () => {
|
|
|
680
661
|
"--- from: assistant (TestBot)
|
|
681
662
|
Reusing context from <#SOURCE_THREAD> to answer prompt...
|
|
682
663
|
Reply with exactly: btw-model-check
|
|
683
|
-
|
|
684
|
-
*project ⋅ main ⋅
|
|
664
|
+
> ok
|
|
665
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2* <@200000000000000920>"
|
|
685
666
|
`);
|
|
686
667
|
expect(forkedSessionModel).toMatchInlineSnapshot(`
|
|
687
668
|
{
|
|
@@ -714,7 +695,7 @@ describe('agent model resolution', () => {
|
|
|
714
695
|
});
|
|
715
696
|
const firstMessages = await discord.thread(thread.id).getMessages();
|
|
716
697
|
const firstFooter = firstMessages.find((m) => {
|
|
717
|
-
return (m
|
|
698
|
+
return (isFooterMessage({ message: m, botUserId: discord.botUserId }));
|
|
718
699
|
});
|
|
719
700
|
expect(firstFooter).toBeDefined();
|
|
720
701
|
// Verify the first run used test-agent's model
|
|
@@ -744,20 +725,20 @@ describe('agent model resolution', () => {
|
|
|
744
725
|
"--- from: user (agent-model-tester)
|
|
745
726
|
Reply with exactly: first-thread-msg
|
|
746
727
|
--- from: assistant (TestBot)
|
|
747
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
748
|
-
|
|
749
|
-
*project ⋅ main ⋅
|
|
728
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
729
|
+
> ok
|
|
730
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
750
731
|
--- from: user (agent-model-tester)
|
|
751
732
|
Reply with exactly: second-thread-msg
|
|
752
733
|
--- from: assistant (TestBot)
|
|
753
|
-
|
|
754
|
-
*project ⋅ main ⋅
|
|
734
|
+
> ok
|
|
735
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
|
|
755
736
|
`);
|
|
756
737
|
const secondMessages = await discord.thread(thread.id).getMessages();
|
|
757
738
|
const secondFooter = [...secondMessages]
|
|
758
739
|
.reverse()
|
|
759
740
|
.find((m) => {
|
|
760
|
-
return (m
|
|
741
|
+
return (isFooterMessage({ message: m, botUserId: discord.botUserId }));
|
|
761
742
|
});
|
|
762
743
|
expect(secondFooter).toBeDefined();
|
|
763
744
|
// The existing thread should still use test-agent's model (AGENT_MODEL),
|
|
@@ -765,23 +746,25 @@ describe('agent model resolution', () => {
|
|
|
765
746
|
expect(secondFooter.content).toContain(AGENT_MODEL);
|
|
766
747
|
expect(secondFooter.content).not.toContain(PLAN_AGENT_MODEL);
|
|
767
748
|
}, 20_000);
|
|
768
|
-
test('
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
await
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
749
|
+
test('/plan-agent with prompt starts a session with the plan agent', async () => {
|
|
750
|
+
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
751
|
+
const prompt = 'Reply with exactly: inline-plan-agent-msg';
|
|
752
|
+
const { id: interactionId } = await discord
|
|
753
|
+
.channel(TEXT_CHANNEL_ID)
|
|
754
|
+
.user(TEST_USER_ID)
|
|
755
|
+
.runSlashCommand({
|
|
756
|
+
name: 'plan-agent',
|
|
757
|
+
options: [{ name: 'prompt', type: 3, value: prompt }],
|
|
777
758
|
});
|
|
759
|
+
await discord
|
|
760
|
+
.channel(TEXT_CHANNEL_ID)
|
|
761
|
+
.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
778
762
|
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
779
763
|
timeout: 4_000,
|
|
780
764
|
predicate: (t) => {
|
|
781
|
-
return t.name ===
|
|
765
|
+
return t.name === prompt;
|
|
782
766
|
},
|
|
783
767
|
});
|
|
784
|
-
// Wait for footer — should show the default model
|
|
785
768
|
await waitForFooterMessage({
|
|
786
769
|
discord,
|
|
787
770
|
threadId: thread.id,
|
|
@@ -789,79 +772,36 @@ describe('agent model resolution', () => {
|
|
|
789
772
|
afterMessageIncludes: 'ok',
|
|
790
773
|
afterAuthorId: discord.botUserId,
|
|
791
774
|
});
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
expect(firstFooter).toBeDefined();
|
|
797
|
-
// First run uses the default model (no agent set)
|
|
798
|
-
expect(firstFooter.content).toContain(DEFAULT_MODEL);
|
|
799
|
-
expect(firstFooter.content).not.toContain(AGENT_MODEL);
|
|
800
|
-
// 2. Set channel agent to test-agent via /test-agent-agent in the CHANNEL
|
|
801
|
-
const { id: interactionId } = await discord
|
|
802
|
-
.channel(TEXT_CHANNEL_ID)
|
|
803
|
-
.user(TEST_USER_ID)
|
|
804
|
-
.runSlashCommand({ name: 'test-agent-agent' });
|
|
805
|
-
await discord
|
|
806
|
-
.channel(TEXT_CHANNEL_ID)
|
|
807
|
-
.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
808
|
-
// 3. Send a second message in the EXISTING thread
|
|
809
|
-
await discord
|
|
810
|
-
.thread(thread.id)
|
|
811
|
-
.user(TEST_USER_ID)
|
|
812
|
-
.sendMessage({
|
|
813
|
-
content: 'Reply with exactly: default-second-msg',
|
|
814
|
-
});
|
|
815
|
-
await waitForFooterMessage({
|
|
816
|
-
discord,
|
|
817
|
-
threadId: thread.id,
|
|
818
|
-
timeout: 4_000,
|
|
819
|
-
afterMessageIncludes: 'default-second-msg',
|
|
820
|
-
afterAuthorId: TEST_USER_ID,
|
|
821
|
-
});
|
|
775
|
+
const sessionId = await getThreadSession(thread.id);
|
|
776
|
+
expect(sessionId).toBeDefined();
|
|
777
|
+
expect(sessionId ? await getSessionAgent(sessionId) : undefined).toBe('plan');
|
|
778
|
+
expect(await getChannelAgent(TEXT_CHANNEL_ID)).toBe('test-agent');
|
|
822
779
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
823
|
-
"--- from:
|
|
824
|
-
Reply with exactly:
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
829
|
-
--- from: user (agent-model-tester)
|
|
830
|
-
Reply with exactly: default-second-msg
|
|
831
|
-
--- from: assistant (TestBot)
|
|
832
|
-
⬥ ok
|
|
833
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
780
|
+
"--- from: assistant (TestBot)
|
|
781
|
+
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-msg
|
|
782
|
+
> *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
783
|
+
> ok
|
|
784
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
834
785
|
`);
|
|
835
|
-
const secondMessages = await discord.thread(thread.id).getMessages();
|
|
836
|
-
const secondFooter = [...secondMessages]
|
|
837
|
-
.reverse()
|
|
838
|
-
.find((m) => {
|
|
839
|
-
return (m.author.id === discord.botUserId && m.content.startsWith('*'));
|
|
840
|
-
});
|
|
841
|
-
expect(secondFooter).toBeDefined();
|
|
842
|
-
// The existing thread should still use the DEFAULT model,
|
|
843
|
-
// NOT the test-agent's model (AGENT_MODEL)
|
|
844
|
-
expect(secondFooter.content).toContain(DEFAULT_MODEL);
|
|
845
|
-
expect(secondFooter.content).not.toContain(AGENT_MODEL);
|
|
846
786
|
}, 20_000);
|
|
847
|
-
test('/plan-agent with prompt
|
|
848
|
-
|
|
849
|
-
const prompt = 'Reply with exactly: inline-plan-agent-msg';
|
|
787
|
+
test('/plan-agent with prompt persists a supported thinking variant', async () => {
|
|
788
|
+
const prompt = 'Reply with exactly: inline-plan-agent-variant-msg';
|
|
850
789
|
const { id: interactionId } = await discord
|
|
851
790
|
.channel(TEXT_CHANNEL_ID)
|
|
852
791
|
.user(TEST_USER_ID)
|
|
853
792
|
.runSlashCommand({
|
|
854
793
|
name: 'plan-agent',
|
|
855
|
-
options: [
|
|
794
|
+
options: [
|
|
795
|
+
{ name: 'prompt', type: 3, value: prompt },
|
|
796
|
+
{ name: 'variant', type: 3, value: 'high' },
|
|
797
|
+
],
|
|
856
798
|
});
|
|
857
799
|
await discord
|
|
858
800
|
.channel(TEXT_CHANNEL_ID)
|
|
859
801
|
.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
860
802
|
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
861
803
|
timeout: 4_000,
|
|
862
|
-
predicate: (t) =>
|
|
863
|
-
return t.name === prompt;
|
|
864
|
-
},
|
|
804
|
+
predicate: (t) => t.name === prompt,
|
|
865
805
|
});
|
|
866
806
|
await waitForFooterMessage({
|
|
867
807
|
discord,
|
|
@@ -872,16 +812,62 @@ describe('agent model resolution', () => {
|
|
|
872
812
|
});
|
|
873
813
|
const sessionId = await getThreadSession(thread.id);
|
|
874
814
|
expect(sessionId).toBeDefined();
|
|
875
|
-
expect(sessionId ? await getSessionAgent(sessionId) : undefined).toBe('plan');
|
|
876
|
-
expect(await getChannelAgent(TEXT_CHANNEL_ID)).toBe('test-agent');
|
|
877
815
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
878
816
|
"--- from: assistant (TestBot)
|
|
879
|
-
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-msg
|
|
880
|
-
*using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
881
|
-
|
|
882
|
-
*project ⋅ main ⋅
|
|
817
|
+
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-variant-msg
|
|
818
|
+
> *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
819
|
+
> ok
|
|
820
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
821
|
+
`);
|
|
822
|
+
expect(sessionId ? await getSessionModel(sessionId) : undefined).toMatchInlineSnapshot(`
|
|
823
|
+
{
|
|
824
|
+
"modelId": "deterministic-provider/plan-model-v2",
|
|
825
|
+
"variant": "high",
|
|
826
|
+
}
|
|
883
827
|
`);
|
|
884
828
|
}, 20_000);
|
|
829
|
+
test('/plan-agent variant persists on the channel without a prompt', async () => {
|
|
830
|
+
const db = await getDb();
|
|
831
|
+
await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, TEXT_CHANNEL_ID));
|
|
832
|
+
const { id: interactionId } = await discord
|
|
833
|
+
.channel(TEXT_CHANNEL_ID)
|
|
834
|
+
.user(TEST_USER_ID)
|
|
835
|
+
.runSlashCommand({
|
|
836
|
+
name: 'plan-agent',
|
|
837
|
+
options: [{ name: 'variant', type: 3, value: 'max' }],
|
|
838
|
+
});
|
|
839
|
+
await discord
|
|
840
|
+
.channel(TEXT_CHANNEL_ID)
|
|
841
|
+
.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
842
|
+
const start = Date.now();
|
|
843
|
+
let confirmation = '';
|
|
844
|
+
while (Date.now() - start < 4_000) {
|
|
845
|
+
const messages = await discord.channel(TEXT_CHANNEL_ID).getMessages();
|
|
846
|
+
const match = [...messages].reverse().find((message) => {
|
|
847
|
+
return (message.author.id === discord.botUserId &&
|
|
848
|
+
message.content.includes('Variant: **max**'));
|
|
849
|
+
});
|
|
850
|
+
if (match) {
|
|
851
|
+
confirmation = match.content;
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
855
|
+
}
|
|
856
|
+
expect(confirmation).toMatchInlineSnapshot(`
|
|
857
|
+
"Switched to **plan** agent for this channel (was **test-agent**)
|
|
858
|
+
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
859
|
+
Variant: **max**
|
|
860
|
+
All new sessions will use this agent."
|
|
861
|
+
`);
|
|
862
|
+
expect(await getChannelAgent(TEXT_CHANNEL_ID)).toBe('plan');
|
|
863
|
+
expect(await getChannelModel(TEXT_CHANNEL_ID)).toMatchInlineSnapshot(`
|
|
864
|
+
{
|
|
865
|
+
"modelId": "deterministic-provider/plan-model-v2",
|
|
866
|
+
"variant": "max",
|
|
867
|
+
}
|
|
868
|
+
`);
|
|
869
|
+
await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, TEXT_CHANNEL_ID));
|
|
870
|
+
}, 20_000);
|
|
885
871
|
test('/plan-agent with prompt in an existing thread changes the session agent', async () => {
|
|
886
872
|
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
887
873
|
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
@@ -922,12 +908,12 @@ describe('agent model resolution', () => {
|
|
|
922
908
|
"--- from: user (agent-model-tester)
|
|
923
909
|
Reply with exactly: inline-existing-first-msg
|
|
924
910
|
--- from: assistant (TestBot)
|
|
925
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
926
|
-
|
|
927
|
-
*project ⋅ main ⋅
|
|
911
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
912
|
+
> ok
|
|
913
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
928
914
|
» **agent-model-tester** (plan): Reply with exactly: inline-existing-plan-msg
|
|
929
|
-
|
|
930
|
-
*project ⋅ main ⋅
|
|
915
|
+
> ok
|
|
916
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
931
917
|
`);
|
|
932
918
|
}, 20_000);
|
|
933
919
|
test('/plan-agent inside a thread switches the model for that thread', async () => {
|
|
@@ -951,7 +937,7 @@ describe('agent model resolution', () => {
|
|
|
951
937
|
afterAuthorId: discord.botUserId,
|
|
952
938
|
});
|
|
953
939
|
const firstFooter = (await discord.thread(thread.id).getMessages()).find((m) => {
|
|
954
|
-
return (m
|
|
940
|
+
return (isFooterMessage({ message: m, botUserId: discord.botUserId }));
|
|
955
941
|
});
|
|
956
942
|
expect(firstFooter).toBeDefined();
|
|
957
943
|
expect(firstFooter.content).toContain(AGENT_MODEL);
|
|
@@ -976,28 +962,128 @@ describe('agent model resolution', () => {
|
|
|
976
962
|
"--- from: user (agent-model-tester)
|
|
977
963
|
Reply with exactly: switch-in-thread-msg
|
|
978
964
|
--- from: assistant (TestBot)
|
|
979
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
980
|
-
|
|
981
|
-
*project ⋅ main ⋅
|
|
965
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
966
|
+
> ok
|
|
967
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
982
968
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
983
969
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
984
970
|
The agent will change on the next message.
|
|
985
971
|
--- from: user (agent-model-tester)
|
|
986
972
|
Reply with exactly: after-switch-msg
|
|
987
973
|
--- from: assistant (TestBot)
|
|
988
|
-
|
|
989
|
-
*project ⋅ main ⋅
|
|
974
|
+
> ok
|
|
975
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
990
976
|
`);
|
|
991
977
|
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
992
978
|
.reverse()
|
|
993
979
|
.find((m) => {
|
|
994
|
-
return (m
|
|
980
|
+
return (isFooterMessage({ message: m, botUserId: discord.botUserId }));
|
|
995
981
|
});
|
|
996
982
|
expect(secondFooter).toBeDefined();
|
|
997
983
|
// After /plan-agent in the thread, model should switch to plan's model
|
|
998
984
|
expect(secondFooter.content).toContain(PLAN_AGENT_MODEL);
|
|
999
985
|
expect(secondFooter.content).not.toContain(AGENT_MODEL);
|
|
1000
986
|
}, 20_000);
|
|
987
|
+
test('/plan-agent then an immediate follow-up uses the new agent', async () => {
|
|
988
|
+
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
989
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
990
|
+
content: 'Reply with exactly: race-switch-first-msg',
|
|
991
|
+
});
|
|
992
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
993
|
+
timeout: 4_000,
|
|
994
|
+
predicate: (t) => {
|
|
995
|
+
return t.name === 'Reply with exactly: race-switch-first-msg';
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
await waitForFooterMessage({
|
|
999
|
+
discord,
|
|
1000
|
+
threadId: thread.id,
|
|
1001
|
+
timeout: 4_000,
|
|
1002
|
+
afterMessageIncludes: 'ok',
|
|
1003
|
+
afterAuthorId: discord.botUserId,
|
|
1004
|
+
});
|
|
1005
|
+
const th = discord.thread(thread.id);
|
|
1006
|
+
const { id: interactionId } = await th
|
|
1007
|
+
.user(TEST_USER_ID)
|
|
1008
|
+
.runSlashCommand({ name: 'plan-agent' });
|
|
1009
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
1010
|
+
content: 'Reply with exactly: race-switch-follow-up',
|
|
1011
|
+
});
|
|
1012
|
+
await th.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
1013
|
+
await waitForFooterMessage({
|
|
1014
|
+
discord,
|
|
1015
|
+
threadId: thread.id,
|
|
1016
|
+
timeout: 4_000,
|
|
1017
|
+
afterMessageIncludes: 'race-switch-follow-up',
|
|
1018
|
+
afterAuthorId: TEST_USER_ID,
|
|
1019
|
+
});
|
|
1020
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
1021
|
+
"--- from: user (agent-model-tester)
|
|
1022
|
+
Reply with exactly: race-switch-first-msg
|
|
1023
|
+
--- from: assistant (TestBot)
|
|
1024
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1025
|
+
> ok
|
|
1026
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
1027
|
+
--- from: user (agent-model-tester)
|
|
1028
|
+
Reply with exactly: race-switch-follow-up
|
|
1029
|
+
--- from: assistant (TestBot)
|
|
1030
|
+
Switched to **plan** agent for this session (was **test-agent**)
|
|
1031
|
+
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1032
|
+
The agent will change on the next message.
|
|
1033
|
+
> ok
|
|
1034
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
1035
|
+
`);
|
|
1036
|
+
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
1037
|
+
.reverse()
|
|
1038
|
+
.find((m) => {
|
|
1039
|
+
return (isFooterMessage({ message: m, botUserId: discord.botUserId }));
|
|
1040
|
+
});
|
|
1041
|
+
expect(secondFooter).toBeDefined();
|
|
1042
|
+
expect(secondFooter.content).toContain(PLAN_AGENT_MODEL);
|
|
1043
|
+
expect(secondFooter.content).not.toContain(AGENT_MODEL);
|
|
1044
|
+
}, 20_000);
|
|
1045
|
+
test('/plan-agent in a channel then an immediate message uses the new agent', async () => {
|
|
1046
|
+
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
1047
|
+
const { id: interactionId } = await discord
|
|
1048
|
+
.channel(TEXT_CHANNEL_ID)
|
|
1049
|
+
.user(TEST_USER_ID)
|
|
1050
|
+
.runSlashCommand({ name: 'plan-agent' });
|
|
1051
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
1052
|
+
content: 'Reply with exactly: race-channel-follow-up',
|
|
1053
|
+
});
|
|
1054
|
+
await discord
|
|
1055
|
+
.channel(TEXT_CHANNEL_ID)
|
|
1056
|
+
.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
1057
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
1058
|
+
timeout: 4_000,
|
|
1059
|
+
predicate: (t) => {
|
|
1060
|
+
return t.name === 'Reply with exactly: race-channel-follow-up';
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
await waitForFooterMessage({
|
|
1064
|
+
discord,
|
|
1065
|
+
threadId: thread.id,
|
|
1066
|
+
timeout: 4_000,
|
|
1067
|
+
afterMessageIncludes: 'ok',
|
|
1068
|
+
afterAuthorId: discord.botUserId,
|
|
1069
|
+
});
|
|
1070
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
1071
|
+
"--- from: user (agent-model-tester)
|
|
1072
|
+
Reply with exactly: race-channel-follow-up
|
|
1073
|
+
--- from: assistant (TestBot)
|
|
1074
|
+
> *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1075
|
+
> ok
|
|
1076
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
1077
|
+
`);
|
|
1078
|
+
const footer = [...(await discord.thread(thread.id).getMessages())]
|
|
1079
|
+
.reverse()
|
|
1080
|
+
.find((m) => {
|
|
1081
|
+
return isFooterMessage({ message: m, botUserId: discord.botUserId });
|
|
1082
|
+
});
|
|
1083
|
+
expect(footer).toBeDefined();
|
|
1084
|
+
expect(footer.content).toContain(PLAN_AGENT_MODEL);
|
|
1085
|
+
expect(footer.content).not.toContain(AGENT_MODEL);
|
|
1086
|
+
}, 20_000);
|
|
1001
1087
|
test('/plan-agent on the same agent refreshes a stale session model', async () => {
|
|
1002
1088
|
await setChannelAgent(TEXT_CHANNEL_ID, 'plan');
|
|
1003
1089
|
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
@@ -1033,9 +1119,9 @@ describe('agent model resolution', () => {
|
|
|
1033
1119
|
"--- from: user (agent-model-tester)
|
|
1034
1120
|
Reply with exactly: refresh-agent-model-msg
|
|
1035
1121
|
--- from: assistant (TestBot)
|
|
1036
|
-
*using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1037
|
-
|
|
1038
|
-
*project ⋅ main ⋅
|
|
1122
|
+
> *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1123
|
+
> ok
|
|
1124
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>
|
|
1039
1125
|
Using **plan** agent for this session
|
|
1040
1126
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1041
1127
|
The agent will change on the next message."
|
|
@@ -1075,9 +1161,9 @@ describe('agent model resolution', () => {
|
|
|
1075
1161
|
"--- from: user (agent-model-tester)
|
|
1076
1162
|
Reply with exactly: channel-vs-agent-msg
|
|
1077
1163
|
--- from: assistant (TestBot)
|
|
1078
|
-
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1079
|
-
|
|
1080
|
-
*project ⋅ main ⋅
|
|
1164
|
+
> *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1165
|
+
> ok
|
|
1166
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
1081
1167
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
1082
1168
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1083
1169
|
The agent will change on the next message."
|
|
@@ -1117,9 +1203,9 @@ describe('agent model resolution', () => {
|
|
|
1117
1203
|
"--- from: user (agent-model-tester)
|
|
1118
1204
|
Reply with exactly: plain-agent-override-msg
|
|
1119
1205
|
--- from: assistant (TestBot)
|
|
1120
|
-
*using deterministic-provider/channel-model-v2*
|
|
1121
|
-
|
|
1122
|
-
*project ⋅ main ⋅
|
|
1206
|
+
> *using deterministic-provider/channel-model-v2*
|
|
1207
|
+
> ok
|
|
1208
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ channel-model-v2* <@200000000000000920>
|
|
1123
1209
|
Switched to **plain** agent for this session
|
|
1124
1210
|
Model: *deterministic-provider/channel-model-v2* (channel override)
|
|
1125
1211
|
This model comes from a channel override. Use /model and press Clear override if you want this agent's model.
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
//
|
|
1
|
+
// LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
|
|
2
|
+
// Helpers for Anthropic OAuth account and Claude Code client identity.
|
|
3
|
+
const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.257 (external, cli)';
|
|
4
|
+
export function applyClaudeCodeRequestIdentity({ headers, accessToken, }) {
|
|
5
|
+
headers.set('authorization', `Bearer ${accessToken}`);
|
|
6
|
+
headers.set('user-agent', process.env.OPENCODE_ANTHROPIC_USER_AGENT || CLAUDE_CODE_USER_AGENT);
|
|
7
|
+
headers.set('x-app', 'cli');
|
|
8
|
+
return headers;
|
|
9
|
+
}
|
|
2
10
|
const identityHintKeys = new Set(['user', 'profile', 'account', 'viewer']);
|
|
3
11
|
const idKeys = ['user_id', 'userId', 'account_id', 'accountId', 'id', 'sub'];
|
|
4
12
|
export function normalizeAnthropicAccountIdentity(identity) {
|