import { randomUUID } from 'crypto';
import { FileStore } from './file-store.js';
import type { Session, Step, SessionResult } from './types.js';
import type { StepInput, StepStatus } from '../tools/schemas.js';
export class SessionManager {
private store: FileStore;
constructor(basePath?: string) {
this.store = new FileStore(basePath);
}
async create(params: {
title: string;
description?: string;
steps: StepInput[];
allowPartialCompletion: boolean;
}): Promise<Session> {
const session: Session = {
id: randomUUID(),
title: params.title,
description: params.description,
steps: params.steps.map((step) => ({
id: step.id,
title: step.title,
description: step.description,
type: step.type ?? 'action',
required: step.required ?? true,
dependsOn: step.dependsOn,
status: 'pending' as const,
})),
allowPartialCompletion: params.allowPartialCompletion,
status: 'active',
createdAt: new Date().toISOString(),
};
await this.store.write(session.id, session);
return session;
}
async resume(sessionId: string): Promise<Session> {
const session = await this.store.read(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
return session;
}
async get(sessionId: string): Promise<Session | null> {
return this.store.read(sessionId);
}
async getLatestActive(): Promise<Session | null> {
return this.store.getLatestActiveSession();
}
async updateStep(
sessionId: string,
stepId: string,
update: { status: StepStatus; notes?: string }
): Promise<Session> {
const session = await this.store.read(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
const step = session.steps.find((s) => s.id === stepId);
if (!step) {
throw new Error(`Step ${stepId} not found in session ${sessionId}`);
}
step.status = update.status;
if (update.status === 'completed' || update.status === 'skipped') {
step.completedAt = new Date().toISOString();
}
if (update.notes !== undefined) {
step.notes = update.notes;
}
// If a step is skipped, auto-skip all dependent steps
if (update.status === 'skipped') {
this.autoSkipDependentSteps(session, stepId);
}
// Check if all steps are complete or skipped
const allComplete = session.steps.every(
(s) => s.status === 'completed' || s.status === 'skipped'
);
// Check if all required steps are complete
const allRequiredComplete = session.steps
.filter((s) => s.required)
.every((s) => s.status === 'completed');
// Mark as completed if all steps are done (completed or skipped)
// OR if all required steps are completed and partial completion is allowed
if (allComplete || (allRequiredComplete && session.allowPartialCompletion)) {
session.status = 'completed';
session.completedAt = new Date().toISOString();
}
await this.store.write(sessionId, session);
return session;
}
async cancel(sessionId: string): Promise<Session> {
const session = await this.store.read(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
session.status = 'cancelled';
session.completedAt = new Date().toISOString();
await this.store.write(sessionId, session);
return session;
}
async markPartial(sessionId: string): Promise<Session> {
const session = await this.store.read(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
session.status = 'partial';
session.completedAt = new Date().toISOString();
await this.store.write(sessionId, session);
return session;
}
async markTimeout(sessionId: string): Promise<Session> {
const session = await this.store.read(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
session.status = 'timeout';
session.completedAt = new Date().toISOString();
await this.store.write(sessionId, session);
return session;
}
private autoSkipDependentSteps(session: Session, skippedStepId: string): void {
// Find all steps that depend on the skipped step
const dependentSteps = session.steps.filter((s) =>
s.dependsOn?.includes(skippedStepId)
);
// Recursively skip dependent steps
for (const depStep of dependentSteps) {
if (depStep.status === 'pending') {
depStep.status = 'skipped';
depStep.completedAt = new Date().toISOString();
// Recursively skip steps that depend on this one
this.autoSkipDependentSteps(session, depStep.id);
}
}
}
getResult(session: Session): SessionResult {
return {
sessionId: session.id,
status: session.status,
steps: session.steps.map((s) => ({
id: s.id,
status: s.status,
completedAt: s.completedAt,
notes: s.notes,
})),
completedCount: session.steps.filter((s) => s.status === 'completed').length,
totalCount: session.steps.length,
startedAt: session.createdAt,
completedAt: session.completedAt,
};
}
getStorePath(): string {
return this.store.getBasePath();
}
}