Files
orca/src/main/native-chat/transcript-opencode-part-blocks.ts
T
Neil 89f9ce08de refactor(shared): organize session contracts and projections (#36979)
* refactor(shared): organize session contracts and projections

* chore: tighten dropped-path flaky pattern allowance

* docs: preserve current agent instructions with valid formatting
2026-11-01 03:14:46 -08:00

180 lines
4.9 KiB
TypeScript

import { fileURLToPath } from 'node:url'
import type {
NativeChatBlock,
NativeChatImageRefBlock,
NativeChatMessage,
NativeChatTextBlock
} from '../../shared/native-chat/native-chat-types'
import { agentSessionHostStatusBody } from '../../shared/agent-session/view/agent-session-host-status-rows'
import { asRecord, extractString, parseJsonObject } from '../ai-vault/session-scanner-values'
// Electron-free: imported by the OpenCode SQLite worker.
export type OpenCodePartRow = {
message_id: string
time_updated: number
data: string | null
}
export const OPENCODE_TRANSCRIPT_MAX_ROW_BYTES = 2 * 1024 * 1024
export function opencodeMessages(
message: Pick<NativeChatMessage, 'id' | 'role' | 'timestamp'>,
partRows: OpenCodePartRow[],
additionalBlocks: NativeChatBlock[] = []
): NativeChatMessage[] {
const { blocks, reasoning, notices } = opencodeMessageContent(partRows)
blocks.push(...additionalBlocks)
const messages: NativeChatMessage[] = []
if (reasoning.length > 0) {
messages.push({
...message,
id: blocks.length > 0 || notices.length > 0 ? `${message.id}:reasoning` : message.id,
role: 'reasoning',
blocks: reasoning,
source: 'transcript'
})
}
if (blocks.length > 0) {
messages.push({ ...message, blocks, source: 'transcript' })
}
if (notices.length > 0) {
messages.push({
...message,
id: messages.length > 0 ? `${message.id}:omission` : message.id,
role: 'system',
blocks: notices,
source: 'transcript'
})
}
return messages
}
function opencodeMessageContent(partRows: OpenCodePartRow[]): {
blocks: NativeChatBlock[]
reasoning: NativeChatBlock[]
notices: NativeChatTextBlock[]
} {
const blocks: NativeChatBlock[] = []
const reasoning: NativeChatBlock[] = []
const notices: NativeChatTextBlock[] = []
for (const partRow of partRows) {
if (partRow.data === null) {
const { text, presentation } = agentSessionHostStatusBody('history-item-too-large')
notices.push({ type: 'text', text, presentation })
continue
}
const part = parseJsonObject(partRow.data)
if (!part) {
continue
}
switch (part.type) {
case 'text': {
if (part.synthetic === true) {
break
}
const text = extractString(part.text)
if (text) {
blocks.push({ type: 'text', text })
}
break
}
case 'reasoning': {
const text = extractString(part.text)
if (text) {
reasoning.push({ type: 'text', text })
}
break
}
case 'tool': {
blocks.push(...opencodeToolBlocks(part))
break
}
case 'patch': {
const files = Array.isArray(part.files)
? part.files.filter((file): file is string => typeof file === 'string')
: []
blocks.push({
type: 'tool-call',
name: 'patch',
state: 'completed',
input: { hash: extractString(part.hash), files }
})
break
}
case 'file': {
const block = opencodeFileBlock(part)
if (block) {
blocks.push(block)
}
break
}
default:
break
}
}
return { blocks, reasoning, notices }
}
function opencodeFileBlock(part: Record<string, unknown>): NativeChatImageRefBlock | null {
const mime = extractString(part.mime)
if (!mime?.startsWith('image/')) {
return null
}
const url = extractString(part.url)
if (!url) {
return null
}
const alt = extractString(part.filename)
const withAlt = alt ? { alt } : {}
if (url.startsWith('data:') || /^https?:\/\//.test(url)) {
return { type: 'image-ref', url, ...withAlt }
}
if (url.startsWith('file://')) {
try {
return { type: 'image-ref', path: fileURLToPath(url), ...withAlt }
} catch {
return { type: 'image-ref', url, ...withAlt }
}
}
return { type: 'image-ref', path: url, ...withAlt }
}
function opencodeToolBlocks(part: Record<string, unknown>): NativeChatBlock[] {
const name = extractString(part.tool) ?? 'tool'
const state = asRecord(part.state)
const callId = extractString(part.callID) ?? extractString(part.id)
const status = state?.status
const lifecycle = status === 'completed' ? 'completed' : status === 'error' ? 'failed' : 'running'
const blocks: NativeChatBlock[] = [
{
type: 'tool-call',
name,
...(callId ? { callId } : {}),
state: lifecycle,
input: state ? state.input : undefined
}
]
if (!state) {
return blocks
}
const output = state.output
const error = state.error
if (typeof output !== 'string' && error == null) {
return blocks
}
blocks.push({
type: 'tool-result',
...(callId ? { callId } : {}),
output:
typeof output === 'string'
? output
: typeof error === 'string'
? error
: error != null
? JSON.stringify(error)
: '',
...(error != null ? { isError: true } : {})
})
return blocks
}