Browse Source

feat: upgrade AI SDK and improve chat session handling with new store implementation

feat/ai-sdk-v6-upgrade
npmrun 11 hours ago
parent
commit
7e45689b9f
  1. 4
      .codegraph/daemon.pid
  2. 3
      app/composables/useAgentChat.ts
  3. 173
      app/composables/useAgentChatStore.ts
  4. 24
      app/pages/index.vue
  5. BIN
      packages/drizzle-pkg/db.sqlite
  6. 2
      server/api/agent/chat/index.post.ts
  7. 2
      server/api/llm/chat/index.post.ts
  8. 2
      server/service/agent/temp-token.ts
  9. 2
      server/service/auth/cookie.ts

4
.codegraph/daemon.pid

@ -1,6 +1,6 @@
{ {
"pid": 6400, "pid": 18358,
"version": "0.9.7", "version": "0.9.7",
"socketPath": "/home/dash/code/nuxt-app/.codegraph/daemon.sock", "socketPath": "/home/dash/code/nuxt-app/.codegraph/daemon.sock",
"startedAt": 1786156990118 "startedAt": 1786169176794
} }

3
app/composables/useAgentChat.ts

@ -110,6 +110,7 @@ export function useAgentChat(options: UseAgentChatOptions) {
try { try {
res = await fetch(`/api/agent/chat/stream?sessionId=${encodeURIComponent(sid)}`, { res = await fetch(`/api/agent/chat/stream?sessionId=${encodeURIComponent(sid)}`, {
method: "GET", method: "GET",
credentials: "include",
}); });
} catch { } catch {
return; return;
@ -456,6 +457,7 @@ export function useAgentChat(options: UseAgentChatOptions) {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(buildRequestBody(trimmed, opts)), body: JSON.stringify(buildRequestBody(trimmed, opts)),
signal: abortController.signal, signal: abortController.signal,
credentials: "include",
}); });
if (!auth.loggedIn.value) { if (!auth.loggedIn.value) {
@ -578,6 +580,7 @@ export function useAgentChat(options: UseAgentChatOptions) {
approvalReason: reason, approvalReason: reason,
}), }),
signal: abortController.signal, signal: abortController.signal,
credentials: "include",
}); });
if (!res.ok) { if (!res.ok) {

173
app/composables/useAgentChatStore.ts

@ -0,0 +1,173 @@
import { ref, computed, type Ref, type ComputedRef } from "vue";
import { useAgentChat, type AgentMessage, type UseAgentChatOptions } from "./useAgentChat";
import { useAgentRateLimit } from "./useAgentRateLimit";
export interface AgentChatInstance {
messages: Ref<AgentMessage[]>;
isLoading: Ref<boolean>;
hasPendingApproval: ComputedRef<boolean>;
errorMessage: Ref<string>;
isStopped: Ref<boolean>;
rateLimit: ReturnType<typeof useAgentRateLimit>;
send: ReturnType<typeof useAgentChat>["send"];
stopGeneration: ReturnType<typeof useAgentChat>["stopGeneration"];
clear: ReturnType<typeof useAgentChat>["clear"];
loadMessages: ReturnType<typeof useAgentChat>["loadMessages"];
respondToApproval: ReturnType<typeof useAgentChat>["respondToApproval"];
sendFeedback: ReturnType<typeof useAgentChat>["sendFeedback"];
}
export interface UseAgentChatStoreOptions {
modelId: () => number | null;
enableThinking: () => boolean;
enableTools: () => boolean;
onStreamComplete?: (sessionId: string) => void;
}
export function useAgentChatStore(storeOptions: UseAgentChatStoreOptions) {
const instances = new Map<string, AgentChatInstance>();
const currentSessionId = ref<string | null>(null);
const loadedSessionIds = ref<Set<string>>(new Set());
function createInstance(sid: string): AgentChatInstance {
const options: UseAgentChatOptions = {
sessionId: () => sid,
modelId: storeOptions.modelId,
enableThinking: storeOptions.enableThinking,
enableTools: storeOptions.enableTools,
onStreamComplete: () => {
storeOptions.onStreamComplete?.(sid);
},
};
const chat = useAgentChat(options);
return chat;
}
function getInstance(sid: string): AgentChatInstance {
let inst = instances.get(sid);
if (!inst) {
inst = createInstance(sid);
instances.set(sid, inst);
}
return inst;
}
function removeInstance(sid: string) {
const inst = instances.get(sid);
if (inst) {
inst.stopGeneration();
inst.clear();
instances.delete(sid);
}
loadedSessionIds.value.delete(sid);
}
const currentInstance = computed<AgentChatInstance | null>(() => {
const sid = currentSessionId.value;
if (!sid) return null;
return getInstance(sid);
});
const messages = computed(() => currentInstance.value?.messages.value ?? []);
const isLoading = computed(() => currentInstance.value?.isLoading.value ?? false);
const hasPendingApproval = computed(() => currentInstance.value?.hasPendingApproval.value ?? false);
const errorMessage = computed(() => currentInstance.value?.errorMessage.value ?? "");
const isStopped = computed(() => currentInstance.value?.isStopped.value ?? false);
const fallbackRateLimit = useAgentRateLimit();
const rateLimit = computed(() => currentInstance.value?.rateLimit ?? fallbackRateLimit);
const anyLoading = computed(() => {
for (const inst of instances.values()) {
if (inst.isLoading.value) return true;
}
return false;
});
function getLoadingSessionIds(): string[] {
const ids: string[] = [];
for (const [sid, inst] of instances) {
if (inst.isLoading.value) ids.push(sid);
}
return ids;
}
async function loadMessages(sid: string) {
const inst = getInstance(sid);
loadedSessionIds.value.add(sid);
await inst.loadMessages(sid);
}
async function send(content: string, opts?: { editMessageId?: string; regenerate?: boolean }) {
const sid = currentSessionId.value;
if (!sid) return;
const inst = getInstance(sid);
await inst.send(content, opts);
}
async function respondToApproval(toolCallId: string, approved: boolean, reason?: string) {
const sid = currentSessionId.value;
if (!sid) return;
const inst = getInstance(sid);
await inst.respondToApproval(toolCallId, approved, reason);
}
async function sendFeedback(messageId: string, feedback: "like" | "dislike") {
const sid = currentSessionId.value;
if (!sid) return;
const inst = getInstance(sid);
await inst.sendFeedback(messageId, feedback);
}
function stopGeneration() {
const sid = currentSessionId.value;
if (!sid) return;
const inst = getInstance(sid);
inst.stopGeneration();
}
function stopAll() {
for (const inst of instances.values()) {
inst.stopGeneration();
}
}
function clear() {
const sid = currentSessionId.value;
if (!sid) return;
const inst = getInstance(sid);
inst.clear();
}
function setCurrentSessionId(sid: string | null) {
currentSessionId.value = sid;
}
function isSessionLoaded(sid: string): boolean {
return loadedSessionIds.value.has(sid);
}
return {
currentSessionId,
currentInstance,
messages,
isLoading,
hasPendingApproval,
errorMessage,
isStopped,
rateLimit,
anyLoading,
getLoadingSessionIds,
loadMessages,
send,
respondToApproval,
sendFeedback,
stopGeneration,
stopAll,
clear,
setCurrentSessionId,
isSessionLoaded,
removeInstance,
getInstance,
};
}

24
app/pages/index.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAgentSessions } from "~/composables/useAgentSessions"; import { useAgentSessions } from "~/composables/useAgentSessions";
import { useAgentChat } from "~/composables/useAgentChat"; import { useAgentChatStore } from "~/composables/useAgentChatStore";
import { useAuthSession } from "~/composables/useAuthSession"; import { useAuthSession } from "~/composables/useAuthSession";
definePageMeta({ definePageMeta({
@ -80,13 +80,11 @@ const enableTools = computed({
}, },
}); });
const chat = useAgentChat({ const chat = useAgentChatStore({
sessionId: () => sessions.currentSessionId.value,
modelId: () => currentModelId.value, modelId: () => currentModelId.value,
enableThinking: () => false, enableThinking: () => false,
enableTools: () => enableTools.value, enableTools: () => enableTools.value,
onStreamComplete: () => { onStreamComplete: (sid) => {
const sid = sessions.currentSessionId.value;
if (!sid) return; if (!sid) return;
setTimeout(() => sessions.refreshSessionTitle(sid), 3000); setTimeout(() => sessions.refreshSessionTitle(sid), 3000);
}, },
@ -95,8 +93,8 @@ const chat = useAgentChat({
const rateLimitInfo = computed(() => { const rateLimitInfo = computed(() => {
if (auth.loggedIn.value) return undefined; if (auth.loggedIn.value) return undefined;
return { return {
sessionRemaining: chat.rateLimit.sessionRemaining.value, sessionRemaining: chat.rateLimit.value.sessionRemaining.value,
ipRemaining: chat.rateLimit.ipRemaining.value, ipRemaining: chat.rateLimit.value.ipRemaining.value,
}; };
}); });
@ -148,6 +146,7 @@ async function loadSystemPrompt() {
watch( watch(
() => sessions.currentSessionId.value, () => sessions.currentSessionId.value,
async (sid) => { async (sid) => {
chat.setCurrentSessionId(sid);
if (sid) { if (sid) {
await chat.loadMessages(sid); await chat.loadMessages(sid);
} else { } else {
@ -170,7 +169,8 @@ async function handleSend(content: string) {
} }
if (sessions.currentSessionId.value) { if (sessions.currentSessionId.value) {
const sid = sessions.currentSessionId.value; const sid = sessions.currentSessionId.value;
const wasNewSession = chat.messages.value.filter((m) => m.role === "assistant").length === 0; const inst = chat.getInstance(sid);
const wasNewSession = inst.messages.value.filter((m) => m.role === "assistant").length === 0;
await chat.send(content); await chat.send(content);
sessions.touchSessionOrder(sid); sessions.touchSessionOrder(sid);
if (wasNewSession) { if (wasNewSession) {
@ -190,6 +190,12 @@ async function handleApprove(toolCallId: string, approved: boolean) {
await chat.respondToApproval(toolCallId, approved); await chat.respondToApproval(toolCallId, approved);
} }
async function handleDeleteSession(id: string) {
chat.stopGeneration();
chat.removeInstance(id);
await sessions.deleteSession(id);
}
async function handleFeedback(messageId: string, feedback: "like" | "dislike") { async function handleFeedback(messageId: string, feedback: "like" | "dislike") {
await chat.sendFeedback(messageId, feedback); await chat.sendFeedback(messageId, feedback);
} }
@ -229,7 +235,7 @@ onMounted(async () => {
@new-session="sessions.newSession(); if (isMobile) mobileSidebarOpen = false" @new-session="sessions.newSession(); if (isMobile) mobileSidebarOpen = false"
@select="sessions.selectSession($event); if (isMobile) mobileSidebarOpen = false" @select="sessions.selectSession($event); if (isMobile) mobileSidebarOpen = false"
@rename="sessions.renameSession" @rename="sessions.renameSession"
@delete="sessions.deleteSession" @delete="handleDeleteSession"
@login="handleLogin" @login="handleLogin"
@toggle="isMobile ? (mobileSidebarOpen = !mobileSidebarOpen) : (sidebarCollapsed = !sidebarCollapsed)" @toggle="isMobile ? (mobileSidebarOpen = !mobileSidebarOpen) : (sidebarCollapsed = !sidebarCollapsed)"
@close="mobileSidebarOpen = false" @close="mobileSidebarOpen = false"

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

2
server/api/agent/chat/index.post.ts

@ -433,7 +433,7 @@ export default defineEventHandler(async (event) => {
model: languageModel, model: languageModel,
system: systemPrompt || undefined, system: systemPrompt || undefined,
messages: modelMessages, messages: modelMessages,
maxOutputTokens: model.maxTokens || undefined, maxOutputTokens: model.maxTokens && model.maxTokens >= 1 && model.maxTokens <= 393216 ? model.maxTokens : undefined,
...(Object.keys(effectiveTools).length > 0 ...(Object.keys(effectiveTools).length > 0
? { tools: effectiveTools, stopWhen: stepCountIs(8) } ? { tools: effectiveTools, stopWhen: stepCountIs(8) }
: {}), : {}),

2
server/api/llm/chat/index.post.ts

@ -94,7 +94,7 @@ export default defineEventHandler(async (event) => {
const result = streamText({ const result = streamText({
model: languageModel, model: languageModel,
messages: modelMessages, messages: modelMessages,
maxOutputTokens: model.maxTokens || undefined, maxOutputTokens: model.maxTokens && model.maxTokens >= 1 && model.maxTokens <= 393216 ? model.maxTokens : undefined,
...(tools && Object.keys(tools).length > 0 ...(tools && Object.keys(tools).length > 0
? { tools, stopWhen: stepCountIs(8) } ? { tools, stopWhen: stepCountIs(8) }
: {}), : {}),

2
server/service/agent/temp-token.ts

@ -11,7 +11,7 @@ export function generateTempToken(): string {
export function setTempTokenCookie(event: H3Event, token: string): void { export function setTempTokenCookie(event: H3Event, token: string): void {
setCookie(event, TEMP_TOKEN_COOKIE, token, { setCookie(event, TEMP_TOKEN_COOKIE, token, {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", secure: false, // process.env.NODE_ENV === "production",
sameSite: "lax", sameSite: "lax",
maxAge: TEMP_TOKEN_TTL_MS / 1000, maxAge: TEMP_TOKEN_TTL_MS / 1000,
path: "/", path: "/",

2
server/service/auth/cookie.ts

@ -21,7 +21,7 @@ export function setSessionCookie(event: H3Event, sessionId: string) {
setCookie(event, SESSION_COOKIE_NAME, sessionId, { setCookie(event, SESSION_COOKIE_NAME, sessionId, {
httpOnly: true, httpOnly: true,
sameSite: "lax", sameSite: "lax",
secure: env.NODE_ENV === "production", secure: false, // env.NODE_ENV === "production",
path: SESSION_COOKIE_PATH, path: SESSION_COOKIE_PATH,
maxAge: SESSION_MAX_AGE_SECONDS, maxAge: SESSION_MAX_AGE_SECONDS,
}); });

Loading…
Cancel
Save