Skip to content
Open
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.15",
"version": "0.7.16",
"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
5 changes: 3 additions & 2 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. The `@` needs no space in front of it, so `请@某人` calls that member. 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.
- **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. The `@` needs no space in front of it, so `请@某人` calls that member. A character that could still be part of the name keeps it open, so `@某人帮忙` calls nobody and `@设计师傅` is not 设计师, while a space or punctuation after the name calls it (`@某人 帮忙`). 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 Down Expand Up @@ -63,7 +63,8 @@ An avatar may come from the avatar library or from an image uploaded in the page

## 0.7.13 A mention that reads like Chinese, and marks that stay out

- A mention works right after a Chinese word. The character in front of the `@` used to end the mention whenever it was a letter of any script, and Chinese has no spaces between words, so `请@某人` and `好的@某人` reached nobody. Only an ASCII letter, digit or underscore in front of the `@` ends it now, which is what the local part of an email address is made of. An address is still not a mention, a name that runs into more words (`@Alicex`) still is not one either, and English is unchanged: `mail@alice.com` and `ok@alice` reach nobody, while `@alice`, `@alice.` and `@Alice Smith` do.
- A mention works right after a Chinese word. The character in front of the `@` used to end the mention whenever it was a letter of any script, and Chinese has no spaces between words, so `请@某人` and `好的@某人` reached nobody. Any letter, digit, underscore or combining mark in front of the `@` ends it now, in every script that writes with spaces: `josé@alice` and `Привет@alice` reach nobody, the way `mail@alice.com` does. Chinese, Japanese and Korean write a word right against the `@`, so their characters do not swallow the mention and `请@某人` still calls that member. A name that runs into more words (`@Alicex`) still is not a mention either, and English is unchanged: `mail@alice.com` and `ok@alice` reach nobody, while `@alice`, `@alice.` and `@Alice Smith` do.
- The side after a name stays closed: a character that could still be part of the name keeps the name open. `@某人帮忙` calls nobody, while `@某人 帮忙` and `@某人,帮忙` call 某人. The same boundary is what keeps `@设计师傅` from calling 设计师. Put a space or punctuation after the name when more text follows it.
- A name, a role, a notice, an instruction or a memory entry cannot close a block of the plugin's own. Those travel into the standing brief of every Bot of the group and into the memory block, and a protocol mark in one of them used to close the block early and let the text after it read as if the plugin had written it. A reply in a group travels into the next Bot's prompt the same way, so a mark in it cannot open a block of its own there either. The marks go the way they go in a reply: out of the text, the words around them untouched.
- The brief of a background job gets the same treatment. The Bot's name and role are written into that brief's block, so a protocol mark in either of them used to close it early as well; both are now cleaned the same way before they go in.

