feat: wire channel mode runtime config and docs

This commit is contained in:
zhi
2026-04-02 04:48:20 +00:00
parent 8073c33f2c
commit d44204fabf
9 changed files with 60 additions and 20 deletions

View File

@@ -30,6 +30,9 @@ Optional:
- `channelPoliciesFile` (per-channel overrides in a standalone JSON file)
- `schedulingIdentifier` (default `➡️`) — moderator handoff identifier
- `enableDirigentPolicyTool` (default true)
- `multiMessageStartMarker` (default `↗️`)
- `multiMessageEndMarker` (default `↙️`)
- `multiMessagePromptMarker` (default `⤵️`)
Unified optional tool:
- `dirigent_tools`
@@ -59,6 +62,15 @@ When the current speaker NO_REPLYs, the moderator bot sends: `<@NEXT_USER_ID>➡
This is a non-semantic scheduling message. The scheduling identifier (`➡️` by default) carries no meaning — it simply signals the next agent to check chat history and decide whether to speak.
## Multi-message mode / shuffle mode
- Human sends the configured start marker (default `↗️`) → channel enters multi-message mode.
- While active, agents are forced to no-reply and the moderator sends only the configured prompt marker (default `⤵️`) after each additional human message.
- Human sends the configured end marker (default `↙️`) → channel exits multi-message mode and normal scheduling resumes.
- No separate moderator "entered/exited mode" confirmation message is sent; the markers themselves are the protocol.
- The first moderator message after exit uses the normal scheduling handoff format: `<@NEXT_USER_ID>➡️`.
- `/dirigent turn-shuffling`, `/dirigent turn-shuffling on`, and `/dirigent turn-shuffling off` control per-channel reshuffling between completed rounds.
## Slash command (Discord)
```
@@ -66,6 +78,9 @@ This is a non-semantic scheduling message. The scheduling identifier (`➡️` b
/dirigent turn-status
/dirigent turn-advance
/dirigent turn-reset
/dirigent turn-shuffling
/dirigent turn-shuffling on
/dirigent turn-shuffling off
```
Debug logging:

View File

@@ -1,10 +1,7 @@
export type ChannelMode = "normal" | "multi-message";
import type { ChannelRuntimeMode, ChannelRuntimeState } from "../rules.js";
export type ChannelModesState = {
mode: ChannelMode;
shuffling: boolean;
lastShuffledAt?: number;
};
export type ChannelMode = ChannelRuntimeMode;
export type ChannelModesState = ChannelRuntimeState;
const channelStates = new Map<string, ChannelModesState>();
@@ -48,4 +45,4 @@ export function markLastShuffled(channelId: string): void {
const state = getChannelState(channelId);
state.lastShuffledAt = Date.now();
channelStates.set(channelId, state);
}
}

View File

@@ -1,7 +1,6 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { resolvePolicy, type DirigentConfig } from "../rules.js";
import { getTurnDebugInfo, onSpeakerDone, setWaitingForHuman } from "../turn-manager.js";
import { isMultiMessageMode } from "../core/channel-modes.js";
type DebugConfig = {
enableDebugLogs?: boolean;
@@ -20,6 +19,7 @@ type BeforeMessageWriteDeps = {
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean;
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise<void> | void;
resolveDiscordUserId: (api: OpenClawPluginApi, accountId: string) => string | undefined;
isMultiMessageMode: (channelId: string) => boolean;
sendModeratorMessage: (
botToken: string,
channelId: string,
@@ -45,6 +45,7 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
shouldDebugLog,
ensureTurnOrder,
resolveDiscordUserId,
isMultiMessageMode,
sendModeratorMessage,
discussionService,
} = deps;

View File

@@ -2,7 +2,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { evaluateDecision, type Decision, type DirigentConfig } from "../rules.js";
import { checkTurn } from "../turn-manager.js";
import { deriveDecisionInputFromPrompt } from "../decision-input.js";
import { isMultiMessageMode } from "../core/channel-modes.js";
type DebugConfig = {
enableDebugLogs?: boolean;
@@ -30,6 +29,7 @@ type BeforeModelResolveDeps = {
pruneDecisionMap: () => void;
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean;
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise<void> | void;
isMultiMessageMode: (channelId: string) => boolean;
discussionService?: {
isClosedDiscussion: (channelId: string) => boolean;
};
@@ -51,6 +51,7 @@ export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): vo
pruneDecisionMap,
shouldDebugLog,
ensureTurnOrder,
isMultiMessageMode,
discussionService,
} = deps;

View File

@@ -2,7 +2,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { onNewMessage, setMentionOverride, getTurnDebugInfo } from "../turn-manager.js";
import { extractDiscordChannelId } from "../channel-resolver.js";
import type { DirigentConfig } from "../rules.js";
import { enterMultiMessageMode, exitMultiMessageMode, isMultiMessageMode } from "../core/channel-modes.js";
type DebugConfig = {
enableDebugLogs?: boolean;
@@ -19,6 +18,8 @@ type MessageReceivedDeps = {
recordChannelAccount: (api: OpenClawPluginApi, channelId: string, accountId: string) => boolean;
extractMentionedUserIds: (content: string) => string[];
buildUserIdToAccountIdMap: (api: OpenClawPluginApi) => Map<string, string>;
enterMultiMessageMode: (channelId: string) => void;
exitMultiMessageMode: (channelId: string) => void;
discussionService?: {
maybeReplyClosedChannel: (channelId: string, senderId?: string) => Promise<boolean>;
};
@@ -35,6 +36,8 @@ export function registerMessageReceivedHook(deps: MessageReceivedDeps): void {
recordChannelAccount,
extractMentionedUserIds,
buildUserIdToAccountIdMap,
enterMultiMessageMode,
exitMultiMessageMode,
discussionService,
} = deps;

View File

@@ -18,6 +18,7 @@ import { debugCtxSummary, pickDefined, shouldDebugLog } from "./core/utils.js";
import { resolveDiscordUserId, sendModeratorMessage } from "./core/moderator-discord.js";
import { startNoReplyApi, stopNoReplyApi } from "./core/no-reply-process.js";
import { createDiscussionService } from "./core/discussion-service.js";
import { enterMultiMessageMode, exitMultiMessageMode, isMultiMessageMode } from "./core/channel-modes.js";
import {
DECISION_TTL_MS,
forceNoReplySessions,
@@ -49,6 +50,9 @@ function normalizePluginConfig(api: OpenClawPluginApi): NormalizedDirigentConfig
noReplyPort: 8787,
schedulingIdentifier: "➡️",
waitIdentifier: "👤",
multiMessageStartMarker: "↗️",
multiMessageEndMarker: "↙️",
multiMessagePromptMarker: "⤵️",
...(api.pluginConfig || {}),
} as NormalizedDirigentConfig;
}
@@ -146,6 +150,8 @@ export default {
recordChannelAccount,
extractMentionedUserIds,
buildUserIdToAccountIdMap,
enterMultiMessageMode,
exitMultiMessageMode,
discussionService,
});
@@ -164,6 +170,7 @@ export default {
pruneDecisionMap,
shouldDebugLog,
ensureTurnOrder,
isMultiMessageMode,
discussionService,
});
@@ -203,6 +210,7 @@ export default {
shouldDebugLog,
ensureTurnOrder,
resolveDiscordUserId,
isMultiMessageMode,
sendModeratorMessage,
discussionService,
});

View File

@@ -12,6 +12,12 @@ export type DirigentConfig = {
schedulingIdentifier?: string;
/** Wait identifier: agent ends with this when waiting for a human reply (default: 👤) */
waitIdentifier?: string;
/** Human-visible marker that enters multi-message mode for a channel (default: ↗️) */
multiMessageStartMarker?: string;
/** Human-visible marker that exits multi-message mode for a channel (default: ↙️) */
multiMessageEndMarker?: string;
/** Moderator marker sent after each human message while multi-message mode is active (default: ⤵️) */
multiMessagePromptMarker?: string;
noReplyProvider: string;
noReplyModel: string;
noReplyPort?: number;
@@ -19,6 +25,14 @@ export type DirigentConfig = {
moderatorBotToken?: string;
};
export type ChannelRuntimeMode = "normal" | "multi-message";
export type ChannelRuntimeState = {
mode: ChannelRuntimeMode;
shuffling: boolean;
lastShuffledAt?: number;
};
export type ChannelPolicy = {
listMode?: "human-list" | "agent-list";
humanList?: string[];

View File

@@ -11,7 +11,7 @@
* - If sender IS in turn order → current = next after sender
*/
import { isMultiMessageMode, exitMultiMessageMode } from "./core/channel-modes.js";
import { getChannelShuffling, isMultiMessageMode, markLastShuffled } from "./core/channel-modes.js";
export type ChannelTurnState = {
/** Ordered accountIds for this channel (auto-populated, shuffled) */
@@ -379,6 +379,7 @@ export function onSpeakerDone(channelId: string, accountId: string, wasNoReply:
const newOrder = reshuffleTurnOrder(channelId, state.turnOrder, prevSpeaker);
if (newOrder !== state.turnOrder) {
state.turnOrder = newOrder;
markLastShuffled(channelId);
console.log(`[dirigent][turn-debug] reshuffled turn order for channel=${channelId} newOrder=${JSON.stringify(newOrder)}`);
}
}