mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
151 lines
5.9 KiB
TypeScript
151 lines
5.9 KiB
TypeScript
import type { AgentState } from '@/types/api'
|
|
import type { AgentEvent, ChatBlock, NormalizedMessage, UsageData } from '@/chat/types'
|
|
import type { ThreadGoal } from '@/types/api'
|
|
import { traceMessages, type TracedMessage } from '@/chat/tracer'
|
|
import { dedupeAgentEvents, foldApiErrorEvents } from '@/chat/reducerEvents'
|
|
import { collectTitleChanges, collectToolIdsFromMessages, ensureToolBlock, getPermissions } from '@/chat/reducerTools'
|
|
import { reduceTimeline } from '@/chat/reducerTimeline'
|
|
|
|
// Calculate context size from usage data
|
|
function calculateContextSize(usage: UsageData): number {
|
|
if (typeof usage.context_tokens === 'number') {
|
|
return usage.context_tokens
|
|
}
|
|
return (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0) + usage.input_tokens
|
|
}
|
|
|
|
function isUsageVisibleInParentContext(usage: UsageData): boolean {
|
|
return usage.scope_role !== 'child'
|
|
}
|
|
|
|
export type LatestUsage = {
|
|
inputTokens: number
|
|
outputTokens: number
|
|
cacheCreation: number
|
|
cacheRead: number
|
|
contextSize: number
|
|
contextWindow: number | null
|
|
timestamp: number
|
|
}
|
|
|
|
function getLatestThreadGoal(normalized: NormalizedMessage[]): ThreadGoal | null {
|
|
let sawNewerNonGoalUserMessage = false
|
|
for (let i = normalized.length - 1; i >= 0; i--) {
|
|
const msg = normalized[i]
|
|
if (msg.role === 'user') {
|
|
if (!/^\s*\/goal(?:\s|$)/i.test(msg.content.text)) {
|
|
sawNewerNonGoalUserMessage = true
|
|
}
|
|
continue
|
|
}
|
|
if (msg.role !== 'event') continue
|
|
const event = msg.content as AgentEvent
|
|
if (event.type === 'thread-goal-cleared') return null
|
|
if (event.type === 'thread-goal-updated') {
|
|
const goal = (event as { goal?: ThreadGoal }).goal ?? null
|
|
if (goal?.status === 'complete' && sawNewerNonGoalUserMessage) {
|
|
return null
|
|
}
|
|
return goal
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function reduceChatBlocks(
|
|
normalized: NormalizedMessage[],
|
|
agentState: AgentState | null | undefined
|
|
): { blocks: ChatBlock[]; hasReadyEvent: boolean; latestUsage: LatestUsage | null; latestGoal: ThreadGoal | null } {
|
|
const permissionsById = getPermissions(agentState)
|
|
const toolIdsInMessages = collectToolIdsFromMessages(normalized)
|
|
const titleChangesByToolUseId = collectTitleChanges(normalized)
|
|
|
|
const traced = traceMessages(normalized)
|
|
const groups = new Map<string, TracedMessage[]>()
|
|
const root: TracedMessage[] = []
|
|
|
|
for (const msg of traced) {
|
|
if (msg.sidechainId) {
|
|
const existing = groups.get(msg.sidechainId) ?? []
|
|
existing.push(msg)
|
|
groups.set(msg.sidechainId, existing)
|
|
} else {
|
|
root.push(msg)
|
|
}
|
|
}
|
|
|
|
const consumedGroupIds = new Set<string>()
|
|
const emittedTitleChangeToolUseIds = new Set<string>()
|
|
const reducerContext = { permissionsById, groups, consumedGroupIds, titleChangesByToolUseId, emittedTitleChangeToolUseIds }
|
|
const rootResult = reduceTimeline(root, reducerContext)
|
|
let hasReadyEvent = rootResult.hasReadyEvent
|
|
|
|
// Only create permission-only tool cards when there is no tool call/result in the transcript.
|
|
// Also skip if the permission is older than the oldest message in the current view,
|
|
// to avoid mixing old tool cards with newer messages when paginating.
|
|
const oldestMessageTime = normalized.length > 0
|
|
? Math.min(...normalized.map(m => m.createdAt))
|
|
: null
|
|
|
|
for (const [id, entry] of permissionsById) {
|
|
if (toolIdsInMessages.has(id)) continue
|
|
if (rootResult.toolBlocksById.has(id)) continue
|
|
|
|
const createdAt = entry.permission.createdAt ?? Date.now()
|
|
|
|
// Skip permissions that are older than the oldest message in the current view.
|
|
// These will be shown when the user loads older messages.
|
|
if (oldestMessageTime !== null && createdAt < oldestMessageTime) {
|
|
continue
|
|
}
|
|
|
|
const block = ensureToolBlock(rootResult.blocks, rootResult.toolBlocksById, id, {
|
|
createdAt,
|
|
localId: null,
|
|
name: entry.toolName,
|
|
input: entry.input,
|
|
description: null,
|
|
permission: entry.permission
|
|
})
|
|
|
|
if (entry.permission.status === 'approved') {
|
|
block.tool.state = 'completed'
|
|
block.tool.completedAt = entry.permission.completedAt ?? createdAt
|
|
if (block.tool.result === undefined) {
|
|
block.tool.result = 'Approved'
|
|
}
|
|
} else if (entry.permission.status === 'denied' || entry.permission.status === 'canceled') {
|
|
block.tool.state = 'error'
|
|
block.tool.completedAt = entry.permission.completedAt ?? createdAt
|
|
if (block.tool.result === undefined && entry.permission.reason) {
|
|
block.tool.result = { error: entry.permission.reason }
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate latest usage from messages (find the most recent message with usage data)
|
|
let latestUsage: LatestUsage | null = null
|
|
for (let i = normalized.length - 1; i >= 0; i--) {
|
|
const msg = normalized[i]
|
|
if (msg.usage && isUsageVisibleInParentContext(msg.usage)) {
|
|
latestUsage = {
|
|
inputTokens: msg.usage.input_tokens,
|
|
outputTokens: msg.usage.output_tokens,
|
|
cacheCreation: msg.usage.cache_creation_input_tokens ?? 0,
|
|
cacheRead: msg.usage.cache_read_input_tokens ?? 0,
|
|
contextSize: calculateContextSize(msg.usage),
|
|
contextWindow: msg.usage.context_window ?? null,
|
|
timestamp: msg.createdAt
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return {
|
|
blocks: dedupeAgentEvents(foldApiErrorEvents(rootResult.blocks)),
|
|
hasReadyEvent,
|
|
latestUsage,
|
|
latestGoal: getLatestThreadGoal(normalized)
|
|
}
|
|
}
|