refactor(session-recovery): extract storage utilities to separate module
Split session-recovery.ts into modular structure: - types.ts: SDK-aligned type definitions - constants.ts: storage paths and part type sets - storage.ts: reusable read/write operations - index.ts: main recovery hook logic
This commit is contained in:
10
src/hooks/session-recovery/constants.ts
Normal file
10
src/hooks/session-recovery/constants.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { join } from "node:path"
|
||||||
|
import { xdgData } from "xdg-basedir"
|
||||||
|
|
||||||
|
export const OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage")
|
||||||
|
export const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message")
|
||||||
|
export const PART_STORAGE = join(OPENCODE_STORAGE, "part")
|
||||||
|
|
||||||
|
export const THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"])
|
||||||
|
export const META_TYPES = new Set(["step-start", "step-finish"])
|
||||||
|
export const CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"])
|
||||||
@@ -1,33 +1,16 @@
|
|||||||
/**
|
|
||||||
* Session Recovery - Message State Error Recovery
|
|
||||||
*
|
|
||||||
* Handles FOUR specific scenarios:
|
|
||||||
* 1. tool_use block exists without tool_result
|
|
||||||
* - Recovery: inject tool_result with "cancelled" content
|
|
||||||
*
|
|
||||||
* 2. Thinking block order violation (first block must be thinking)
|
|
||||||
* - Recovery: prepend empty thinking block
|
|
||||||
*
|
|
||||||
* 3. Thinking disabled but message contains thinking blocks
|
|
||||||
* - Recovery: strip thinking/redacted_thinking blocks
|
|
||||||
*
|
|
||||||
* 4. Empty content message (non-empty content required)
|
|
||||||
* - Recovery: inject text part directly via filesystem
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
||||||
import { join } from "node:path"
|
|
||||||
import { xdgData } from "xdg-basedir"
|
|
||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import type { createOpencodeClient } from "@opencode-ai/sdk"
|
import type { createOpencodeClient } from "@opencode-ai/sdk"
|
||||||
|
import { findFirstEmptyMessage, injectTextPart } from "./storage"
|
||||||
|
import type { MessageData } from "./types"
|
||||||
|
|
||||||
type Client = ReturnType<typeof createOpencodeClient>
|
type Client = ReturnType<typeof createOpencodeClient>
|
||||||
|
|
||||||
const OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage")
|
type RecoveryErrorType =
|
||||||
const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message")
|
| "tool_result_missing"
|
||||||
const PART_STORAGE = join(OPENCODE_STORAGE, "part")
|
| "thinking_block_order"
|
||||||
|
| "thinking_disabled_violation"
|
||||||
type RecoveryErrorType = "tool_result_missing" | "thinking_block_order" | "thinking_disabled_violation" | "empty_content_message" | null
|
| "empty_content_message"
|
||||||
|
| null
|
||||||
|
|
||||||
interface MessageInfo {
|
interface MessageInfo {
|
||||||
id?: string
|
id?: string
|
||||||
@@ -58,11 +41,6 @@ interface MessagePart {
|
|||||||
input?: Record<string, unknown>
|
input?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageData {
|
|
||||||
info?: MessageInfo
|
|
||||||
parts?: MessagePart[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getErrorMessage(error: unknown): string {
|
function getErrorMessage(error: unknown): string {
|
||||||
if (!error) return ""
|
if (!error) return ""
|
||||||
if (typeof error === "string") return error.toLowerCase()
|
if (typeof error === "string") return error.toLowerCase()
|
||||||
@@ -120,7 +98,7 @@ async function recoverToolResultMissing(
|
|||||||
try {
|
try {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
// @ts-expect-error - SDK types may not include tool_result parts, but runtime accepts it
|
// @ts-expect-error - SDK types may not include tool_result parts
|
||||||
body: { parts: toolResultParts },
|
body: { parts: toolResultParts },
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -150,26 +128,17 @@ async function recoverThinkingBlockOrder(
|
|||||||
path: { id: messageID },
|
path: { id: messageID },
|
||||||
body: { parts: patchedParts },
|
body: { parts: patchedParts },
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {}
|
||||||
// message.update not available
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error - Experimental API
|
// @ts-expect-error - Experimental API
|
||||||
await client.session.patch?.({
|
await client.session.patch?.({
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: {
|
body: { messageID, parts: patchedParts },
|
||||||
messageID,
|
|
||||||
parts: patchedParts,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {}
|
||||||
// session.patch not available
|
|
||||||
}
|
|
||||||
|
|
||||||
return await fallbackRevertStrategy(client, sessionID, failedAssistantMsg, directory)
|
return await fallbackRevertStrategy(client, sessionID, failedAssistantMsg, directory)
|
||||||
}
|
}
|
||||||
@@ -197,205 +166,31 @@ async function recoverThinkingDisabledViolation(
|
|||||||
path: { id: messageID },
|
path: { id: messageID },
|
||||||
body: { parts: strippedParts },
|
body: { parts: strippedParts },
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {}
|
||||||
// message.update not available
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error - Experimental API
|
// @ts-expect-error - Experimental API
|
||||||
await client.session.patch?.({
|
await client.session.patch?.({
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: {
|
body: { messageID, parts: strippedParts },
|
||||||
messageID,
|
|
||||||
parts: strippedParts,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {}
|
||||||
// session.patch not available
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"])
|
|
||||||
const META_TYPES = new Set(["step-start", "step-finish"])
|
|
||||||
|
|
||||||
interface StoredMessageMeta {
|
|
||||||
id: string
|
|
||||||
sessionID: string
|
|
||||||
role: string
|
|
||||||
parentID?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StoredPart {
|
|
||||||
id: string
|
|
||||||
sessionID: string
|
|
||||||
messageID: string
|
|
||||||
type: string
|
|
||||||
text?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function generatePartId(): string {
|
|
||||||
const timestamp = Date.now().toString(16)
|
|
||||||
const random = Math.random().toString(36).substring(2, 10)
|
|
||||||
return `prt_${timestamp}${random}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMessageDir(sessionID: string): string {
|
|
||||||
const projectHash = readdirSync(MESSAGE_STORAGE).find((dir) => {
|
|
||||||
const sessionDir = join(MESSAGE_STORAGE, dir)
|
|
||||||
try {
|
|
||||||
return readdirSync(sessionDir).some((f) => f.includes(sessionID.replace("ses_", "")))
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (projectHash) {
|
|
||||||
return join(MESSAGE_STORAGE, projectHash, sessionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const dir of readdirSync(MESSAGE_STORAGE)) {
|
|
||||||
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
|
|
||||||
if (existsSync(sessionPath)) {
|
|
||||||
return sessionPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function readMessagesFromStorage(sessionID: string): StoredMessageMeta[] {
|
|
||||||
const messageDir = getMessageDir(sessionID)
|
|
||||||
if (!messageDir || !existsSync(messageDir)) return []
|
|
||||||
|
|
||||||
const messages: StoredMessageMeta[] = []
|
|
||||||
for (const file of readdirSync(messageDir)) {
|
|
||||||
if (!file.endsWith(".json")) continue
|
|
||||||
try {
|
|
||||||
const content = readFileSync(join(messageDir, file), "utf-8")
|
|
||||||
messages.push(JSON.parse(content))
|
|
||||||
} catch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages.sort((a, b) => a.id.localeCompare(b.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
function readPartsFromStorage(messageID: string): StoredPart[] {
|
|
||||||
const partDir = join(PART_STORAGE, messageID)
|
|
||||||
if (!existsSync(partDir)) return []
|
|
||||||
|
|
||||||
const parts: StoredPart[] = []
|
|
||||||
for (const file of readdirSync(partDir)) {
|
|
||||||
if (!file.endsWith(".json")) continue
|
|
||||||
try {
|
|
||||||
const content = readFileSync(join(partDir, file), "utf-8")
|
|
||||||
parts.push(JSON.parse(content))
|
|
||||||
} catch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectTextPartToStorage(sessionID: string, messageID: string, text: string): boolean {
|
|
||||||
const partDir = join(PART_STORAGE, messageID)
|
|
||||||
|
|
||||||
if (!existsSync(partDir)) {
|
|
||||||
mkdirSync(partDir, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const partId = generatePartId()
|
|
||||||
const part: StoredPart = {
|
|
||||||
id: partId,
|
|
||||||
sessionID,
|
|
||||||
messageID,
|
|
||||||
type: "text",
|
|
||||||
text,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
writeFileSync(join(partDir, `${partId}.json`), JSON.stringify(part, null, 2))
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findEmptyContentMessageFromStorage(sessionID: string): string | null {
|
|
||||||
const messages = readMessagesFromStorage(sessionID)
|
|
||||||
|
|
||||||
for (let i = 0; i < messages.length; i++) {
|
|
||||||
const msg = messages[i]
|
|
||||||
if (msg.role !== "assistant") continue
|
|
||||||
|
|
||||||
const isLastMessage = i === messages.length - 1
|
|
||||||
if (isLastMessage) continue
|
|
||||||
|
|
||||||
const parts = readPartsFromStorage(msg.id)
|
|
||||||
const hasContent = parts.some((p) => {
|
|
||||||
if (THINKING_TYPES.has(p.type)) return false
|
|
||||||
if (META_TYPES.has(p.type)) return false
|
|
||||||
if (p.type === "text" && p.text?.trim()) return true
|
|
||||||
if (p.type === "tool_use" || p.type === "tool") return true
|
|
||||||
if (p.type === "tool_result") return true
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!hasContent) {
|
|
||||||
return msg.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasNonEmptyOutput(msg: MessageData): boolean {
|
|
||||||
const parts = msg.parts
|
|
||||||
if (!parts || parts.length === 0) return false
|
|
||||||
|
|
||||||
return parts.some((p) => {
|
|
||||||
if (THINKING_TYPES.has(p.type)) return false
|
|
||||||
if (p.type === "step-start" || p.type === "step-finish") return false
|
|
||||||
if (p.type === "text" && p.text && p.text.trim()) return true
|
|
||||||
if ((p.type === "tool_use" || p.type === "tool") && p.id) return true
|
|
||||||
if (p.type === "tool_result") return true
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function findEmptyContentMessage(msgs: MessageData[]): MessageData | null {
|
|
||||||
for (let i = 0; i < msgs.length; i++) {
|
|
||||||
const msg = msgs[i]
|
|
||||||
const isLastMessage = i === msgs.length - 1
|
|
||||||
const isAssistant = msg.info?.role === "assistant"
|
|
||||||
|
|
||||||
if (isLastMessage && isAssistant) continue
|
|
||||||
|
|
||||||
if (!hasNonEmptyOutput(msg)) {
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function recoverEmptyContentMessage(
|
async function recoverEmptyContentMessage(
|
||||||
_client: Client,
|
_client: Client,
|
||||||
sessionID: string,
|
sessionID: string,
|
||||||
failedAssistantMsg: MessageData,
|
failedAssistantMsg: MessageData,
|
||||||
_directory: string
|
_directory: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const emptyMessageID = findEmptyContentMessageFromStorage(sessionID) || failedAssistantMsg.info?.id
|
const emptyMessageID = findFirstEmptyMessage(sessionID) || failedAssistantMsg.info?.id
|
||||||
if (!emptyMessageID) return false
|
if (!emptyMessageID) return false
|
||||||
|
|
||||||
return injectTextPartToStorage(sessionID, emptyMessageID, "(interrupted)")
|
return injectTextPart(sessionID, emptyMessageID, "(interrupted)")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fallbackRevertStrategy(
|
async function fallbackRevertStrategy(
|
||||||
@@ -508,16 +303,14 @@ export function createSessionRecoveryHook(ctx: PluginInput) {
|
|||||||
tool_result_missing: "Injecting cancelled tool results...",
|
tool_result_missing: "Injecting cancelled tool results...",
|
||||||
thinking_block_order: "Fixing message structure...",
|
thinking_block_order: "Fixing message structure...",
|
||||||
thinking_disabled_violation: "Stripping thinking blocks...",
|
thinking_disabled_violation: "Stripping thinking blocks...",
|
||||||
empty_content_message: "Deleting empty message...",
|
empty_content_message: "Fixing empty message...",
|
||||||
}
|
}
|
||||||
const toastTitle = toastTitles[errorType]
|
|
||||||
const toastMessage = toastMessages[errorType]
|
|
||||||
|
|
||||||
await ctx.client.tui
|
await ctx.client.tui
|
||||||
.showToast({
|
.showToast({
|
||||||
body: {
|
body: {
|
||||||
title: toastTitle,
|
title: toastTitles[errorType],
|
||||||
message: toastMessage,
|
message: toastMessages[errorType],
|
||||||
variant: "warning",
|
variant: "warning",
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
},
|
},
|
||||||
138
src/hooks/session-recovery/storage.ts
Normal file
138
src/hooks/session-recovery/storage.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import { MESSAGE_STORAGE, PART_STORAGE, THINKING_TYPES, META_TYPES } from "./constants"
|
||||||
|
import type { StoredMessageMeta, StoredPart, StoredTextPart } from "./types"
|
||||||
|
|
||||||
|
export function generatePartId(): string {
|
||||||
|
const timestamp = Date.now().toString(16)
|
||||||
|
const random = Math.random().toString(36).substring(2, 10)
|
||||||
|
return `prt_${timestamp}${random}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMessageDir(sessionID: string): string {
|
||||||
|
if (!existsSync(MESSAGE_STORAGE)) return ""
|
||||||
|
|
||||||
|
const directPath = join(MESSAGE_STORAGE, sessionID)
|
||||||
|
if (existsSync(directPath)) {
|
||||||
|
return directPath
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dir of readdirSync(MESSAGE_STORAGE)) {
|
||||||
|
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
|
||||||
|
if (existsSync(sessionPath)) {
|
||||||
|
return sessionPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readMessages(sessionID: string): StoredMessageMeta[] {
|
||||||
|
const messageDir = getMessageDir(sessionID)
|
||||||
|
if (!messageDir || !existsSync(messageDir)) return []
|
||||||
|
|
||||||
|
const messages: StoredMessageMeta[] = []
|
||||||
|
for (const file of readdirSync(messageDir)) {
|
||||||
|
if (!file.endsWith(".json")) continue
|
||||||
|
try {
|
||||||
|
const content = readFileSync(join(messageDir, file), "utf-8")
|
||||||
|
messages.push(JSON.parse(content))
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages.sort((a, b) => a.id.localeCompare(b.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readParts(messageID: string): StoredPart[] {
|
||||||
|
const partDir = join(PART_STORAGE, messageID)
|
||||||
|
if (!existsSync(partDir)) return []
|
||||||
|
|
||||||
|
const parts: StoredPart[] = []
|
||||||
|
for (const file of readdirSync(partDir)) {
|
||||||
|
if (!file.endsWith(".json")) continue
|
||||||
|
try {
|
||||||
|
const content = readFileSync(join(partDir, file), "utf-8")
|
||||||
|
parts.push(JSON.parse(content))
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasContent(part: StoredPart): boolean {
|
||||||
|
if (THINKING_TYPES.has(part.type)) return false
|
||||||
|
if (META_TYPES.has(part.type)) return false
|
||||||
|
|
||||||
|
if (part.type === "text") {
|
||||||
|
const textPart = part as StoredTextPart
|
||||||
|
return !!(textPart.text?.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.type === "tool" || part.type === "tool_use") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.type === "tool_result") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messageHasContent(messageID: string): boolean {
|
||||||
|
const parts = readParts(messageID)
|
||||||
|
return parts.some(hasContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function injectTextPart(sessionID: string, messageID: string, text: string): boolean {
|
||||||
|
const partDir = join(PART_STORAGE, messageID)
|
||||||
|
|
||||||
|
if (!existsSync(partDir)) {
|
||||||
|
mkdirSync(partDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const partId = generatePartId()
|
||||||
|
const part: StoredTextPart = {
|
||||||
|
id: partId,
|
||||||
|
sessionID,
|
||||||
|
messageID,
|
||||||
|
type: "text",
|
||||||
|
text,
|
||||||
|
synthetic: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(join(partDir, `${partId}.json`), JSON.stringify(part, null, 2))
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findEmptyMessages(sessionID: string): string[] {
|
||||||
|
const messages = readMessages(sessionID)
|
||||||
|
const emptyIds: string[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < messages.length; i++) {
|
||||||
|
const msg = messages[i]
|
||||||
|
if (msg.role !== "assistant") continue
|
||||||
|
|
||||||
|
const isLastMessage = i === messages.length - 1
|
||||||
|
if (isLastMessage) continue
|
||||||
|
|
||||||
|
if (!messageHasContent(msg.id)) {
|
||||||
|
emptyIds.push(msg.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyIds
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findFirstEmptyMessage(sessionID: string): string | null {
|
||||||
|
const emptyIds = findEmptyMessages(sessionID)
|
||||||
|
return emptyIds.length > 0 ? emptyIds[0] : null
|
||||||
|
}
|
||||||
82
src/hooks/session-recovery/types.ts
Normal file
82
src/hooks/session-recovery/types.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
export type ThinkingPartType = "thinking" | "redacted_thinking" | "reasoning"
|
||||||
|
export type MetaPartType = "step-start" | "step-finish"
|
||||||
|
export type ContentPartType = "text" | "tool" | "tool_use" | "tool_result"
|
||||||
|
|
||||||
|
export interface StoredMessageMeta {
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
role: "user" | "assistant"
|
||||||
|
parentID?: string
|
||||||
|
time?: {
|
||||||
|
created: number
|
||||||
|
completed?: number
|
||||||
|
}
|
||||||
|
error?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoredTextPart {
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
type: "text"
|
||||||
|
text: string
|
||||||
|
synthetic?: boolean
|
||||||
|
ignored?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoredToolPart {
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
type: "tool"
|
||||||
|
callID: string
|
||||||
|
tool: string
|
||||||
|
state: {
|
||||||
|
status: "pending" | "running" | "completed" | "error"
|
||||||
|
input: Record<string, unknown>
|
||||||
|
output?: string
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoredReasoningPart {
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
type: "reasoning"
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoredStepPart {
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
type: "step-start" | "step-finish"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StoredPart = StoredTextPart | StoredToolPart | StoredReasoningPart | StoredStepPart | {
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
type: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageData {
|
||||||
|
info?: {
|
||||||
|
id?: string
|
||||||
|
role?: string
|
||||||
|
sessionID?: string
|
||||||
|
parentID?: string
|
||||||
|
error?: unknown
|
||||||
|
}
|
||||||
|
parts?: Array<{
|
||||||
|
type: string
|
||||||
|
id?: string
|
||||||
|
text?: string
|
||||||
|
thinking?: string
|
||||||
|
name?: string
|
||||||
|
input?: Record<string, unknown>
|
||||||
|
callID?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user