mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-21 12:55:09 +00:00
- Add tool approval system with per-category approval configuration - Build unified Governance dashboard (renamed from Admin) with pending approvals view and execution audit log - Move execution history tracking into the app shell - Extract buildChatRequestBody helper and add newtab system prompt - Add approval config change detection for mid-conversation rebuilds
145 lines
3.9 KiB
TypeScript
145 lines
3.9 KiB
TypeScript
import type { UIMessage } from 'ai'
|
|
|
|
export type ToolInvocationState =
|
|
| 'partial-call'
|
|
| 'call'
|
|
| 'result'
|
|
| 'input-streaming'
|
|
| 'input-available'
|
|
| 'output-available'
|
|
| 'output-error'
|
|
| 'approval-requested'
|
|
| 'approval-responded'
|
|
| 'output-denied'
|
|
|
|
export interface ToolInvocationInfo {
|
|
state: ToolInvocationState
|
|
toolCallId: string
|
|
toolName: string
|
|
input: Record<string, unknown>
|
|
output: unknown[]
|
|
approval?: { id: string; approved?: boolean; reason?: string }
|
|
}
|
|
|
|
export type NudgeType = 'schedule_suggestion' | 'app_connection'
|
|
|
|
export interface NudgeData {
|
|
type: NudgeType
|
|
[key: string]: unknown
|
|
}
|
|
|
|
export type MessageSegment =
|
|
| { type: 'text'; key: string; text: string }
|
|
| { type: 'reasoning'; key: string; text: string; isStreaming: boolean }
|
|
| { type: 'tool-batch'; key: string; tools: ToolInvocationInfo[] }
|
|
| { type: 'nudge'; key: string; nudgeType: NudgeType; data: NudgeData }
|
|
|
|
const NUDGE_TOOLS = new Set(['suggest_schedule', 'suggest_app_connection'])
|
|
|
|
function parseNudgeOutput(output: unknown): NudgeData | null {
|
|
try {
|
|
// output is { content: [{ type: "text", text: "JSON..." }], isError: false }
|
|
const result = output as {
|
|
content?: Array<{ type: string; text?: string }>
|
|
isError?: boolean
|
|
}
|
|
if (result?.isError) return null
|
|
|
|
const text = result?.content?.find((c) => c.type === 'text')?.text
|
|
if (!text) return null
|
|
|
|
const parsed = JSON.parse(text)
|
|
if (
|
|
parsed?.type === 'schedule_suggestion' ||
|
|
parsed?.type === 'app_connection'
|
|
) {
|
|
return parsed as NudgeData
|
|
}
|
|
} catch {
|
|
// ignore parse errors
|
|
}
|
|
return null
|
|
}
|
|
|
|
export const getMessageSegments = (
|
|
message: UIMessage,
|
|
isLastMessage: boolean,
|
|
isStreaming: boolean,
|
|
): MessageSegment[] => {
|
|
const segments: MessageSegment[] = []
|
|
let currentToolBatch: ToolInvocationInfo[] = []
|
|
let textSegmentCount = 0
|
|
let reasoningSegmentCount = 0
|
|
|
|
const flushToolBatch = () => {
|
|
if (currentToolBatch.length > 0) {
|
|
segments.push({
|
|
type: 'tool-batch',
|
|
key: `${message.id}-tools-${currentToolBatch[0].toolCallId}`,
|
|
tools: [...currentToolBatch],
|
|
})
|
|
currentToolBatch = []
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < message.parts.length; i++) {
|
|
const part = message.parts[i]
|
|
|
|
if (part.type === 'text') {
|
|
flushToolBatch()
|
|
segments.push({
|
|
type: 'text',
|
|
key: `${message.id}-text-${textSegmentCount}`,
|
|
text: part.text,
|
|
})
|
|
textSegmentCount++
|
|
} else if (part.type === 'reasoning') {
|
|
flushToolBatch()
|
|
segments.push({
|
|
type: 'reasoning',
|
|
key: `${message.id}-reasoning-${reasoningSegmentCount}`,
|
|
text: part.text,
|
|
isStreaming:
|
|
isStreaming && i === message.parts.length - 1 && isLastMessage,
|
|
})
|
|
reasoningSegmentCount++
|
|
} else if (part.type?.startsWith('tool-')) {
|
|
const toolPart = part as {
|
|
toolCallId: string
|
|
type: string
|
|
state: ToolInvocationState
|
|
input: Record<string, unknown>
|
|
output: unknown
|
|
approval?: { id: string; approved?: boolean; reason?: string }
|
|
}
|
|
const toolName = toolPart.type?.replace('tool-', '')
|
|
|
|
if (NUDGE_TOOLS.has(toolName) && toolPart.state === 'output-available') {
|
|
flushToolBatch()
|
|
const nudgeData = parseNudgeOutput(toolPart.output)
|
|
if (nudgeData) {
|
|
segments.push({
|
|
type: 'nudge',
|
|
key: `${message.id}-nudge-${toolPart.toolCallId}`,
|
|
nudgeType: nudgeData.type,
|
|
data: nudgeData,
|
|
})
|
|
}
|
|
} else if (!NUDGE_TOOLS.has(toolName)) {
|
|
currentToolBatch.push({
|
|
state: toolPart.state,
|
|
toolCallId: toolPart.toolCallId,
|
|
toolName,
|
|
input: toolPart?.input ?? {},
|
|
output: (toolPart?.output as unknown[]) ?? [],
|
|
approval: toolPart?.approval,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
flushToolBatch()
|
|
|
|
return segments
|
|
}
|