Files
EJClaw/src/types.ts

328 lines
8.7 KiB
TypeScript

import {
PAIRED_ROOM_ROLES,
isPairedRoomRole,
normalizePairedRoomRole,
normalizePairedRoomRoleOrNull,
type PairedRoomRole,
type TaskContextMode,
} from 'ejclaw-runners-shared';
import type { VisibleVerdict } from './paired-verdict.js';
export {
PAIRED_ROOM_ROLES,
isPairedRoomRole,
normalizePairedRoomRole,
normalizePairedRoomRoleOrNull,
type PairedRoomRole,
};
export interface AgentConfig {
timeout?: number; // Default: 300000 (5 minutes)
// Per-group model/effort overrides (take precedence over global env vars)
codexModel?: string;
codexEffort?: string;
codexGoals?: boolean;
claudeModel?: string;
claudeEffort?: string;
claudeThinking?: 'adaptive' | 'enabled' | 'disabled';
claudeThinkingBudget?: number;
}
export type AgentType = 'claude-code' | 'codex';
export type RoomMode = 'single' | 'tribunal';
/** Phase of agent output as emitted by the runner. */
export type AgentOutputPhase =
| 'progress'
| 'final'
| 'tool-activity'
| 'intermediate';
/** Phase as visible in the UI (mapped from AgentOutputPhase). */
export type VisiblePhase = 'silent' | 'progress' | 'final';
export type AgentVisibility = 'public' | 'silent';
export interface OutboundAttachment {
path: string;
name?: string;
mime?: string;
}
export interface SendMessageOptions {
attachments?: OutboundAttachment[];
/**
* Extra realpath roots that are valid for this delivery attempt. Runtime
* callers can pass the active project/workspace directory without widening
* the global Discord attachment allowlist.
*/
attachmentBaseDirs?: string[];
}
export interface SendMessageResult {
primaryMessageId: string | null;
messageIds: string[];
visible: boolean;
}
export interface DeleteRecentMessagesByContentOptions {
contentIncludes: string;
exceptMessageId?: string | null;
limit?: number;
}
export type PairedTaskStatus =
| 'active'
| 'review_ready'
| 'in_review'
| 'merge_ready'
| 'completed'
| 'arbiter_requested'
| 'in_arbitration';
export type PairedTurnReservationIntentKind =
| 'owner-turn'
| 'reviewer-turn'
| 'arbiter-turn'
| 'owner-follow-up'
| 'finalize-owner-turn';
export type ArbiterVerdict = 'proceed' | 'revise' | 'reset' | 'escalate';
export type PairedWorkspaceRole = 'owner' | 'reviewer';
export type PairedWorkspaceStatus = 'ready' | 'stale';
export interface RoomRoleContext {
serviceId: string;
role: PairedRoomRole;
ownerServiceId: string;
reviewerServiceId: string;
ownerAgentType?: AgentType;
reviewerAgentType?: AgentType | null;
failoverOwner: boolean;
arbiterServiceId?: string;
arbiterAgentType?: AgentType | null;
}
export interface PairedProject {
chat_jid: string;
group_folder: string;
canonical_work_dir: string;
created_at: string;
updated_at: string;
}
export interface PairedTask {
id: string;
chat_jid: string;
group_folder: string;
owner_service_id: string;
reviewer_service_id: string;
owner_agent_type?: AgentType | null;
reviewer_agent_type?: AgentType | null;
arbiter_agent_type?: AgentType | null;
title: string | null;
source_ref: string | null;
plan_notes: string | null;
review_requested_at: string | null;
round_trip_count: number;
owner_failure_count?: number | null;
owner_step_done_streak?: number | null;
finalize_step_done_count?: number | null;
task_done_then_user_reopen_count?: number | null;
empty_step_done_streak?: number | null;
status: PairedTaskStatus;
arbiter_verdict: string | null;
arbiter_requested_at: string | null;
completion_reason: string | null;
created_at: string;
updated_at: string;
}
export interface PairedTurnOutput {
id: number;
task_id: string;
turn_number: number;
role: PairedRoomRole;
output_text: string;
verdict?: VisibleVerdict | null;
created_at: string;
}
export interface PairedWorkspace {
id: string;
task_id: string;
role: PairedWorkspaceRole;
workspace_dir: string;
snapshot_source_dir: string | null;
snapshot_ref: string | null;
status: PairedWorkspaceStatus;
snapshot_refreshed_at: string | null;
created_at: string;
updated_at: string;
}
export type AgentResponsePolicy = 'normal' | 'silent-unless-addressed';
export type StructuredAgentOutput =
| {
visibility: 'public';
text: string;
attachments?: OutboundAttachment[];
}
| {
visibility: 'silent';
};
export function normalizeAgentOutputPhase(
phase?: AgentOutputPhase,
): AgentOutputPhase {
return phase ?? 'final';
}
export function toVisiblePhase(phase: AgentOutputPhase): VisiblePhase {
switch (phase) {
case 'intermediate':
case 'tool-activity':
return 'silent';
case 'progress':
return 'progress';
case 'final':
return 'final';
default: {
const exhaustive: never = phase;
throw new Error(`Unknown agent output phase: ${exhaustive}`);
}
}
}
export interface RegisteredGroup {
name: string;
folder: string;
trigger?: string;
added_at: string;
agentConfig?: AgentConfig;
requiresTrigger?: boolean; // Whether non-paired messages must match the room trigger
isMain?: boolean; // True for the main control group (no trigger, elevated privileges)
agentType?: AgentType;
workDir?: string; // Working directory for the agent (defaults to group folder)
}
export const MESSAGE_SOURCE_KINDS = [
'human',
'bot',
'trusted_external_bot',
'ipc_injected_human',
'ipc_injected_bot',
] as const;
export type MessageSourceKind = (typeof MESSAGE_SOURCE_KINDS)[number];
export const DEFAULT_MESSAGE_SOURCE_KIND: MessageSourceKind = 'human';
export interface NewMessage {
id: string;
chat_jid: string;
sender: string;
sender_name: string;
content: string;
timestamp: string;
seq?: number;
is_from_me?: boolean;
is_bot_message?: boolean;
message_source_kind?: MessageSourceKind;
}
export interface ScheduledTask {
id: string;
group_folder: string;
chat_jid: string;
agent_type: AgentType | null;
room_role?: PairedRoomRole | null;
ci_provider?: 'github' | null;
ci_metadata?: string | null;
max_duration_ms?: number | null;
status_message_id: string | null;
status_started_at: string | null;
prompt: string;
schedule_type: 'cron' | 'interval' | 'once';
schedule_value: string;
context_mode: TaskContextMode;
next_run: string | null;
last_run: string | null;
last_result: string | null;
status: 'active' | 'paused' | 'completed';
suspended_until?: string | null;
created_at: string;
}
export interface TaskRunLog {
task_id: string;
run_at: string;
duration_ms: number;
status: 'success' | 'error';
result: string | null;
error: string | null;
}
// --- Channel abstraction ---
export interface ChannelMeta {
name: string;
position: number;
category: string;
categoryPosition: number;
}
export interface ChannelOutboundAuditMeta {
channelName: string;
botUserId: string | null;
botUsername: string | null;
}
export interface Channel {
name: string;
connect(): Promise<void>;
sendMessage(
jid: string,
text: string,
options?: SendMessageOptions,
): Promise<SendMessageResult | void>;
isConnected(): boolean;
ownsJid(jid: string): boolean;
// Optional: whether a stored inbound message was authored by this channel's own bot/user.
isOwnMessage?(msg: NewMessage): boolean;
disconnect(): Promise<void>;
// Optional: typing indicator. Channels that support it implement it.
setTyping?(jid: string, isTyping: boolean): Promise<void>;
// Optional: edit/delete messages (used by status dashboard and tracked progress cleanup).
editMessage?(jid: string, messageId: string, text: string): Promise<void>;
deleteMessage?(jid: string, messageId: string): Promise<void>;
deleteRecentMessagesByContent?(
jid: string,
options: DeleteRecentMessagesByContentOptions,
): Promise<number>;
sendAndTrack?(jid: string, text: string): Promise<string | null>;
// Optional: sync group/chat names from the platform.
syncGroups?(force: boolean): Promise<void>;
// Optional: get channel metadata (position, category) for ordering.
getChannelMeta?(jids: string[]): Promise<Map<string, ChannelMeta>>;
// Optional: delete all messages in a channel (used for dashboard cleanup).
purgeChannel?(jid: string): Promise<number>;
// Optional: expose runtime sender identity for outbound audit logs.
getOutboundAuditMeta?(): ChannelOutboundAuditMeta;
}
// Callback type that channels use to deliver inbound messages
export type OnInboundMessage = (chatJid: string, message: NewMessage) => void;
// Callback for chat metadata discovery.
// name is optional — channels that deliver names inline (Telegram) pass it here;
// channels that sync names separately (via syncGroups) omit it.
export type OnChatMetadata = (
chatJid: string,
timestamp: string,
name?: string,
channel?: string,
isGroup?: boolean,
) => void;