LCOV - code coverage report
Current view: top level - dashql-app/src/agent - agent_question_store.ts Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 78 78
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import type { ScriptData, SessionScripts } from '../session/scripts/session_scripts.js';
       2              : import type { AgentFollowTarget, AgentQuestion } from './use_agent_questions.js';
       3              : 
       4              : /// The feed that shows a session, it runs and follows the scripts that the agent writes
       5              : export interface AgentQuestionFeed {
       6              :     /// The current scripts of the session
       7              :     sessionScripts: () => SessionScripts;
       8              :     /// Runs a script that the agent wrote
       9              :     runScript: (next: SessionScripts, scriptData: ScriptData) => void;
      10              :     /// Follows the agent to a script
      11              :     followScript: (scriptKey: number, target: AgentFollowTarget) => void;
      12              : }
      13              : 
      14              : /// The questions of a session
      15              : interface SessionAgentQuestions {
      16              :     /// The running, failed and answered questions by question id, oldest first
      17              :     questions: ReadonlyMap<number, AgentQuestion>;
      18              :     /// The questions that the agent works on, notifications of other requests are dropped
      19              :     working: Set<number>;
      20              :     /// The turns of the questions, once the server queued them
      21              :     turnIds: Map<number, string>;
      22              :     /// The feed that shows the session, null while no feed shows it
      23              :     feed: AgentQuestionFeed | null;
      24              : }
      25              : 
      26            1 : const NO_QUESTIONS: ReadonlyMap<number, AgentQuestion> = new Map();
      27              : 
      28              : /// The questions of the sessions that the agent answers.
      29              : /// The bridge keeps them, a question outlives the feed that asked it and shows again when the user returns to the session.
      30            1 : export class AgentQuestionStore {
      31            1 :     private sessions = new Map<string, SessionAgentQuestions>();
      32            1 :     private listeners = new Set<() => void>();
      33            1 :     private nextQuestionId = 1;
      34              : 
      35            1 :     private session(sessionId: string): SessionAgentQuestions {
      36            1 :         let session = this.sessions.get(sessionId);
      37            1 :         if (session == null) {
      38            1 :             session = { questions: NO_QUESTIONS, working: new Set(), turnIds: new Map(), feed: null };
      39            1 :             this.sessions.set(sessionId, session);
      40            1 :         }
      41            1 :         return session;
      42            1 :     }
      43            1 :     private setQuestions(sessionId: string, questions: ReadonlyMap<number, AgentQuestion>) {
      44            1 :         this.session(sessionId).questions = questions;
      45            1 :         for (const listener of this.listeners) listener();
      46            1 :     }
      47              : 
      48              :     /// Subscribe to the questions of all sessions, returns the unsubscribe
      49            1 :     subscribe = (listener: () => void): (() => void) => {
      50            1 :         this.listeners.add(listener);
      51            1 :         return () => { this.listeners.delete(listener); };
      52            1 :     };
      53              :     /// The questions of a session, the same map until they change
      54            1 :     questions(sessionId: string): ReadonlyMap<number, AgentQuestion> {
      55            1 :         return this.sessions.get(sessionId)?.questions ?? NO_QUESTIONS;
      56            1 :     }
      57              : 
      58              :     /// Add a question that the agent works on, returns its question id
      59            1 :     add(sessionId: string, question: AgentQuestion): number {
      60            1 :         const questionId = this.nextQuestionId++;
      61            1 :         const session = this.session(sessionId);
      62            1 :         session.working.add(questionId);
      63            1 :         this.setQuestions(sessionId, new Map(session.questions).set(questionId, question));
      64            1 :         return questionId;
      65            1 :     }
      66              :     /// Update a question, null drops it
      67            1 :     update(sessionId: string, questionId: number, update: (prev: AgentQuestion) => AgentQuestion | null) {
      68            1 :         const session = this.sessions.get(sessionId);
      69            1 :         const current = session?.questions.get(questionId);
      70            1 :         if (session == null || current == null) return;
      71            1 :         const next = new Map(session.questions);
      72            1 :         const updated = update(current);
      73            1 :         if (updated == null) next.delete(questionId);
      74            1 :         else next.set(questionId, updated);
      75            1 :         this.setQuestions(sessionId, next);
      76            1 :     }
      77              :     /// Drop a question
      78            1 :     remove(sessionId: string, questionId: number) {
      79            1 :         const session = this.sessions.get(sessionId);
      80            1 :         if (session == null || !session.questions.has(questionId)) return;
      81            1 :         const next = new Map(session.questions);
      82            1 :         next.delete(questionId);
      83            1 :         this.setQuestions(sessionId, next);
      84            1 :     }
      85              : 
      86              :     /// Does the agent work on a question?
      87            1 :     isWorking(sessionId: string, questionId: number): boolean {
      88            1 :         return this.sessions.get(sessionId)?.working.has(questionId) ?? false;
      89            1 :     }
      90              :     /// The server queued the turn of a question
      91            1 :     setTurnId(sessionId: string, questionId: number, turnId: string) {
      92            1 :         this.session(sessionId).turnIds.set(questionId, turnId);
      93            1 :     }
      94              :     /// The turn of a question, null before the server queued it
      95            1 :     turnId(sessionId: string, questionId: number): string | null {
      96            1 :         return this.sessions.get(sessionId)?.turnIds.get(questionId) ?? null;
      97            1 :     }
      98              :     /// The agent stopped working on a question, false if it did not work on it
      99            1 :     finish(sessionId: string, questionId: number): boolean {
     100            1 :         const session = this.sessions.get(sessionId);
     101            1 :         if (session == null) return false;
     102            1 :         session.turnIds.delete(questionId);
     103            1 :         return session.working.delete(questionId);
     104            1 :     }
     105              : 
     106              :     /// Forget the questions of a deleted session
     107            1 :     drop(sessionId: string) {
     108            1 :         if (!this.sessions.delete(sessionId)) return;
     109            1 :         for (const listener of this.listeners) listener();
     110            1 :     }
     111              : 
     112              :     /// Show a session in a feed, returns the detach
     113            1 :     attach(sessionId: string, feed: AgentQuestionFeed): () => void {
     114            1 :         this.session(sessionId).feed = feed;
     115            1 :         return () => {
     116            1 :             const session = this.sessions.get(sessionId);
     117            1 :             if (session?.feed === feed) session.feed = null;
     118            1 :         };
     119            1 :     }
     120              :     /// The feed that shows a session, null while no feed shows it
     121            1 :     feed(sessionId: string): AgentQuestionFeed | null {
     122            1 :         return this.sessions.get(sessionId)?.feed ?? null;
     123            1 :     }
     124            1 : }
        

Generated by: LCOV version 2.0-1