Expand Down
12 changes: 7 additions & 5 deletions plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1178,13 +1178,15 @@ export class Engine {
.sort((a, b) => b.name.length - a.name.length);
// A word character of any script: it ends a name, so "Alicex" is not "Alice".
const isWord = (value) => Boolean(value && /[\p{L}\p{N}\p{M}_]/u.test(value));
// An ASCII word character is what makes an '@' part of a handle: the local part of an
// email address. Chinese has no spaces between words, so a character before the '@' is a
// word of its own there ("请@某人"), and it must not swallow the mention.
const isAsciiWord = (value) => Boolean(value && /[A-Za-z0-9_]/.test(value));
// Chinese, Japanese kana and Korean write a word right up against the '@' without a space
// ("请@某人"), so a character of those scripts does not belong to an address and must not
// swallow the mention. Any other word character in front of the '@' does belong to one
// ("mail@alice.com", "josé@alice").
const isSpaceless = (value) => Boolean(value && /[\p{Script_Extensions=Han}\p{Script_Extensions=Hiragana}\p{Script_Extensions=Katakana}\p{Script_Extensions=Hangul}]/u.test(value));
const isAddressChar = (value) => isWord(value) && !isSpaceless(value);
const found = [];
for (let index = lower.indexOf('@'); index >= 0; index = lower.indexOf('@', index + 1)) {
if (isAsciiWord(lower[index - 1])) continue;
if (isAddressChar(lower[index - 1])) continue;
const member = members.find(({ name }) => lower.startsWith(name, index + 1) && !isWord(lower[index + 1 + name.length]));
if (!member) continue;
if (member.id !== excludeId && !found.includes(member.id)) found.push(member.id);
Expand Down
2 changes: 1 addition & 1 deletion plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ export function createMcpHandler({ team, engine }) {
result = {
protocolVersion: PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0],
capabilities: { tools: { listChanged: false } },
serverInfo: { name: 'minimax-bot', version: '0.7.15' },
serverInfo: { name: 'minimax-bot', version: '0.7.16' },
instructions: t('mcp.instructions'),
};
break;
Expand Down
2 changes: 1 addition & 1 deletion plugins/minimax-ai/minimax-bot/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "minimax-bot",
"version": "0.7.15",
"version": "0.7.16",
"private": true,
"type": "module",
"mcode": {
Expand Down
70 changes: 70 additions & 0 deletions plugins/minimax-ai/minimax-bot/tests/mention-nonascii.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// @ts-check
// The "@" of a mention when a letter of another script stands in front of it.
//
// Chinese, Japanese kana and Korean write a word right up against the "@" ("请@某人"), so a
// character of those scripts does not swallow the mention. A letter of any other script is
// still the local part of an address: "josé@alice" is an address and calls nobody, the same
// way "mail@alice.com" does not. The name itself still has to end at the "@", so "@alicex" is
// not Alice either.

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { Store } from '../miniapp/node/store.mjs';
import { MemoryStore } from '../miniapp/node/memory.mjs';
import { Engine } from '../miniapp/node/engine.mjs';

async function fixture(t) {
const dir = await mkdtemp(join(tmpdir(), 'bot-mention-nonascii-'));
const store = new Store(dir);
const memory = new MemoryStore(dir);
await store.init();
await memory.init();
const engine = new Engine({
store, memory, logger: { warn() {} }, broadcast: () => {}, retryDelaysMs: [],
});
t.after(async () => {
engine.dispose();
await rm(dir, { recursive: true, force: true });
});
store.state.bots = [
{ id: 'alice', name: 'Alice' },
{ id: 'bob', name: 'Bob' },
];
return { store, engine, group: { memberIds: ['alice', 'bob'] } };
}

test('a letter of a script that writes with spaces makes an address, not a mention', async (t) => {
const { engine, group } = await fixture(t);
// Accented Latin letters, in both compositions.
assert.deepEqual(engine.mentionsIn('josé@alice', group), []);
assert.deepEqual(engine.mentionsIn('José@alice', group), []);
assert.deepEqual(engine.mentionsIn('jose\u0301@alice', group), []);
// Greek and Cyrillic letters.
assert.deepEqual(engine.mentionsIn('Παύλος@alice', group), []);
assert.deepEqual(engine.mentionsIn('Привет@alice', group), []);
// A mention later in the same text is still found.
assert.deepEqual(engine.mentionsIn('josé@alice @bob', group), ['bob']);
});

test('Chinese, Japanese and Korean write up against the "@" and still reach the member', async (t) => {
const { engine, group } = await fixture(t);
assert.deepEqual(engine.mentionsIn('请@alice', group), ['alice']);
assert.deepEqual(engine.mentionsIn('(@alice)', group), ['alice']);
assert.deepEqual(engine.mentionsIn(',@alice', group), ['alice']);
assert.deepEqual(engine.mentionsIn('@alice', group), ['alice']);
assert.deepEqual(engine.mentionsIn('日本語@alice', group), ['alice']);
assert.deepEqual(engine.mentionsIn('カタカナ@alice', group), ['alice']);
assert.deepEqual(engine.mentionsIn('한국어@alice', group), ['alice']);
});

test('the address forms stay silent', async (t) => {
const { engine, group } = await fixture(t);
assert.deepEqual(engine.mentionsIn('mail@alice.com', group), []);
assert.deepEqual(engine.mentionsIn('@alicex', group), []);
assert.deepEqual(engine.mentionsIn('1@alice', group), []);
assert.deepEqual(engine.mentionsIn('_@alice', group), []);
});
10 changes: 5 additions & 5 deletions plugins/minimax-ai/minimax-bot/tests/mention-text.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
//
// A mention is "@" plus a member's name. Chinese has no spaces between words, so the character
// in front of the "@" is a word of its own ("请@某人") and must not swallow the mention the way
// it does in "mail@alice.com". What makes an "@" part of a handle is an ASCII word character in
// front of it, so that is what the guard looks at. English keeps the old rule: an ASCII letter,
// digit or underscore in front of the "@" still ends the mention, and the address, the longer
// name and the name that runs into more words are all as they were.
// it does in "mail@alice.com". What makes an "@" part of an address is a word character of a
// script that writes with spaces, so that is what the guard looks at. English keeps the old
// rule: an ASCII letter, digit or underscore in front of the "@" still ends the mention, and
// the address, the longer name and the name that runs into more words are all as they were.

import assert from 'node:assert/strict';
import { test } from 'node:test';
Expand Down Expand Up @@ -72,7 +72,7 @@ test('a mention in English stays the way it was', async (t) => {

test('an English address is still not a mention', async (t) => {
const { engine, group } = await fixture(t);
// The local part of an address is ASCII, so it ends the mention the way it always did.
// An ASCII local part, so it ends the mention the way it always did.
assert.deepEqual(engine.mentionsIn('mail@alice.com', group), []);
assert.deepEqual(engine.mentionsIn('user.name@alice.com', group), []);
assert.deepEqual(engine.mentionsIn('mail@设计师', group), []);
Expand Down
57 changes: 57 additions & 0 deletions plugins/minimax-ai/minimax-bot/tests/mention-trailing.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @ts-check
// A Chinese name stays open while more of the same script follows it.
//
// Chinese writes without spaces, so a name cannot be told apart from the words around it by
// whitespace alone: after "@某人", the next character could belong to the name or start a new
// word. The mention ends only at a character of another kind, which is what keeps "@设计师傅"
// from calling 设计师. The trade-off is that "@某人帮忙" is read as a name that is still going
// and calls nobody; a space or punctuation after the name calls the member.

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { Store } from '../miniapp/node/store.mjs';
import { MemoryStore } from '../miniapp/node/memory.mjs';
import { Engine } from '../miniapp/node/engine.mjs';

async function fixture(t) {
const dir = await mkdtemp(join(tmpdir(), 'bot-mention-trailing-'));
const store = new Store(dir);
const memory = new MemoryStore(dir);
await store.init();
await memory.init();
const engine = new Engine({
store, memory, logger: { warn() {} }, broadcast: () => {}, retryDelaysMs: [],
});
t.after(async () => {
engine.dispose();
await rm(dir, { recursive: true, force: true });
});
store.state.bots = [
{ id: 'someone', name: '某人' },
{ id: 'designer', name: '设计师' },
];
return { store, engine, group: { memberIds: ['someone', 'designer'] } };
}

test('a Chinese name that runs into more Chinese words is not a mention', async (t) => {
const { engine, group } = await fixture(t);
// The character after the name could still be part of it, so nobody is called.
assert.deepEqual(engine.mentionsIn('@某人帮忙', group), []);
assert.deepEqual(engine.mentionsIn('请@某人帮忙看一下', group), []);
// The same boundary keeps a longer name from calling the shorter one it starts with.
assert.deepEqual(engine.mentionsIn('@设计师傅', group), []);
assert.deepEqual(engine.mentionsIn('请@设计师傅做', group), []);
});

test('a space or punctuation after the Chinese name calls the member', async (t) => {
const { engine, group } = await fixture(t);
assert.deepEqual(engine.mentionsIn('@某人 帮忙', group), ['someone']);
assert.deepEqual(engine.mentionsIn('请@某人 帮忙', group), ['someone']);
assert.deepEqual(engine.mentionsIn('@某人,帮忙', group), ['someone']);
assert.deepEqual(engine.mentionsIn('请@某人。', group), ['someone']);
assert.deepEqual(engine.mentionsIn('@设计师 看一下', group), ['designer']);
});
Loading