/**
* Shared types for MCP human-in-the-loop communication
*
* @packageDocumentation
*
* This module defines the core types used for communication between
* MCP clients, the server, and the web UI in the human-in-the-loop system.
*
* @example
* ```typescript
* import { HumanRequest, HumanResponse } from '@ask-me-mcp/askme-shared';
*
* const request: HumanRequest = {
* id: 'req-123',
* sessionId: 'session-456',
* question: 'Please approve this deployment',
* context: {
* environment: 'production',
* changes: ['Update API version', 'Add new feature']
* },
* timestamp: new Date()
* };
* ```
*
* @mermaid
* graph TD
* A[MCP Client] -->|HumanRequest| B[Server Queue]
* B -->|QueuedRequest| C[Web UI]
* C -->|HumanResponse| B
* B -->|Response| A
*/
/**
* Base request from MCP client for human input
*/
export interface HumanRequest {
/** Unique request identifier */
id: string;
/** Session ID this request belongs to */
sessionId: string;
/** The question or prompt for the human */
question: string;
/** Optional context data to help the human make a decision */
context?: Record<string, unknown>;
/** Timestamp when the request was created */
timestamp: Date;
/** Type of request for UI routing */
type?: 'single-question' | 'multiple-choice' | 'hypothesis-challenge' | 'choose-next';
}
/**
* Single choice option in a multiple choice question
*/
export interface MultipleChoiceOption {
/** Unique option identifier */
id: string;
/** Display text for the option */
text: string;
/** Whether this option is selected */
selected: boolean;
/** Optional comment from the user */
comment?: string;
/** Priority level from -3 to +3 (-3=lowest, 0=neutral, +3=highest) (only applies when selected=true) */
priority?: number;
/** Whether user chose "Won't answer" for this option (makes question unanswered) */
wontAnswer?: boolean;
}
/**
* Multiple choice question structure
*/
export interface MultipleChoiceQuestion {
/** Unique question identifier */
id: string;
/** Question text */
text: string;
/** Available options for this question */
options: MultipleChoiceOption[];
/** Reason for not answering (only when user selects "Won't answer") */
whyNotAnswering?: string;
}
/**
* Agreement levels for hypothesis challenges (-3 to +3)
*/
export enum AgreementLevel {
FULLY_DISAGREE = -3,
DISAGREE = -2,
SLIGHTLY_DISAGREE = -1,
UNSURE = 0,
SLIGHTLY_AGREE = 1,
AGREE = 2,
FULLY_AGREE = 3
}
/**
* Single hypothesis in a challenge
*/
export interface Hypothesis {
/** Unique hypothesis identifier */
id: string;
/** Hypothesis statement */
text: string;
/** User's agreement level (-3 to +3) */
agreementLevel?: AgreementLevel;
/** Optional comment from the user */
comment?: string;
/** Whether user chose "Won't answer" for this hypothesis */
wontAnswer?: boolean;
}
/**
* Hypothesis challenge structure
*/
export interface HypothesisChallenge {
/** Unique challenge identifier */
id: string;
/** Challenge title or context */
title: string;
/** Optional description or instructions */
description?: string;
/** List of hypotheses to evaluate */
hypotheses: Hypothesis[];
/** Reason for not answering (only when user selects "Won't answer" for entire challenge) */
whyNotAnswering?: string;
}
/**
* Single option in a choose-next decision
*/
export interface ChooseNextOption {
/** Unique option identifier */
id: string;
/** Option title/name */
title: string;
/** Detailed description of what this option means */
description: string;
/** Optional emoji or icon to display */
icon?: string;
}
/**
* Choose-next decision structure for workflow decision points
*/
export interface ChooseNextChallenge {
/** Unique challenge identifier */
id: string;
/** Decision title or context */
title: string;
/** Detailed description in markdown explaining why this decision is needed */
description: string;
/** List of options to choose from */
options: ChooseNextOption[];
}
/**
* Request for multiple choice input from MCP client
*/
export interface MultipleChoiceRequest extends Omit<HumanRequest, 'question'> {
/** Type identifier */
type: 'multiple-choice';
/** Array of questions with multiple choice options */
questions: MultipleChoiceQuestion[];
}
/**
* Request for hypothesis challenge input from MCP client
*/
export interface HypothesisChallengeRequest extends Omit<HumanRequest, 'question'> {
/** Type identifier */
type: 'hypothesis-challenge';
/** Hypothesis challenge to evaluate */
challenge: HypothesisChallenge;
}
/**
* Request for choose-next decision input from MCP client
*/
export interface ChooseNextRequest extends Omit<HumanRequest, 'question'> {
/** Type identifier */
type: 'choose-next';
/** Choose-next decision to present */
challenge: ChooseNextChallenge;
}
/**
* Base response from human via web UI
*/
export interface HumanResponse {
/** The request ID this response is for */
requestId: string;
/** Session ID this response belongs to */
sessionId: string;
/** The human's response text */
response: string;
/** Timestamp when the response was submitted */
timestamp: Date;
/** Type of response for processing */
type?: 'single-question' | 'multiple-choice' | 'hypothesis-challenge' | 'choose-next';
}
/**
* Response for multiple choice requests
*/
export interface MultipleChoiceResponse extends Omit<HumanResponse, 'response'> {
/** Type identifier */
type: 'multiple-choice';
/** Array of questions with user selections and comments */
questions: MultipleChoiceQuestion[];
/** Completion status indicating how the user wants to proceed */
completionStatus?: 'done' | 'drill-deeper';
}
/**
* Response for hypothesis challenge requests
*/
export interface HypothesisChallengeResponse extends Omit<HumanResponse, 'response'> {
/** Type identifier */
type: 'hypothesis-challenge';
/** Hypothesis challenge with user evaluations */
challenge: HypothesisChallenge;
}
/**
* Response for choose-next decision requests
*/
export interface ChooseNextResponse extends Omit<HumanResponse, 'response'> {
/** Type identifier */
type: 'choose-next';
/** User's action type */
action: 'selected' | 'abort' | 'new-ideas';
/** Selected option (only when action is 'selected') */
selectedOption?: ChooseNextOption;
/** Additional message from user (optional) */
message?: string;
}
/**
* Request in the queue with additional metadata
*/
export interface QueuedRequest extends HumanRequest {
/** Current status of the request */
status: 'pending' | 'active' | 'completed' | 'error';
/** Optional response if completed */
response?: HumanResponse | MultipleChoiceResponse | HypothesisChallengeResponse | ChooseNextResponse;
/** Error message if status is 'error' */
error?: string;
}
/**
* Session information
*/
export interface SessionInfo {
/** Unique session identifier */
id: string;
/** When the session was created */
createdAt: Date;
/** Number of pending requests */
pendingRequests: number;
/** Total requests processed */
totalRequests: number;
}
/**
* WebSocket/SSE message types
*/
export enum MessageType {
/** New request added to queue */
NEW_REQUEST = 'new_request',
/** Request status updated */
REQUEST_UPDATE = 'request_update',
/** Session status changed */
SESSION_UPDATE = 'session_update',
/** Error occurred */
ERROR = 'error'
}
/**
* Server-sent event message structure
*/
export interface ServerMessage<T = unknown> {
/** Type of message */
type: MessageType;
/** Message payload */
data: T;
/** Timestamp of the message */
timestamp: Date;
}
/**
* Error response structure
*/
export interface ErrorResponse {
/** Error code for programmatic handling */
code: string;
/** Human-readable error message */
message: string;
/** Additional error details */
details?: Record<string, unknown>;
}
/**
* Type guards for runtime type checking
*/
export const isHumanRequest = (obj: unknown): obj is HumanRequest => {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'sessionId' in obj &&
'question' in obj &&
'timestamp' in obj
);
};
export const isHumanResponse = (obj: unknown): obj is HumanResponse => {
return (
typeof obj === 'object' &&
obj !== null &&
'requestId' in obj &&
'sessionId' in obj &&
'response' in obj &&
'timestamp' in obj
);
};
export const isMultipleChoiceRequest = (obj: unknown): obj is MultipleChoiceRequest => {
return (
typeof obj === 'object' &&
obj !== null &&
'type' in obj &&
(obj as any).type === 'multiple-choice' &&
'questions' in obj &&
Array.isArray((obj as any).questions)
);
};
export const isMultipleChoiceResponse = (obj: unknown): obj is MultipleChoiceResponse => {
return (
typeof obj === 'object' &&
obj !== null &&
'type' in obj &&
(obj as any).type === 'multiple-choice' &&
'requestId' in obj &&
'sessionId' in obj &&
'questions' in obj &&
Array.isArray((obj as any).questions)
);
};
export const isHypothesisChallengeRequest = (obj: unknown): obj is HypothesisChallengeRequest => {
return (
typeof obj === 'object' &&
obj !== null &&
'type' in obj &&
(obj as any).type === 'hypothesis-challenge' &&
'challenge' in obj &&
typeof (obj as any).challenge === 'object'
);
};
export const isHypothesisChallengeResponse = (obj: unknown): obj is HypothesisChallengeResponse => {
return (
typeof obj === 'object' &&
obj !== null &&
'type' in obj &&
(obj as any).type === 'hypothesis-challenge' &&
'requestId' in obj &&
'sessionId' in obj &&
'challenge' in obj &&
typeof (obj as any).challenge === 'object'
);
};
export const isChooseNextRequest = (obj: unknown): obj is ChooseNextRequest => {
return (
typeof obj === 'object' &&
obj !== null &&
'type' in obj &&
(obj as any).type === 'choose-next' &&
'challenge' in obj &&
typeof (obj as any).challenge === 'object' &&
'options' in (obj as any).challenge &&
Array.isArray((obj as any).challenge.options)
);
};
export const isChooseNextResponse = (obj: unknown): obj is ChooseNextResponse => {
return (
typeof obj === 'object' &&
obj !== null &&
'type' in obj &&
(obj as any).type === 'choose-next' &&
'requestId' in obj &&
'sessionId' in obj &&
'action' in obj &&
['selected', 'abort', 'new-ideas'].includes((obj as any).action)
);
};