src/agents/baseAgentSession.ts

import { createAbortError, isAbortError } from "../tools/abortError.ts"; import { EMPTY_USAGE, type SubAgentSession, type SubAgentTurnOptions, type SubAgentTurnResult } from "./subAgentSession.ts";

type BaseAgentSessionOptions = { id: string; type: SubAgentSession["type"]; ownerUserId: string | null; baseSignal?: AbortSignal; logAction?: (entry: { kind: string; userId?: string | null; content?: string; metadata?: Record<string, unknown>; }) => void; };

export abstract class BaseAgentSession implements SubAgentSession { readonly id: string; readonly type: SubAgentSession["type"]; readonly createdAt: number; readonly ownerUserId: string | null; lastUsedAt: number; status: SubAgentSession["status"];

private readonly baseSignal?: AbortSignal; private readonly logAction?: BaseAgentSessionOptions["logAction"]; protected activeAbortController: AbortController | null;

protected constructor(options: BaseAgentSessionOptions) { this.id = options.id; this.type = options.type; this.createdAt = Date.now(); this.lastUsedAt = Date.now(); this.ownerUserId = options.ownerUserId; this.status = "idle"; this.baseSignal = options.baseSignal; this.logAction = options.logAction; this.activeAbortController = null; }

async runTurn(input: string, options: SubAgentTurnOptions = {}): Promise { if (this.status === "cancelled" || this.status === "error" || this.status === "completed") { this.logLifecycle("turn_rejected", { reason: session_${this.status}, inputLength: String(input || "").length }); return { text: Session is ${this.status} and cannot accept new turns., costUsd: 0, isError: true, errorMessage: Session ${this.status}, sessionCompleted: this.status === "completed", usage: { ...EMPTY_USAGE } }; }

this.status = "running";
this.lastUsedAt = Date.now();
this.activeAbortController = new AbortController();
const turnSignal = this.buildTurnSignal(options.signal);
this.logLifecycle("turn_started", {
  inputLength: String(input || "").length
});

try {
  const result = await this.executeTurn(input, {
    ...options,
    signal: turnSignal
  });
  if (this.status === "running") {
    this.status = result.sessionCompleted ? "completed" : "idle";
  }
  this.lastUsedAt = Date.now();
  this.logLifecycle("turn_completed", {
    sessionCompleted: Boolean(result.sessionCompleted),
    isError: Boolean(result.isError),
    costUsd: result.costUsd
  });
  return result;
} catch (error) {
  if (isAbortError(error) || turnSignal?.aborted) {
    this.status = "cancelled";
    this.lastUsedAt = Date.now();
    this.onCancelled(String(turnSignal?.reason || error || `${this.type} session cancelled`));
    this.logLifecycle("turn_cancelled", {
      reason: String(turnSignal?.reason || error || `${this.type} session cancelled`)
    });
    throw createAbortError(turnSignal?.reason || error);
  }

  this.status = "error";
  this.lastUsedAt = Date.now();
  this.logLifecycle("turn_failed", {
    error: String(error instanceof Error ? error.message : error || "Session turn failed.")
  });
  return this.handleTurnError(error, input);
} finally {
  this.activeAbortController = null;
}

}

cancel(reason = ${this.type} session cancelled): void { if (this.status === "cancelled") return; this.status = "cancelled"; try { this.activeAbortController?.abort(reason); } catch { // ignore } this.onCancelled(reason); this.logLifecycle("session_cancelled", { reason }); }

close(): void { const reason = ${this.type} session closed; if (this.status === "idle" || this.status === "running") { this.cancel(reason); } else if (this.status !== "cancelled") { this.onCancelled(reason); } this.onClosed(); this.logLifecycle("session_closed", { reason }); }

protected buildTurnSignal(signal?: AbortSignal): AbortSignal | undefined { const signals = [this.baseSignal, this.activeAbortController?.signal, signal] .filter((entry): entry is AbortSignal => Boolean(entry)); if (signals.length <= 0) return undefined; if (signals.length === 1) return signals[0]; return AbortSignal.any(signals); }

protected handleTurnError(error: unknown, _input: string): SubAgentTurnResult { const message = String(error instanceof Error ? error.message : error || "Session turn failed."); return { text: message, costUsd: 0, isError: true, errorMessage: message, usage: { ...EMPTY_USAGE } }; }

protected onCancelled(_reason: string): void { // subclasses can override }

protected onClosed(): void { // subclasses can override }

protected logLifecycle(content: string, metadata?: Record<string, unknown>): void { this.logAction?.({ kind: "sub_agent_session_lifecycle", userId: this.ownerUserId || null, content, metadata: { sessionId: this.id, sessionType: this.type, status: this.status, ...(metadata || {}) } }); }

protected abstract executeTurn(input: string, options: SubAgentTurnOptions): Promise; }