Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"name": "minimax-bot",
"displayName": "MiniMax Bot",
"version": "0.7.9",
"version": "0.7.10",
"description": "Turn MiniMax Code into an always-on team of Bots. A Chief Bot helps you build the team; in group chats the admin answers and @mentions hand work to other members. Every Bot has its own octopus avatar and memory, with scheduled tasks and usage stats.",
"author": "MiniMax Code",
"icon": "icon.png",
Expand Down
15 changes: 12 additions & 3 deletions plugins/minimax-ai/minimax-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Copy this directory, including the hidden `.minimax-plugin/` directory, into the

- **Long-lived Bots**: every Bot has a name, a role, instructions and its own Agent session. When its setup, the team roster or its memory changes, the next turn automatically carries the latest context.
- **Chief Bot**: created on first launch to help you build your team. Chief can create, edit and delete Bots and groups; other Bots are refused when they try to do so.
- **Group chats**: the group admin answers by default. `@Name` calls on a specific member, and a Bot that `@`-mentions another member in its reply hands the turn over. Members with nothing to add reply `[PASS]` and are skipped. Each member keeps a separate session per group, and groups cannot see private chats. There is no fixed reply cap by default; a group can set one in the group settings. Discussions finish naturally, or wait for more instructions after repeated handoffs without tool progress.
- **Group chats**: the group admin answers by default. `@Name` calls on a specific member, and a Bot that `@`-mentions another member in its reply hands the turn over. Members with nothing to add reply `[PASS]` and are skipped. Each member keeps a separate session per group, and groups cannot see private chats. There is no fixed reply cap by default; a group can set one in the group settings. A group can also keep a short note of what it said, which survives clearing the chat. Discussions finish naturally, or wait for more instructions after repeated handoffs without tool progress.
- **Hand-offs between Bots**: `message_bot` hands work to another Bot and immediately returns an admission receipt; the completed result returns to the source chat. `post_group` is **private chat only**: it posts to a group and returns immediately while the discussion continues according to the group's reply mode. From a private chat, Chief can post to any group without joining it; other Bots can only post to groups they belong to. During a group turn, `post_group` is disabled for everyone, including Chief and admins, for both same-group and cross-group posts. Use `@Name` in a reply to hand off within the group; start cross-group work from a private chat.
- **Background work**: when an isolated execution context is useful, a Bot calls `start_helper` to run it in the background in a fresh, separate session; the Bot's turn ends at once and you can keep chatting. The Bot simply says something like "let me look into it" without internal jargon or IDs. When the job finishes, its report comes back to the Bot as a new turn, and the Bot reviews it and passes it on in its own words (in groups too, where it can keep handing off with `@`). At most 2 background jobs run at once (one slot is always kept free for chat) and the rest queue; each Bot can have at most 4 queued or running. Running jobs are shown above the chat input and can be stopped at any time; the strip orders them by how far along they are, the ones already working first, then the ones waiting for another job, then the queued ones; when more than three jobs are listed, it keeps the first three in the open and folds the rest into one line that says how many are left, and one button below the rows opens the whole queue; tool approvals from a background job appear in its chat; jobs still running when the Mini App restarts are marked interrupted. Bots can also manage jobs with `list_helpers` / `stop_helper`. "Allow background helpers automatically" is on by default in Bot settings; when off, a Bot only does so when you explicitly ask.
- **Per-Bot model and reasoning effort**: pick a model and reasoning effort in Bot settings (only the levels the model supports are listed, and thinking can be turned off where supported; Bots that follow the default model can still set their own effort). Chief can also set them with the `model` (a model ID, `provider/model` or display name; `default` follows the default) and `effort` (`low`, `high`, `off`, ...) parameters of `create_bot` / `update_bot`. Requires a MiniMax Code version with `agent.models()` / `run({ model })`; older versions prompt you to upgrade.
Expand All @@ -30,6 +30,15 @@ Copy this directory, including the hidden `.minimax-plugin/` directory, into the
- **Reply actions**: the reaction, copy, quote-reply and open-session buttons sit at the bottom-left of a reply, under its last line. A long reply therefore keeps its actions within reach at the end of the text instead of pushing them off the top of the message. The reaction indicator sits directly above that row and is painted with the app's own tag colours, so it follows the light and dark themes.
- **Working messages of one reply**: the Host records a text part for every turn a Bot takes between two tool calls, so a reply that worked for a while arrives with many parts, and the page draws one bubble per part. The bubbles keep the official shape and order, and a small gap between them tells them apart instead of letting them read as one block of text.

