feat: wait for human reply (waitIdentifier 👤)
- Add waitIdentifier config (default: 👤) to DirigentConfig and plugin schema - Prompt injection: tells agents to end with 👤 when they need a human reply, warns to use sparingly (only when human is actively participating) - Detection in before_message_write and message_sent hooks - Turn manager: new waitingForHuman state - checkTurn() blocks all agents when waiting - onNewMessage() clears state on human message - Non-human messages ignored while waiting - resetTurn() also clears waiting state - All agents routed to no-reply model during waiting state - Update docs (FEAT.md, CHANGELOG.md, TASKLIST.md, README.md)
This commit is contained in:
@@ -25,6 +25,9 @@ export type ChannelTurnState = {
|
||||
savedTurnOrder?: string[];
|
||||
/** First agent in override cycle; used to detect cycle completion */
|
||||
overrideFirstAgent?: string;
|
||||
// ── Wait-for-human state ──
|
||||
/** When true, an agent used the wait identifier — all agents should stay silent until a human speaks */
|
||||
waitingForHuman: boolean;
|
||||
};
|
||||
|
||||
const channelTurns = new Map<string, ChannelTurnState>();
|
||||
@@ -64,6 +67,7 @@ export function initTurnOrder(channelId: string, botAccountIds: string[]): void
|
||||
currentSpeaker: null, // start dormant
|
||||
noRepliedThisCycle: new Set(),
|
||||
lastChangedAt: Date.now(),
|
||||
waitingForHuman: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,6 +84,11 @@ export function checkTurn(channelId: string, accountId: string): {
|
||||
return { allowed: true, currentSpeaker: null, reason: "no_turn_state" };
|
||||
}
|
||||
|
||||
// Waiting for human → block all agents
|
||||
if (state.waitingForHuman) {
|
||||
return { allowed: false, currentSpeaker: null, reason: "waiting_for_human" };
|
||||
}
|
||||
|
||||
// Not in turn order (human or unknown) → always allowed
|
||||
if (!state.turnOrder.includes(accountId)) {
|
||||
return { allowed: true, currentSpeaker: state.currentSpeaker, reason: "not_in_turn_order" };
|
||||
@@ -122,7 +131,8 @@ export function onNewMessage(channelId: string, senderAccountId: string | undefi
|
||||
if (!state || state.turnOrder.length === 0) return;
|
||||
|
||||
if (isHuman) {
|
||||
// Human message without @mentions: restore original order if overridden, activate from first
|
||||
// Human message: clear wait-for-human, restore original order if overridden, activate from first
|
||||
state.waitingForHuman = false;
|
||||
restoreOriginalOrder(state);
|
||||
state.currentSpeaker = state.turnOrder[0];
|
||||
state.noRepliedThisCycle = new Set();
|
||||
@@ -130,6 +140,11 @@ export function onNewMessage(channelId: string, senderAccountId: string | undefi
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.waitingForHuman) {
|
||||
// Waiting for human — ignore non-human messages
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.currentSpeaker !== null) {
|
||||
// Already active, no change needed from incoming message
|
||||
return;
|
||||
@@ -204,6 +219,27 @@ export function hasMentionOverride(channelId: string): boolean {
|
||||
return !!state?.savedTurnOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the channel to "waiting for human" state.
|
||||
* All agents will be routed to no-reply until a human sends a message.
|
||||
*/
|
||||
export function setWaitingForHuman(channelId: string): void {
|
||||
const state = channelTurns.get(channelId);
|
||||
if (!state) return;
|
||||
state.waitingForHuman = true;
|
||||
state.currentSpeaker = null;
|
||||
state.noRepliedThisCycle = new Set();
|
||||
state.lastChangedAt = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the channel is waiting for a human reply.
|
||||
*/
|
||||
export function isWaitingForHuman(channelId: string): boolean {
|
||||
const state = channelTurns.get(channelId);
|
||||
return !!state?.waitingForHuman;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the current speaker finishes (end symbol detected) or says NO_REPLY.
|
||||
* @param wasNoReply - true if the speaker said NO_REPLY (empty/silent)
|
||||
@@ -288,6 +324,7 @@ export function resetTurn(channelId: string): void {
|
||||
restoreOriginalOrder(state);
|
||||
state.currentSpeaker = null;
|
||||
state.noRepliedThisCycle = new Set();
|
||||
state.waitingForHuman = false;
|
||||
state.lastChangedAt = Date.now();
|
||||
}
|
||||
}
|
||||
@@ -306,6 +343,7 @@ export function getTurnDebugInfo(channelId: string): Record<string, unknown> {
|
||||
noRepliedThisCycle: [...state.noRepliedThisCycle],
|
||||
lastChangedAt: state.lastChangedAt,
|
||||
dormant: state.currentSpeaker === null,
|
||||
waitingForHuman: state.waitingForHuman,
|
||||
hasOverride: !!state.savedTurnOrder,
|
||||
overrideFirstAgent: state.overrideFirstAgent || null,
|
||||
savedTurnOrder: state.savedTurnOrder || null,
|
||||
|
||||
Reference in New Issue
Block a user