1. Rename tool: whispergateway_tools → whispergate_tools 2. Turn-based speaking mechanism: - New turn-manager.ts maintains per-channel turn state - ChannelPolicy新增turnOrder字段配置发言顺序 - before_model_resolve hook检查当前agent是否为发言人 - 非当前发言人直接切换到no-reply模型 - message_sent hook检测结束符或NO_REPLY时推进turn - message_received检测到human消息时重置turn 3. 注入提示词增强: - buildEndMarkerInstruction增加isGroupChat参数 - 群聊时追加规则:与自己无关时主动回复NO_REPLY 4. Slash command支持: - /whispergate status - 查看频道策略 - /whispergate turn-status - 查看轮流状态 - /whispergate turn-advance - 手动推进轮流 - /whispergate turn-reset - 重置轮流顺序
137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
/**
|
|
* Turn-based speaking manager for group channels.
|
|
*
|
|
* Maintains per-channel turn order so that only one agent speaks at a time.
|
|
* When the current speaker finishes (end symbol or NO_REPLY), the turn advances
|
|
* to the next agent in the rotation.
|
|
*/
|
|
|
|
export type TurnPolicy = {
|
|
/** Ordered list of Discord account IDs (bot user IDs) that participate in turn rotation */
|
|
turnOrder: string[];
|
|
};
|
|
|
|
export type ChannelTurnState = {
|
|
/** Index into turnOrder for the current speaker */
|
|
currentIndex: number;
|
|
/** Timestamp of last turn advance */
|
|
lastAdvancedAt: number;
|
|
};
|
|
|
|
/** In-memory turn state per channel */
|
|
const channelTurns = new Map<string, ChannelTurnState>();
|
|
|
|
/** Turn policies per channel (loaded from channel policies) */
|
|
const turnPolicies = new Map<string, TurnPolicy>();
|
|
|
|
/** Turn timeout: if the current speaker hasn't responded in this time, auto-advance */
|
|
const TURN_TIMEOUT_MS = 30_000;
|
|
|
|
export function setTurnPolicy(channelId: string, policy: TurnPolicy | undefined): void {
|
|
if (!policy || !policy.turnOrder?.length) {
|
|
turnPolicies.delete(channelId);
|
|
channelTurns.delete(channelId);
|
|
return;
|
|
}
|
|
turnPolicies.set(channelId, policy);
|
|
// Initialize turn state if not exists
|
|
if (!channelTurns.has(channelId)) {
|
|
channelTurns.set(channelId, { currentIndex: 0, lastAdvancedAt: Date.now() });
|
|
}
|
|
}
|
|
|
|
export function getTurnPolicy(channelId: string): TurnPolicy | undefined {
|
|
return turnPolicies.get(channelId);
|
|
}
|
|
|
|
export function getTurnState(channelId: string): ChannelTurnState | undefined {
|
|
return channelTurns.get(channelId);
|
|
}
|
|
|
|
/**
|
|
* Check if the given accountId is the current speaker for this channel.
|
|
* Returns: { isSpeaker: true } or { isSpeaker: false, reason: string }
|
|
*/
|
|
export function checkTurn(channelId: string, accountId: string): { isSpeaker: boolean; currentSpeaker?: string; reason: string } {
|
|
const policy = turnPolicies.get(channelId);
|
|
if (!policy) {
|
|
return { isSpeaker: true, reason: "no_turn_policy" };
|
|
}
|
|
|
|
const order = policy.turnOrder;
|
|
if (!order.includes(accountId)) {
|
|
// Not in turn order — could be human or unmanaged agent, allow through
|
|
return { isSpeaker: true, reason: "not_in_turn_order" };
|
|
}
|
|
|
|
let state = channelTurns.get(channelId);
|
|
if (!state) {
|
|
state = { currentIndex: 0, lastAdvancedAt: Date.now() };
|
|
channelTurns.set(channelId, state);
|
|
}
|
|
|
|
// Auto-advance if turn has timed out
|
|
const now = Date.now();
|
|
if (now - state.lastAdvancedAt > TURN_TIMEOUT_MS) {
|
|
advanceTurn(channelId);
|
|
state = channelTurns.get(channelId)!;
|
|
}
|
|
|
|
const currentSpeaker = order[state.currentIndex];
|
|
if (accountId === currentSpeaker) {
|
|
return { isSpeaker: true, currentSpeaker, reason: "is_current_speaker" };
|
|
}
|
|
|
|
return { isSpeaker: false, currentSpeaker, reason: "not_current_speaker" };
|
|
}
|
|
|
|
/**
|
|
* Advance turn to the next agent in rotation.
|
|
* Returns the new current speaker's accountId.
|
|
*/
|
|
export function advanceTurn(channelId: string): string | undefined {
|
|
const policy = turnPolicies.get(channelId);
|
|
if (!policy) return undefined;
|
|
|
|
const order = policy.turnOrder;
|
|
let state = channelTurns.get(channelId);
|
|
if (!state) {
|
|
state = { currentIndex: 0, lastAdvancedAt: Date.now() };
|
|
channelTurns.set(channelId, state);
|
|
return order[0];
|
|
}
|
|
|
|
state.currentIndex = (state.currentIndex + 1) % order.length;
|
|
state.lastAdvancedAt = Date.now();
|
|
return order[state.currentIndex];
|
|
}
|
|
|
|
/**
|
|
* Reset turn to the first agent (e.g., when a human sends a message).
|
|
*/
|
|
export function resetTurn(channelId: string): void {
|
|
const state = channelTurns.get(channelId);
|
|
if (state) {
|
|
state.currentIndex = 0;
|
|
state.lastAdvancedAt = Date.now();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get debug info for a channel's turn state.
|
|
*/
|
|
export function getTurnDebugInfo(channelId: string): Record<string, unknown> {
|
|
const policy = turnPolicies.get(channelId);
|
|
const state = channelTurns.get(channelId);
|
|
if (!policy) return { channelId, hasTurnPolicy: false };
|
|
return {
|
|
channelId,
|
|
hasTurnPolicy: true,
|
|
turnOrder: policy.turnOrder,
|
|
currentIndex: state?.currentIndex ?? 0,
|
|
currentSpeaker: policy.turnOrder[state?.currentIndex ?? 0],
|
|
lastAdvancedAt: state?.lastAdvancedAt,
|
|
timeSinceAdvanceMs: state ? Date.now() - state.lastAdvancedAt : null,
|
|
};
|
|
}
|