mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +00:00
Port web/src/chat/** file-for-file into HapiProtocol/Chat/: - ChatTypes: block/tool/permission/usage models; AgentEvent as a record-backed struct (verbatim wire projection) with a typed Kind view and exact-key-set constructors for every synthesized event shape - Normalize/NormalizeUser/NormalizeAgent: the full decode tree (codex / output incl. agy / event families, skip rules, stringify-never-drop) - Tracer: sidechain grouping via parentToolUseId with prompt-match and parentUuid-chain fallbacks (orphan reparenting included) - ReducerEvents (pipe-format limit parsing, dedupe, api-error folding), ReducerTools (BlockBox shared-mutation model, JS-spread-faithful permission merging via presentKeys), ReducerCliOutput, ReducerTimeline (tool pairing, stream coalescing, agent-run cards, title-changed synthesis, sentinel suppression), Reducer (pending-permission synthesis gates, latestUsage, goal filtering) - ToolGroups + codex-exploration family (codexCommandPresentation's grouping predicates); ToolPresentation (minimal title/subtitle, full knownTools catalog deferred to HapiUI) - JSInterop: JS semantics pinned once (nullish coalescing over wire values, truthiness, safeStringify, canonical JSON serializer with integer-preserving number formatting, JS-anchored regex helpers) - FixtureProjection: the normative projection (projection.ts) plus runChatFixturePipeline - ChatFixtureTests: one swift-testing case per shared/fixtures/chat/*.json (48), canonical-JSON byte comparison, per-fixture line-level diff on mismatch, VERSION gate Dictionary-order note: wherever the TS relies on JS object insertion order (agentState requests iteration), the port imposes ascending key order — equivalent to the reference replay because fixture inputs are canonically key-sorted on disk and JSON.parse preserves that order.
130 lines
4.6 KiB
Swift
130 lines
4.6 KiB
Swift
import Foundation
|
|
|
|
// Port of web/src/chat/tracer.ts — groups sidechain messages under their
|
|
// parent Agent/Task tool call. `TracedMessage` is `NormalizedMessage` with
|
|
// `sidechainId` populated (the field lives on the struct).
|
|
|
|
/// Port of `isSubagentToolName` (web/src/chat/subagentTool.ts).
|
|
public func isSubagentToolName(_ name: String) -> Bool {
|
|
name == "Task" || name == "Agent" || name.hasPrefix("Agent:") || name.hasPrefix("Task:")
|
|
}
|
|
|
|
private struct TracerState {
|
|
var promptToTaskId: [String: String] = [:]
|
|
var toolUseIdToTaskId: [String: String] = [:]
|
|
var uuidToSidechainId: [String: String] = [:]
|
|
var orphanMessages: [String: [NormalizedMessage]] = [:]
|
|
}
|
|
|
|
private func getMessageUuid(_ message: NormalizedMessage) -> String? {
|
|
guard let content = message.agentContent, let first = content.first else { return nil }
|
|
return first.uuid
|
|
}
|
|
|
|
private func getParentUuid(_ message: NormalizedMessage) -> String? {
|
|
guard let content = message.agentContent, let first = content.first else { return nil }
|
|
return first.parentUUID
|
|
}
|
|
|
|
private func getParentToolUseId(_ message: NormalizedMessage) -> String? {
|
|
guard message.agentContent != nil else { return nil }
|
|
return message.parentToolUseId
|
|
}
|
|
|
|
private func processOrphans(_ state: inout TracerState, parentUuid: String, sidechainId: String) -> [NormalizedMessage] {
|
|
var results: [NormalizedMessage] = []
|
|
guard let orphans = state.orphanMessages[parentUuid] else { return results }
|
|
state.orphanMessages.removeValue(forKey: parentUuid)
|
|
|
|
for orphan in orphans {
|
|
let uuid = getMessageUuid(orphan)
|
|
if let uuid {
|
|
state.uuidToSidechainId[uuid] = sidechainId
|
|
}
|
|
|
|
var traced = orphan
|
|
traced.sidechainId = sidechainId
|
|
results.append(traced)
|
|
|
|
if let uuid {
|
|
results.append(contentsOf: processOrphans(&state, parentUuid: uuid, sidechainId: sidechainId))
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
/// Port of `traceMessages`.
|
|
public func traceMessages(_ messages: [NormalizedMessage]) -> [NormalizedMessage] {
|
|
var state = TracerState()
|
|
var results: [NormalizedMessage] = []
|
|
|
|
// Index Task/Agent prompts and tool_use ids (sidechain ones included).
|
|
for message in messages {
|
|
guard let content = message.agentContent else { continue }
|
|
for item in content {
|
|
guard case .toolCall(let toolCall) = item, isSubagentToolName(toolCall.name) else { continue }
|
|
state.toolUseIdToTaskId[toolCall.id] = message.id
|
|
guard let prompt = toolCall.input?.objectValue?["prompt"]?.stringValue else { continue }
|
|
state.promptToTaskId[prompt] = message.id
|
|
}
|
|
}
|
|
|
|
for message in messages {
|
|
if !message.isSidechain {
|
|
results.append(message)
|
|
continue
|
|
}
|
|
|
|
let uuid = getMessageUuid(message)
|
|
let parentUuid = getParentUuid(message)
|
|
|
|
// Preferred: group by the SDK's parent_tool_use_id.
|
|
var sidechainId: String?
|
|
if let parentToolUseId = getParentToolUseId(message) {
|
|
sidechainId = state.toolUseIdToTaskId[parentToolUseId]
|
|
}
|
|
|
|
// Fallback: sidechain-root prompt matching (pre-parentToolUseId data).
|
|
if sidechainId == nil, let content = message.agentContent {
|
|
for item in content {
|
|
guard case .sidechain(let sidechain) = item else { continue }
|
|
if let taskId = state.promptToTaskId[sidechain.prompt] {
|
|
sidechainId = taskId
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if let sidechainId, let uuid {
|
|
state.uuidToSidechainId[uuid] = sidechainId
|
|
var traced = message
|
|
traced.sidechainId = sidechainId
|
|
results.append(traced)
|
|
results.append(contentsOf: processOrphans(&state, parentUuid: uuid, sidechainId: sidechainId))
|
|
continue
|
|
}
|
|
|
|
if let parentUuid {
|
|
if let parentSidechainId = state.uuidToSidechainId[parentUuid] {
|
|
if let uuid {
|
|
state.uuidToSidechainId[uuid] = parentSidechainId
|
|
}
|
|
var traced = message
|
|
traced.sidechainId = parentSidechainId
|
|
results.append(traced)
|
|
if let uuid {
|
|
results.append(contentsOf: processOrphans(&state, parentUuid: uuid, sidechainId: parentSidechainId))
|
|
}
|
|
} else {
|
|
state.orphanMessages[parentUuid, default: []].append(message)
|
|
}
|
|
continue
|
|
}
|
|
|
|
results.append(message)
|
|
}
|
|
|
|
return results
|
|
}
|