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 : }
|