## 0.7.10 Optional group notes

- A group can keep a note of what it said. After every round the group records the closing lines of the replies, and every Bot of the group gets that record in its standing brief.
- The note is kept with the group settings, not with the messages. It is still there after the chat was cleared (**Start new chat**), and a Bot that joins the group later reads it too, so a group that was reset does not start from nothing.
- It is prompt text: the note is written from the replies the round already produced, so it costs no extra Agent turn, no extra tool call and no memory write.
- The note is a record, not an agreement. It says what the Bots said at the end of earlier rounds, and every Bot of the group is told exactly that in the prompt.
- The note is off unless you switch it on in the group settings, and a group without a note gets exactly the prompt it got before. Only you can set it: a Bot cannot switch it on, and the record itself is written by the plugin — a Bot cannot write into it.
- The note holds the most recent rounds and stays within a small size limit. The group settings show what was recorded, and you can clear it there.

## 0.7.9 Optional relay guardrails for groups

- A group can set a hop limit in the group settings: how many Bots may speak in one relay chain. `0` (the default) means no limit, exactly as before; the largest limit is 20.
Expand Down Expand Up @@ -103,8 +112,8 @@ Avatars are stored in the Mini App's `dataDir/avatars/`. `GET /api/avatars` list

## Data & access

- **Agent turns**: every Bot reply, scheduled task and background job is an Agent turn run through `context.agent`, with the Agent's full capabilities (files, shell, Skills and MCP, including this plugin's own MCP tools). A tool call that needs approval is shown in the page or in the session's sidebar tab; it runs only after you choose **Allow once**. Nothing is approved automatically. A group with a relay limit spends one more Agent turn on the round that reaches the limit: the wrap-up, which replaces the blocked hand-off.
- **Files written**: only inside the Mini App's `dataDir`: `state.json`, `messages/`, `usage.jsonl`, `memory/` (including its journal, archive and backups), `uploads/` (your attachments) and `avatars/`.
- **Agent turns**: every Bot reply, scheduled task and background job is an Agent turn run through `context.agent`, with the Agent's full capabilities (files, shell, Skills and MCP, including this plugin's own MCP tools). A tool call that needs approval is shown in the page or in the session's sidebar tab; it runs only after you choose **Allow once**. Nothing is approved automatically. A group with a relay limit spends one more Agent turn on the round that reaches the limit: the wrap-up, which replaces the blocked hand-off. A group note spends no turn at all: it is written from the replies the round already produced.
- **Files written**: only inside the Mini App's `dataDir`: `state.json`, `messages/`, `usage.jsonl`, `memory/` (including its journal, archive and backups), `uploads/` (your attachments) and `avatars/`. A group's note is part of that group's record in `state.json`, so it is removed with the group.
- **Files read outside `dataDir`**:
- `/api/media` serves a local file (up to 1 GiB) only when its absolute path appears in a Bot message of that same chat.
- An avatar given as a local image path (by you or by Chief through `update_bot`) is read once and copied into `avatars/`.
Expand Down
30 changes: 30 additions & 0 deletions plugins/minimax-ai/minimax-bot/miniapp/client/panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,36 @@ function groupOverview(group) {
),
h('p', { class: 'hint', style: { marginTop: '8px' } }, t('panels.group.handoverHint')),
),
h(
'section',
null,
h('h3', { class: 'section-title' }, t('panels.group.notesHeading')),
h(
'label',
{ class: 'check' },
h('input', {
type: 'checkbox',
class: 'switch',
name: 'notes',
checked: group.notes === true,
onchange: (event) => patch({ notes: event.target.checked === true }),
}),
h('span', null, t('panels.group.notes')),
),
h('p', { class: 'hint', style: { marginTop: '8px' } }, t('panels.group.notesHint')),
...(group.notes === true && Array.isArray(group.noteEntries) && group.noteEntries.length
? [
h('p', { class: 'hint', style: { marginTop: '8px' } }, t('panels.group.notesList')),
...group.noteEntries.map((entry) => clipped(`[${entry.speaker}] ${entry.text}`)),
h(
'button',
{ type: 'button', class: 'link-row', onclick: () => patch({ clearNotes: true }) },
icon('eraser'),
h('span', { class: 'grow' }, t('panels.group.notesClear')),
),
]
: []),
),
h(
'section',
null,
Expand Down
10 changes: 10 additions & 0 deletions plugins/minimax-ai/minimax-bot/miniapp/client/strings-panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ export default {
'panels.group.relayLimitHint': '0 表示不限:接力链可以一直跑下去。填上数字后(最多 20),这个群里一轮接力最多几个 Bot 说话;到上限时群里会说明本轮收口。「所有人回复」的群里,这个上限连初始成员一起算。',
'panels.group.handover': '把上一位的结论交给下一位',
'panels.group.handoverHint': '下一位的提示词里会带上上一位回复的结论(就是它的尾部),省得下一位再翻一遍工具。上一位的原话本来就在「新消息」里,这里是再强调一遍结论。只多一段提示词,不多花回合。',
'panels.group.notesHeading': '群纪要',
'panels.group.notes': '把本群说过的话记下来',
'panels.group.notesHint': '每轮结束后,群会把各 Bot 回复的结尾记成一份纪要,写在群资料里:清空聊天记录(新建对话)之后纪要还在,新加入的 Bot 也能看到。纪要只是记录,不代表群里已经达成共识。只多一段提示词,不多花回合。',
'panels.group.notesList': '已记录的纪要:',
'panels.group.notesClear': '清空纪要',
'panels.group.settingsKept': '成员设置保留',
'panels.group.needMember': '至少选一个成员',
'panels.group.name': '群名',
Expand Down Expand Up @@ -536,6 +541,11 @@ export default {
'panels.group.relayLimitHint': '0 means no limit: the relay chain can keep going. With a number (20 at most), at most that many Bots speak in one chain in this group; when the limit is reached the group says so and the round wraps up. In a group where every member replies, the limit counts those first replies too.',
'panels.group.handover': 'Hand the previous conclusion to the next Bot',
'panels.group.handoverHint': "The next Bot's prompt carries the conclusion of the previous reply (its tail), so the next Bot does not have to dig the same ground out of the tools again. The previous reply is in the new messages anyway — this repeats the conclusion on purpose. It is prompt text only — no extra turn.",
'panels.group.notesHeading': 'Group notes',
'panels.group.notes': 'Keep a record of what this group said',
'panels.group.notesHint': 'After every round the group records the closing lines of the replies as a note, kept with the group settings: the note is still there after the chat was cleared (Start new chat), and a Bot that joins later sees it too. A note is a record, not an agreement the group reached. It is prompt text only — no extra turn.',
'panels.group.notesList': 'Recorded so far:',
'panels.group.notesClear': 'Clear the note',
'panels.group.settingsKept': 'Member settings are kept',
'panels.group.needMember': 'Pick at least one member',
'panels.group.name': 'Group name',
Expand Down
51 changes: 45 additions & 6 deletions plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UserInbox } from './user-inbox.mjs';
import { TaskDispatcher } from './task-dispatcher.mjs';
import { modelKey, newId, withEffort } from './store.mjs';
import { CONVERGE_MARK, handoverRequested, handoverSummary, hopLimit, renderHandover } from './relay-guard.mjs';
import { appendNote, notesEnabled, renderNote } from './group-note.mjs';

/** @typedef {import('./miniapp-api.js').MiniAppAgentClient} MiniAppAgentClient */
/** @typedef {import('./miniapp-api.js').MiniAppAgentRunEvent} MiniAppAgentRunEvent */
Expand Down Expand Up @@ -323,7 +324,7 @@ export class Engine {
* @param {{ reason: string, max: number }} info
* @param {{ botId: string, name: string, text: string, ts: number } | undefined} last
* @param {{ sourceMessageId?: string, sourceMessageIds?: string[], taskId?: string, signal?: AbortSignal }} [options]
* @returns {Promise<{ ok: boolean, reason: string }>}
* @returns {Promise<{ ok: boolean, reason: string, text: string }>}
*/
async convergeGroup(group, chatId, info, last, options = {}) {
const notice = await this.store.appendMessage(chatId, {
Expand All @@ -338,7 +339,7 @@ export class Engine {
type: 'group.hopLimit', chatId, groupId: group.id, reason: info.reason, max: info.max, hops: info.max,
});
const bot = last?.botId ? this.store.getBot(last.botId) : undefined;
if (!bot) return { ok: false, reason: 'no_speaker' };
if (!bot) return { ok: false, reason: 'no_speaker', text: '' };
const result = await this.runBotTurn({
bot,
chatId,
Expand All @@ -353,9 +354,31 @@ export class Engine {
});
if (result.status !== 'done') {
this.logger.warn('minimax_bot.relay_converge_failed', { code: errorCode(result) || 'unknown' });
return { ok: false, reason: 'turn_failed' };
return { ok: false, reason: 'turn_failed', text: '' };
}
return { ok: true, reason: 'converged' };
return { ok: true, reason: 'converged', text: result.text ?? '' };
}

/**
* Write what the round said into the group's note.
*
* The note lives in the group record, so it survives a reset of the group's chat, and every
* Bot of the group gets it in its standing brief. Nothing here spends a turn: the text is the
* closing part of the replies the group already produced. A group that keeps no note is left
* alone, and so is a round that said nothing.
*
* @param {any} group
* @param {{ speaker: string, text: string }[]} round
*/
async recordGroupNote(group, round) {
if (!round.length) return;
const current = this.store.getGroup(group.id);
if (!current || !notesEnabled(current)) return;
const entries = appendNote(current.noteEntries, round);
if (!entries.length) return;
current.noteEntries = entries;
await this.store.save();
this.broadcast({ type: 'state.changed' });
}

/** The Bot's standing context, written in the user's language so replies default to it. */
Expand All @@ -366,6 +389,9 @@ export class Engine {
// F7 · the group has the relay guardrails on: the prompt gets the one-owner rule. With the
// guardrails off this adds nothing, so the prompt stays byte-identical.
const relayGuard = group ? this.relayGuardActive(group) : false;
// The group's note: what its Bots said at the end of the earlier rounds. It is empty unless
// the group keeps one, so the brief of a group without notes stays byte-identical.
const note = group ? renderNote(group) : '';
const lines = [
'<minimax-bot-context>',
t('brief.intro', { name: bot.name, id: bot.id }),
Expand All @@ -388,6 +414,9 @@ export class Engine {
...(relayGuard ? [t('brief.groupOneOwner')] : []),
].join(t('brief.sentenceJoin'))
: t('brief.private'),
// The note joins the brief only when the group has one. It is a block of its own, so it
// does not disturb the sentence the group line is made of.
...(note ? ['', note] : []),
'',
t('brief.hTeam'),
...botLines,
Expand Down Expand Up @@ -1174,6 +1203,8 @@ export class Engine {
let hops = 0;
/** The conclusion of the last hop, handed over to the next one. */
let lastConclusion;
/** What the Bots said in this round, for the group's note. */
const noteRound = [];
/** Set when the chain stopped because of the hop limit. */
let converged;
try {
Expand Down Expand Up @@ -1217,7 +1248,10 @@ export class Engine {
}
// Waiting on the user (or stopped): the hand-off resumes from answerQuestion once they reply.
if (result.status !== 'done' || result.pendingQuestion) break;
if (result.text) lastConclusion = { botId: bot.id, name: bot.name, text: result.text, ts: Date.now() };
if (result.text) {
lastConclusion = { botId: bot.id, name: bot.name, text: result.text, ts: Date.now() };
noteRound.push({ speaker: bot.name, text: result.text });
}
const next = this.mentionsIn(result.text, fresh, bot.id);
// The limit is reached and the reply calls somebody: that Bot is not called, the chain
// converges. This is the whole point of the limit — a chain that never ends.
Expand All @@ -1234,10 +1268,15 @@ export class Engine {
}
if (converged) {
// The chain stops here: the group is told why, and the Bot that just spoke wraps up.
await this.convergeGroup(group, chatId, converged, lastConclusion, {
const wrap = await this.convergeGroup(group, chatId, converged, lastConclusion, {
sourceMessageId, sourceMessageIds, signal: options.signal, taskId: options.taskId,
});
// The wrap-up is the conclusion of the round, so it belongs in the note.
if (wrap.text && lastConclusion) noteRound.push({ speaker: lastConclusion.name, text: wrap.text });
}
// The group's note: what this round said. It is written from text the group already
// produced, so it costs no turn, and a group that keeps no note is left alone.
await this.recordGroupNote(group, noteRound);
if (paused) {
const note = await this.store.appendMessage(chatId, { role: 'system', key: 'notice.noProgress', text: t('notice.noProgress') });
this.broadcast({ type: 'message.upsert', chatId, message: note });
Expand Down
Loading
Loading