runtime.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import type {
  2. Message,
  3. MessagePart,
  4. PipelineStage,
  5. SessionMessage,
  6. SessionRuntime,
  7. SessionSummary,
  8. ToolExecution,
  9. } from "../../types";
  10. const NULL_BOOK_KEY = "__null__";
  11. const AGENT_LABELS: Record<string, string> = {
  12. architect: "建书",
  13. writer: "写作",
  14. auditor: "审计",
  15. reviser: "修订",
  16. exporter: "导出",
  17. };
  18. const TOOL_LABELS: Record<string, string> = {
  19. read: "读取文件",
  20. edit: "编辑文件",
  21. grep: "搜索",
  22. ls: "列目录",
  23. };
  24. export function bookKey(bookId: string | null | undefined): string {
  25. return bookId ?? NULL_BOOK_KEY;
  26. }
  27. export function extractErrorMessage(error: string | { code?: string; message?: string }): string {
  28. if (typeof error === "string") return error;
  29. return error.message ?? "Unknown error";
  30. }
  31. export function resolveToolLabel(tool: string, agent?: string): string {
  32. if (tool === "sub_agent" && agent) return AGENT_LABELS[agent] ?? agent;
  33. return TOOL_LABELS[tool] ?? tool;
  34. }
  35. export function summarizeResult(result: unknown): string {
  36. if (typeof result === "string") return result.slice(0, 200);
  37. if (result && typeof result === "object") {
  38. const record = result as Record<string, unknown>;
  39. if (typeof record.content === "string") return record.content.slice(0, 200);
  40. }
  41. return String(result).slice(0, 200);
  42. }
  43. export function extractToolError(result: unknown): string {
  44. if (typeof result === "string") return result.slice(0, 500);
  45. if (result && typeof result === "object") {
  46. const record = result as Record<string, unknown>;
  47. if (typeof record.content === "string") return record.content.slice(0, 500);
  48. if (record.content && Array.isArray(record.content)) {
  49. const textPart = record.content.find((content: any) => content.type === "text");
  50. if (textPart) return (textPart as any).text?.slice(0, 500) ?? "";
  51. }
  52. }
  53. return String(result).slice(0, 500);
  54. }
  55. export function getOrCreateStream(
  56. messages: ReadonlyArray<Message>,
  57. streamTs: number,
  58. ): [ReadonlyArray<Message>, Message] {
  59. const last = messages[messages.length - 1];
  60. if (last?.timestamp === streamTs && last.role === "assistant") {
  61. return [messages, last];
  62. }
  63. const message: Message = { role: "assistant", content: "", timestamp: streamTs, parts: [] };
  64. return [[...messages, message], message];
  65. }
  66. export function replaceLast(
  67. messages: ReadonlyArray<Message>,
  68. updated: Message,
  69. ): ReadonlyArray<Message> {
  70. return [...messages.slice(0, -1), updated];
  71. }
  72. export function findRunningToolPart(
  73. parts: MessagePart[],
  74. ): (MessagePart & { type: "tool" }) | undefined {
  75. for (let i = parts.length - 1; i >= 0; i -= 1) {
  76. const part = parts[i];
  77. if (part.type === "tool" && part.execution.status === "running") {
  78. return part as MessagePart & { type: "tool" };
  79. }
  80. }
  81. return undefined;
  82. }
  83. export function deriveFlat(
  84. parts: MessagePart[],
  85. ): { content: string; thinking?: string; thinkingStreaming?: boolean; toolExecutions?: ToolExecution[] } {
  86. let content = "";
  87. let thinking = "";
  88. let thinkingStreaming = false;
  89. const toolExecutions: ToolExecution[] = [];
  90. for (const part of parts) {
  91. if (part.type === "thinking") {
  92. if (thinking) thinking += "\n\n---\n\n";
  93. thinking += part.content;
  94. if (part.streaming) thinkingStreaming = true;
  95. continue;
  96. }
  97. if (part.type === "text") {
  98. content += part.content;
  99. continue;
  100. }
  101. toolExecutions.push(part.execution);
  102. }
  103. return {
  104. content,
  105. ...(thinking ? { thinking } : {}),
  106. ...(thinkingStreaming ? { thinkingStreaming: true } : {}),
  107. ...(toolExecutions.length > 0 ? { toolExecutions } : {}),
  108. };
  109. }
  110. export function createSessionRuntime(input: {
  111. sessionId: string;
  112. bookId: string | null;
  113. title: string | null;
  114. messages?: ReadonlyArray<Message>;
  115. isDraft?: boolean;
  116. }): SessionRuntime {
  117. return {
  118. sessionId: input.sessionId,
  119. bookId: input.bookId,
  120. title: input.title,
  121. messages: input.messages ?? [],
  122. stream: null,
  123. isStreaming: false,
  124. lastError: null,
  125. pendingBookArgs: null,
  126. isDraft: input.isDraft ?? false,
  127. };
  128. }
  129. export function deserializeMessages(
  130. msgs: ReadonlyArray<SessionMessage>,
  131. ): ReadonlyArray<Message> {
  132. return msgs
  133. .filter((message) => message.role === "user" || message.role === "assistant")
  134. .map((message) => {
  135. const toolExecutions = (message as any).toolExecutions as ToolExecution[] | undefined;
  136. const parts: MessagePart[] = [];
  137. if (message.thinking) parts.push({ type: "thinking", content: message.thinking, streaming: false });
  138. if (toolExecutions) {
  139. for (const execution of toolExecutions) {
  140. parts.push({ type: "tool", execution });
  141. }
  142. }
  143. if (message.content) parts.push({ type: "text", content: message.content });
  144. return {
  145. role: message.role as "user" | "assistant",
  146. content: message.content,
  147. thinking: message.thinking,
  148. toolExecutions,
  149. timestamp: message.timestamp,
  150. parts: parts.length > 0 ? parts : undefined,
  151. };
  152. });
  153. }
  154. export function updateSession(
  155. sessions: Record<string, SessionRuntime>,
  156. sessionId: string,
  157. updater: (session: SessionRuntime) => Partial<SessionRuntime>,
  158. ): Record<string, SessionRuntime> {
  159. const existing = sessions[sessionId];
  160. if (!existing) return sessions;
  161. return {
  162. ...sessions,
  163. [sessionId]: {
  164. ...existing,
  165. ...updater(existing),
  166. },
  167. };
  168. }
  169. export function upsertSessionSummary(
  170. sessions: Record<string, SessionRuntime>,
  171. summary: Pick<SessionSummary, "sessionId" | "bookId" | "title">,
  172. ): Record<string, SessionRuntime> {
  173. const existing = sessions[summary.sessionId];
  174. return {
  175. ...sessions,
  176. [summary.sessionId]: existing
  177. ? { ...existing, bookId: summary.bookId, title: summary.title }
  178. : createSessionRuntime(summary),
  179. };
  180. }
  181. export function mergeSessionIds(
  182. existing: ReadonlyArray<string> | undefined,
  183. incoming: ReadonlyArray<string>,
  184. ): ReadonlyArray<string> {
  185. if (!existing?.length) return [...incoming];
  186. const seen = new Set(existing);
  187. const appended = incoming.filter((id) => !seen.has(id));
  188. if (appended.length === 0) return existing as string[];
  189. return [...existing, ...appended];
  190. }
  191. export function sessionMatchesEvent(sessionId: string, data: unknown): boolean {
  192. if (!data || typeof data !== "object") return false;
  193. return (data as { sessionId?: unknown }).sessionId === sessionId;
  194. }