Files
NomadArch/packages/ui/src/lib/lite-mode.ts
Gemini AI 64c7fb8d47
Some checks failed
Release Binaries / release (push) Has been cancelled
Add automatic session migration when switching from SDK to Native mode
2025-12-27 11:13:43 +04:00

276 lines
9.5 KiB
TypeScript

/**
* Lite Mode API Client - Binary-Free Mode
*
* This provides a client for working with NomadArch in Binary-Free Mode,
* using native session management instead of the OpenCode binary.
*/
import { CODENOMAD_API_BASE } from "./api-client"
import { getLogger } from "./logger"
const log = getLogger("lite-mode")
export interface ModeInfo {
mode: "lite" | "full"
binaryFreeMode: boolean
nativeSessions: boolean
opencodeBinaryAvailable: boolean
providers: {
qwen: boolean
zai: boolean
zen: boolean
}
}
export interface NativeSession {
id: string
workspaceId: string
title?: string
parentId?: string | null
createdAt: number
updatedAt: number
messageIds: string[]
model?: {
providerId: string
modelId: string
}
agent?: string
}
export interface NativeMessage {
id: string
sessionId: string
role: "user" | "assistant" | "system" | "tool"
content?: string
createdAt: number
updatedAt: number
status?: "pending" | "streaming" | "completed" | "error"
}
let modeCache: ModeInfo | null = null
/**
* Get the current running mode (lite or full)
*/
export async function getMode(): Promise<ModeInfo> {
if (modeCache) return modeCache
try {
const response = await fetch(`${CODENOMAD_API_BASE}/api/meta/mode`)
if (!response.ok) {
throw new Error(`Failed to fetch mode: ${response.status}`)
}
modeCache = await response.json()
log.info(`Running in ${modeCache?.mode} mode`, { binaryFree: modeCache?.binaryFreeMode })
return modeCache!
} catch (error) {
log.warn("Failed to fetch mode, assuming lite mode", error)
// Default to lite mode if we can't determine
return {
mode: "lite",
binaryFreeMode: true,
nativeSessions: true,
opencodeBinaryAvailable: false,
providers: { qwen: true, zai: true, zen: true }
}
}
}
/**
* Check if running in Binary-Free (lite) mode
*/
export async function isLiteMode(): Promise<boolean> {
const mode = await getMode()
return mode.binaryFreeMode
}
/**
* Native Session API for Binary-Free Mode
*/
export const nativeSessionApi = {
async listSessions(workspaceId: string): Promise<NativeSession[]> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions`)
if (!response.ok) throw new Error("Failed to list sessions")
const data = await response.json()
return data.sessions
},
async createSession(workspaceId: string, options?: {
title?: string
parentId?: string
model?: { providerId: string; modelId: string }
agent?: string
}): Promise<NativeSession> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(options ?? {})
})
if (!response.ok) throw new Error("Failed to create session")
const data = await response.json()
return data.session
},
async getSession(workspaceId: string, sessionId: string): Promise<NativeSession | null> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`)
if (response.status === 404) return null
if (!response.ok) throw new Error("Failed to get session")
const data = await response.json()
return data.session
},
async updateSession(workspaceId: string, sessionId: string, updates: Partial<NativeSession>): Promise<NativeSession | null> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates)
})
if (response.status === 404) return null
if (!response.ok) throw new Error("Failed to update session")
const data = await response.json()
return data.session
},
async deleteSession(workspaceId: string, sessionId: string): Promise<boolean> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`, {
method: "DELETE"
})
return response.ok || response.status === 204
},
async forkSession(workspaceId: string, sessionId: string): Promise<NativeSession> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/fork`, {
method: "POST"
})
if (!response.ok) throw new Error("Failed to fork session")
const data = await response.json()
return data.session
},
async revertSession(workspaceId: string, sessionId: string, messageId?: string): Promise<NativeSession> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/revert`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messageId })
})
if (!response.ok) throw new Error("Failed to revert session")
const data = await response.json()
return data.session
},
async getMessages(workspaceId: string, sessionId: string): Promise<NativeMessage[]> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/messages`)
if (!response.ok) throw new Error("Failed to get messages")
const data = await response.json()
return data.messages
},
/**
* Import sessions from SDK mode to Native mode
*/
async importSessions(workspaceId: string, sessions: Array<{
id: string
title?: string
parentId?: string | null
createdAt?: number
updatedAt?: number
model?: { providerId: string; modelId: string }
agent?: string
messages?: Array<{
id: string
role: "user" | "assistant" | "system" | "tool"
content?: string
createdAt?: number
}>
}>): Promise<{ success: boolean; imported: number; skipped: number }> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/import`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessions })
})
if (!response.ok) throw new Error("Failed to import sessions")
return response.json()
},
/**
* Send a prompt to the session and get a streaming response
*/
async* streamPrompt(
workspaceId: string,
sessionId: string,
content: string,
options?: {
provider?: "qwen" | "zai" | "zen"
accessToken?: string
resourceUrl?: string
enableTools?: boolean
}
): AsyncGenerator<{ type: "content" | "done" | "error"; data?: string }> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/prompt`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content,
provider: options?.provider ?? "qwen",
accessToken: options?.accessToken,
resourceUrl: options?.resourceUrl,
enableTools: options?.enableTools ?? true
})
})
if (!response.ok) {
yield { type: "error", data: `Request failed: ${response.status}` }
return
}
const reader = response.body?.getReader()
if (!reader) {
yield { type: "error", data: "No response body" }
return
}
const decoder = new TextDecoder()
let buffer = ""
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n")
buffer = lines.pop() ?? ""
for (const line of lines) {
if (!line.trim()) continue
if (line.startsWith("data: ")) {
const data = line.slice(6)
if (data === "[DONE]") {
yield { type: "done" }
return
}
try {
const parsed = JSON.parse(data)
if (parsed.error) {
yield { type: "error", data: parsed.error }
} else if (parsed.choices?.[0]?.delta?.content) {
yield { type: "content", data: parsed.choices[0].delta.content }
}
} catch {
// Skip invalid JSON
}
}
}
}
yield { type: "done" }
}
}
/**
* Clear mode cache (for testing or after config changes)
*/
export function clearModeCache(): void {
modeCache = null
}