Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/main/claude/claude-structured-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ function sessionFor(send = vi.fn().mockResolvedValue(undefined)): ClaudeSession
dispatchWaiters: [],
options: new Map(),
reportedOptions: {},
events: undefined
events: undefined,
translator: null
}
}

Expand Down
158 changes: 158 additions & 0 deletions src/main/claude/claude-structured-item-translation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import type {
AgentJournalItemBody,
AgentJournalItemIdentity,
AgentJournalMessageItem
} from '../../shared/agent-session-journal-types'
import type { NativeChatBlock } from '../../shared/native-chat-types'
import {
boundInlineText,
DEFAULT_JOURNAL_PAYLOAD_LIMITS
} from '../native-chat/agent-session-journal/journal-payload-bounds'

export type ClaudeMessageEnvelope = {
sessionId: string
uuid: string
role: 'assistant' | 'user'
content: unknown[]
}

export type ClaudeToolUse = { id: string; name: string; input: unknown }
export type ClaudeToolResult = { toolUseId: string; output: string; failed: boolean }

export function claudeRecord(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null
}

export function claudeText(value: unknown): string | null {
return typeof value === 'string' && value.length > 0 ? value : null
}

export function readClaudeMessageEnvelope(
frame: Record<string, unknown>
): ClaudeMessageEnvelope | null {
if (frame.type !== 'assistant' && frame.type !== 'user') {
return null
}
const message = claudeRecord(frame.message)
const sessionId = claudeText(frame.session_id)
const uuid = claudeText(frame.uuid)
const role = message?.role
return sessionId && uuid && (role === 'assistant' || role === 'user')
? {
sessionId,
uuid,
role,
content: Array.isArray(message?.content) ? message.content : []
}
: null
}

export function claudeMessageIdentity(
envelope: Pick<ClaudeMessageEnvelope, 'sessionId' | 'uuid'>
): AgentJournalItemIdentity {
return { provider: 'claude', sessionId: envelope.sessionId, uuid: envelope.uuid }
}

function messageBlocks(envelope: ClaudeMessageEnvelope): NativeChatBlock[] {
const blocks: NativeChatBlock[] = []
for (const value of envelope.content) {
const part = claudeRecord(value)
const text = claudeText(part?.text)
if (part?.type === 'text' && text) {
blocks.push({ type: 'text', text })
continue
}
const source = claudeRecord(part?.source)
const url = claudeText(source?.url)
if (part?.type === 'image' && source?.type === 'url' && url) {
blocks.push({ type: 'image-ref', url })
}
}
return blocks
}

export function claudeMessageBody(envelope: ClaudeMessageEnvelope): AgentJournalMessageItem | null {
const blocks = messageBlocks(envelope)
return blocks.length > 0 ? { kind: 'message', role: envelope.role, blocks } : null
}

export function claudeToolUses(envelope: ClaudeMessageEnvelope): ClaudeToolUse[] {
return envelope.content.flatMap((value) => {
const part = claudeRecord(value)
const id = claudeText(part?.id)
const name = claudeText(part?.name)
return part?.type === 'tool_use' && id && name ? [{ id, name, input: part.input ?? null }] : []
})
}

function resultText(value: unknown): string {
if (typeof value === 'string') {
return value
}
if (!Array.isArray(value)) {
return value === undefined ? '' : JSON.stringify(value)
}
return value
.flatMap((entry) => {
if (typeof entry === 'string') {
return [entry]
}
const part = claudeRecord(entry)
return part?.type === 'text' && typeof part.text === 'string' ? [part.text] : []
})
.join('\n')
}

export function claudeToolResults(envelope: ClaudeMessageEnvelope): ClaudeToolResult[] {
return envelope.content.flatMap((value) => {
const part = claudeRecord(value)
const toolUseId = claudeText(part?.tool_use_id)
return part?.type === 'tool_result' && toolUseId
? [
{
toolUseId,
output: resultText(part.content),
failed: part.is_error === true
}
]
: []
})
}

export function claudeThinkingText(envelope: ClaudeMessageEnvelope): string | null {
const parts = envelope.content.flatMap((value) => {
const part = claudeRecord(value)
const thinking = claudeText(part?.thinking)
return part?.type === 'thinking' && thinking ? [thinking] : []
})
return parts.length > 0 ? parts.join('\n') : null
}

export function claudeToolBody(input: {
tool: ClaudeToolUse
result?: ClaudeToolResult
}): AgentJournalItemBody {
return {
kind: 'tool-call',
name: input.tool.name,
input: input.tool.input,
state: input.result ? (input.result.failed ? 'failed' : 'completed') : 'running',
...(input.result
? { output: boundInlineText(input.result.output, DEFAULT_JOURNAL_PAYLOAD_LIMITS).bounded }
: {})
}
}

export function claudeStreamingMessageBody(text: string): AgentJournalMessageItem {
return { kind: 'message', role: 'assistant', blocks: [{ type: 'text', text }] }
}

export function claudeToolIdentity(sessionId: string, toolUseId: string): AgentJournalItemIdentity {
return { provider: 'orca', clientMessageId: `claude-tool:${sessionId}:${toolUseId}` }
}

export function claudeThinkingIdentity(sessionId: string, uuid: string): AgentJournalItemIdentity {
return { provider: 'orca', clientMessageId: `claude-thinking:${sessionId}:${uuid}` }
}
Loading
Loading