Add engagement-only conversation mode
This commit is contained in:
1 parent
b7fcc7db05
commit
7af7b211fa
24 files changed
+204
-23
No files matched your search
@@ -29,6 +29,7 @@ import type { ConversationLearningProvider, LearningStage, RetrievedLearningExam
|
||||
|
||||
export type ConversationAutomationResultKind =
|
||||
| 'sent'
|
||||
| 'engaged'
|
||||
| 'human-required'
|
||||
| 'boundary'
|
||||
| 'image-received'
|
||||
@@ -111,7 +112,8 @@ const PENDING_REPLY_RETRY_DELAY_MS = 2 * 60_000;
|
||||
/**
|
||||
* Sends at most one reply for each newly received user message. Durable state
|
||||
* makes retries and restarts idempotent. The opener subreddit selects a
|
||||
* snapshotted legacy, autonomous R4R, hybrid R4R-to-media, or manual policy.
|
||||
* snapshotted legacy, engagement-only, autonomous R4R, hybrid R4R-to-media,
|
||||
* or manual policy.
|
||||
*/
|
||||
export class ConversationAutomationService {
|
||||
private processing = false;
|
||||
@@ -271,6 +273,9 @@ export class ConversationAutomationService {
|
||||
this.logger.info(`[automation] Paused u/${conversation.username}; an operator-authored message was observed`);
|
||||
return null;
|
||||
}
|
||||
if (state.automation.policyMode === 'engagement' && state.automation.status === 'active') {
|
||||
return this.completeEngagement(conversation, conversationId, state, incomingKey);
|
||||
}
|
||||
const existingClaim = this.claims.getClaim?.(conversationId, incomingKey);
|
||||
if (existingClaim?.status === 'pending') {
|
||||
const expectedReplyFingerprint = existingClaim.replyText
|
||||
@@ -900,6 +905,34 @@ export class ConversationAutomationService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Records the first verified response as the terminal goal of engagement mode. */
|
||||
private completeEngagement(
|
||||
conversation: ActiveChat,
|
||||
conversationId: string,
|
||||
state: ConversationState,
|
||||
incomingKey: string,
|
||||
): ConversationAutomationResult {
|
||||
state.automation.status = 'engaged';
|
||||
state.automation.lastIncomingKey = incomingKey;
|
||||
state.automation.handoffReason = 'The recipient replied; the engagement goal is complete.';
|
||||
this.states.save(conversationId, state);
|
||||
this.claims.markSuperseded?.(conversationId, incomingKey);
|
||||
try {
|
||||
this.options.triage?.setStatus(conversationId, 'done', null, this.now());
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`[automation] Completed engagement with u/${conversation.username} but could not resolve triage state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
this.logger.info(`[automation] Engagement complete for u/${conversation.username}; no automated reply will be sent`);
|
||||
return {
|
||||
conversationId,
|
||||
username: conversation.username,
|
||||
kind: 'engaged',
|
||||
remainingTurns: 0,
|
||||
};
|
||||
}
|
||||
|
||||
private finish(
|
||||
conversation: ActiveChat,
|
||||
conversationId: string,
|
||||
|
||||
@@ -48,8 +48,8 @@ export interface ConversationState {
|
||||
|
||||
export const LEGACY_AUTOMATED_CONVERSATION_TURNS = 3;
|
||||
export const DEFAULT_HYBRID_RAPPORT_TURNS = 3;
|
||||
export const CONVERSATION_POLICY_VERSION = 4;
|
||||
export type ConversationPolicyMode = 'legacy' | 'r4r' | 'r4r-media' | 'manual';
|
||||
export const CONVERSATION_POLICY_VERSION = 5;
|
||||
export type ConversationPolicyMode = 'legacy' | 'engagement' | 'r4r' | 'r4r-media' | 'manual';
|
||||
export type HybridConversationPhase = 'rapport' | 'media-ask' | 'post-media-ask';
|
||||
|
||||
/**
|
||||
@@ -60,6 +60,7 @@ export type HybridConversationPhase = 'rapport' | 'media-ask' | 'post-media-ask'
|
||||
*/
|
||||
export type ConversationAutomationStatus =
|
||||
| 'active'
|
||||
| 'engaged'
|
||||
| 'image-received'
|
||||
| 'human-required'
|
||||
| 'boundary'
|
||||
@@ -308,6 +309,7 @@ function parseAutomationState(value: unknown): ConversationAutomationState {
|
||||
if (!value || typeof value !== 'object') return createConversationAutomationState();
|
||||
const parsed = value as Partial<ConversationAutomationState>;
|
||||
const policyMode: ConversationPolicyMode = parsed.policyMode === 'r4r'
|
||||
|| parsed.policyMode === 'engagement'
|
||||
|| parsed.policyMode === 'r4r-media'
|
||||
|| parsed.policyMode === 'manual'
|
||||
? parsed.policyMode
|
||||
@@ -316,7 +318,7 @@ function parseAutomationState(value: unknown): ConversationAutomationState {
|
||||
? Math.max(1, Math.min(20, Math.trunc(parsed.maxAutomatedTurns)))
|
||||
: LEGACY_AUTOMATED_CONVERSATION_TURNS;
|
||||
const status: ConversationAutomationStatus =
|
||||
parsed.status === 'image-received' || parsed.status === 'human-required' || parsed.status === 'boundary'
|
||||
parsed.status === 'engaged' || parsed.status === 'image-received' || parsed.status === 'human-required' || parsed.status === 'boundary'
|
||||
|| parsed.status === 'user-overridden' || parsed.status === 'killed'
|
||||
? parsed.status
|
||||
: 'active';
|
||||
|
||||
@@ -514,6 +514,8 @@ export class AccountRuntimeLoop {
|
||||
type: 'response',
|
||||
message: result.kind === 'human-required'
|
||||
? `Human follow-up required for u/${result.username}`
|
||||
: result.kind === 'engaged'
|
||||
? `Engagement completed after a reply from u/${result.username}`
|
||||
: result.kind === 'image-received'
|
||||
? `Image received from u/${result.username}`
|
||||
: result.kind === 'killed'
|
||||
|
||||
Reference in new issue
Block a user