Something went wrong. Try again.
more up to date at codeberg https://codeberg.org/cowie/tau.git
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421import { copyFileSync, existsSync, mkdirSync } from "node:fs";import { basename, join, resolve } from "node:path";import { resolvePath } from "../utils/paths.ts";import type { AgentSession } from "./agent-session.ts";import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agent-session-services.ts";import type { ReplacedSessionContext, SessionShutdownEvent, SessionStartEvent } from "./extensions/index.ts";import { emitSessionShutdownEvent } from "./extensions/runner.ts";import type { CreateAgentSessionResult } from "./sdk.ts";import { assertSessionCwdExists } from "./session-cwd.ts";import { SessionManager } from "./session-manager.ts";
/** * Result returned by runtime creation. * * The caller gets the created session, its cwd-bound services, and all * diagnostics collected during setup. */export interface CreateAgentSessionRuntimeResult extends CreateAgentSessionResult { services: AgentSessionServices; diagnostics: AgentSessionRuntimeDiagnostic[];}
/** * Creates a full runtime for a target cwd and session manager. * * The factory closes over process-global fixed inputs, recreates cwd-bound * services for the effective cwd, resolves session options against those * services, and finally creates the AgentSession. */export type CreateAgentSessionRuntimeFactory = (options: { cwd: string; agentDir: string; sessionManager: SessionManager; sessionStartEvent?: SessionStartEvent;}) => Promise<CreateAgentSessionRuntimeResult>;
/** * Thrown when /import references a JSONL file path that does not exist. */export class SessionImportFileNotFoundError extends Error { readonly filePath: string;
constructor(filePath: string) { super(`File not found: ${filePath}`); this.name = "SessionImportFileNotFoundError"; this.filePath = filePath; }}
function extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string { if (typeof content === "string") { return content; }
return content .filter((part): part is { type: "text"; text: string } => part.type === "text" && typeof part.text === "string") .map((part) => part.text) .join("");}
/** * Owns the current AgentSession plus its cwd-bound services. * * Session replacement methods tear down the current runtime first, then create * and apply the next runtime. If creation fails, the error is propagated to the * caller. The caller is responsible for user-facing error handling. */export class AgentSessionRuntime { private rebindSession?: (session: AgentSession) => Promise<void>; private beforeSessionInvalidate?: () => void; private _session: AgentSession; private _services: AgentSessionServices; private readonly createRuntime: CreateAgentSessionRuntimeFactory; private _diagnostics: AgentSessionRuntimeDiagnostic[]; private _modelFallbackMessage?: string;
constructor( _session: AgentSession, _services: AgentSessionServices, createRuntime: CreateAgentSessionRuntimeFactory, _diagnostics: AgentSessionRuntimeDiagnostic[] = [], _modelFallbackMessage?: string, ) { this._session = _session; this._services = _services; this.createRuntime = createRuntime; this._diagnostics = _diagnostics; this._modelFallbackMessage = _modelFallbackMessage; }
get services(): AgentSessionServices { return this._services; }
get session(): AgentSession { return this._session; }
get cwd(): string { return this._services.cwd; }
get diagnostics(): readonly AgentSessionRuntimeDiagnostic[] { return this._diagnostics; }
get modelFallbackMessage(): string | undefined { return this._modelFallbackMessage; }
setRebindSession(rebindSession?: (session: AgentSession) => Promise<void>): void { this.rebindSession = rebindSession; }
/** * Set a synchronous callback that runs after `session_shutdown` handlers finish * but before the current session is invalidated. * * This is for host-owned UI teardown that must not yield to the event loop, * such as detaching extension-provided TUI components before the old extension * context becomes stale. */ setBeforeSessionInvalidate(beforeSessionInvalidate?: () => void): void { this.beforeSessionInvalidate = beforeSessionInvalidate; }
private async emitBeforeSwitch( reason: "new" | "resume", targetSessionFile?: string, ): Promise<{ cancelled: boolean }> { const runner = this.session.extensionRunner; if (!runner.hasHandlers("session_before_switch")) { return { cancelled: false }; }
const result = await runner.emit({ type: "session_before_switch", reason, targetSessionFile, }); return { cancelled: result?.cancel === true }; }
private async emitBeforeFork( entryId: string, options: { position: "before" | "at" }, ): Promise<{ cancelled: boolean }> { const runner = this.session.extensionRunner; if (!runner.hasHandlers("session_before_fork")) { return { cancelled: false }; }
const result = await runner.emit({ type: "session_before_fork", entryId, ...options, }); return { cancelled: result?.cancel === true }; }
private async teardownCurrent(reason: SessionShutdownEvent["reason"], targetSessionFile?: string): Promise<void> { await emitSessionShutdownEvent(this.session.extensionRunner, { type: "session_shutdown", reason, targetSessionFile, }); this.beforeSessionInvalidate?.(); this.session.dispose(); }
private apply(result: CreateAgentSessionRuntimeResult): void { this._session = result.session; this._services = result.services; this._diagnostics = result.diagnostics; this._modelFallbackMessage = result.modelFallbackMessage; }
private async finishSessionReplacement(withSession?: (ctx: ReplacedSessionContext) => Promise<void>): Promise<void> { if (this.rebindSession) { await this.rebindSession(this.session); } if (withSession) { await withSession(this.session.createReplacedSessionContext()); } }
async switchSession( sessionPath: string, options?: { cwdOverride?: string; withSession?: (ctx: ReplacedSessionContext) => Promise<void> }, ): Promise<{ cancelled: boolean }> { const beforeResult = await this.emitBeforeSwitch("resume", sessionPath); if (beforeResult.cancelled) { return beforeResult; }
const previousSessionFile = this.session.sessionFile; const sessionManager = SessionManager.open(sessionPath, undefined, options?.cwdOverride); assertSessionCwdExists(sessionManager, this.cwd); await this.teardownCurrent("resume", sessionManager.getSessionFile()); this.apply( await this.createRuntime({ cwd: sessionManager.getCwd(), agentDir: this.services.agentDir, sessionManager, sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile }, }), ); await this.finishSessionReplacement(options?.withSession); return { cancelled: false }; }
async newSession(options?: { parentSession?: string; setup?: (sessionManager: SessionManager) => Promise<void>; withSession?: (ctx: ReplacedSessionContext) => Promise<void>; }): Promise<{ cancelled: boolean }> { const beforeResult = await this.emitBeforeSwitch("new"); if (beforeResult.cancelled) { return beforeResult; }
const previousSessionFile = this.session.sessionFile; const sessionDir = this.session.sessionManager.getSessionDir(); const sessionManager = SessionManager.create(this.cwd, sessionDir); if (options?.parentSession) { sessionManager.newSession({ parentSession: options.parentSession }); }
await this.teardownCurrent("new", sessionManager.getSessionFile()); this.apply( await this.createRuntime({ cwd: this.cwd, agentDir: this.services.agentDir, sessionManager, sessionStartEvent: { type: "session_start", reason: "new", previousSessionFile }, }), ); if (options?.setup) { await options.setup(this.session.sessionManager); this.session.agent.state.messages = this.session.sessionManager.buildSessionContext().messages; } await this.finishSessionReplacement(options?.withSession); return { cancelled: false }; }
async fork( entryId: string, options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> }, ): Promise<{ cancelled: boolean; selectedText?: string }> { const position = options?.position ?? "before"; const beforeResult = await this.emitBeforeFork(entryId, { position }); if (beforeResult.cancelled) { return { cancelled: true }; } let targetLeafId: string | null; let selectedText: string | undefined;
const selectedEntry = this.session.sessionManager.getEntry(entryId); if (!selectedEntry) { throw new Error("Invalid entry ID for forking"); }
if (position === "at") { targetLeafId = selectedEntry.id; } else { if (selectedEntry.type !== "message" || selectedEntry.message.role !== "user") { throw new Error("Invalid entry ID for forking"); } targetLeafId = selectedEntry.parentId; selectedText = extractUserMessageText(selectedEntry.message.content); }
const previousSessionFile = this.session.sessionFile; if (this.session.sessionManager.isPersisted()) { const currentSessionFile = this.session.sessionFile; if (!currentSessionFile) { throw new Error("Persisted session is missing a session file"); } const sessionDir = this.session.sessionManager.getSessionDir(); if (!targetLeafId) { const sessionManager = SessionManager.create(this.cwd, sessionDir); sessionManager.newSession({ parentSession: currentSessionFile }); await this.teardownCurrent("fork", sessionManager.getSessionFile()); this.apply( await this.createRuntime({ cwd: this.cwd, agentDir: this.services.agentDir, sessionManager, sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile }, }), ); await this.finishSessionReplacement(options?.withSession); return { cancelled: false, selectedText }; }
const sessionManager = SessionManager.open(currentSessionFile, sessionDir); const forkedSessionPath = sessionManager.createBranchedSession(targetLeafId); if (!forkedSessionPath) { throw new Error("Failed to create forked session"); } await this.teardownCurrent("fork", sessionManager.getSessionFile()); this.apply( await this.createRuntime({ cwd: sessionManager.getCwd(), agentDir: this.services.agentDir, sessionManager, sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile }, }), ); await this.finishSessionReplacement(options?.withSession); return { cancelled: false, selectedText }; }
const sessionManager = this.session.sessionManager; if (!targetLeafId) { sessionManager.newSession({ parentSession: this.session.sessionFile }); } else { sessionManager.createBranchedSession(targetLeafId); } await this.teardownCurrent("fork", sessionManager.getSessionFile()); this.apply( await this.createRuntime({ cwd: this.cwd, agentDir: this.services.agentDir, sessionManager, sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile }, }), ); await this.finishSessionReplacement(options?.withSession); return { cancelled: false, selectedText }; }
/** * Import a session JSONL file and switch runtime state to the imported session. * * @returns `{ cancelled: true }` when cancelled by `session_before_switch`, otherwise `{ cancelled: false }`. * @throws {SessionImportFileNotFoundError} When the input path does not exist. * @throws {MissingSessionCwdError} When the imported session cwd cannot be resolved and no override is provided. */ async importFromJsonl(inputPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> { const resolvedPath = resolvePath(inputPath); if (!existsSync(resolvedPath)) { throw new SessionImportFileNotFoundError(resolvedPath); }
const sessionDir = this.session.sessionManager.getSessionDir(); if (!existsSync(sessionDir)) { mkdirSync(sessionDir, { recursive: true }); }
const destinationPath = join(sessionDir, basename(resolvedPath)); const beforeResult = await this.emitBeforeSwitch("resume", destinationPath); if (beforeResult.cancelled) { return beforeResult; }
const previousSessionFile = this.session.sessionFile; if (resolve(destinationPath) !== resolvedPath) { copyFileSync(resolvedPath, destinationPath); }
const sessionManager = SessionManager.open(destinationPath, sessionDir, cwdOverride); assertSessionCwdExists(sessionManager, this.cwd); await this.teardownCurrent("resume", sessionManager.getSessionFile()); this.apply( await this.createRuntime({ cwd: sessionManager.getCwd(), agentDir: this.services.agentDir, sessionManager, sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile }, }), ); await this.finishSessionReplacement(); return { cancelled: false }; }
async dispose(): Promise<void> { await emitSessionShutdownEvent(this.session.extensionRunner, { type: "session_shutdown", reason: "quit", }); this.beforeSessionInvalidate?.(); this.session.dispose(); }}
/** * Create the initial runtime from a runtime factory and initial session target. * * The same factory is stored on the returned AgentSessionRuntime and reused for * later /new, /resume, /fork, and import flows. */export async function createAgentSessionRuntime( createRuntime: CreateAgentSessionRuntimeFactory, options: { cwd: string; agentDir: string; sessionManager: SessionManager; sessionStartEvent?: SessionStartEvent; },): Promise<AgentSessionRuntime> { assertSessionCwdExists(options.sessionManager, options.cwd); const result = await createRuntime(options); return new AgentSessionRuntime( result.session, result.services, createRuntime, result.diagnostics, result.modelFallbackMessage, );}
export { type AgentSessionRuntimeDiagnostic, type AgentSessionServices, type CreateAgentSessionFromServicesOptions, type CreateAgentSessionServicesOptions, createAgentSessionFromServices, createAgentSessionServices,} from "./agent-session-services.ts";