feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
271
product-source/hololake-platform/src/lib/aiAgentSession.ts
Normal file
271
product-source/hololake-platform/src/lib/aiAgentSession.ts
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
|
||||
import {
|
||||
appendLocalResponse,
|
||||
appendLocalMarker,
|
||||
appendStreamingMessage,
|
||||
buildFormattedMessage,
|
||||
createMissingAgentResponse,
|
||||
type AgentStatus,
|
||||
type AgentExecutionContext,
|
||||
type AiAgentMessage,
|
||||
type PendingUserPrompt,
|
||||
} from './aiAgentConversation'
|
||||
import type { AgentFileCallbacks } from './aiAgentFileOperations'
|
||||
import { createStreamCallbacks } from './aiAgentStreamCallbacks'
|
||||
import type { ToolInvocation } from './aiAgentMessageState'
|
||||
import { trackAiAgentMessageBlocked, trackAiAgentMessageSent, trackAiAgentResponseStopped } from './productAnalytics'
|
||||
import { streamAiAgent } from '../utils/streamAiAgent'
|
||||
import { streamAiModel } from '../utils/streamAiModel'
|
||||
import { hydrateNoteReferences } from '../utils/ai-reference-content'
|
||||
import { createTranslator } from './i18n'
|
||||
|
||||
export interface AiAgentAbortState {
|
||||
aborted: boolean
|
||||
controller?: AbortController
|
||||
}
|
||||
|
||||
export interface AiAgentSessionRuntime {
|
||||
setMessages: Dispatch<SetStateAction<AiAgentMessage[]>>
|
||||
setStatus: Dispatch<SetStateAction<AgentStatus>>
|
||||
abortRef: MutableRefObject<AiAgentAbortState>
|
||||
responseAccRef: MutableRefObject<string>
|
||||
fileCallbacksRef: MutableRefObject<AgentFileCallbacks | undefined>
|
||||
toolInputMapRef: MutableRefObject<Map<string, ToolInvocation>>
|
||||
messagesRef: MutableRefObject<AiAgentMessage[]>
|
||||
statusRef: MutableRefObject<AgentStatus>
|
||||
}
|
||||
|
||||
interface SendAgentMessageOptions {
|
||||
runtime: AiAgentSessionRuntime
|
||||
context: AgentExecutionContext
|
||||
prompt: PendingUserPrompt
|
||||
}
|
||||
|
||||
interface RegenerateAgentMessageOptions {
|
||||
runtime: AiAgentSessionRuntime
|
||||
context: AgentExecutionContext
|
||||
messageId: string
|
||||
}
|
||||
|
||||
interface SelectedTargetStreamRequest {
|
||||
context: AgentExecutionContext
|
||||
formattedMessage: string
|
||||
systemPrompt: string
|
||||
callbacks: ReturnType<typeof createStreamCallbacks>
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
function normalizePrompt(prompt: PendingUserPrompt): PendingUserPrompt {
|
||||
return {
|
||||
text: prompt.text.trim(),
|
||||
references: prompt.references && prompt.references.length > 0 ? prompt.references : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function completedMessageCount(messages: AiAgentMessage[]): number {
|
||||
return messages.filter((message) => !message.isStreaming && !message.localMarker).length
|
||||
}
|
||||
|
||||
function shouldIgnorePrompt(status: AgentStatus, prompt: PendingUserPrompt): boolean {
|
||||
return !prompt.text || status === 'thinking' || status === 'tool-executing'
|
||||
}
|
||||
|
||||
function blockMissingVault(runtime: AiAgentSessionRuntime, context: AgentExecutionContext, prompt: PendingUserPrompt): void {
|
||||
trackAiAgentMessageBlocked(context.agent, 'missing_vault')
|
||||
appendLocalResponse(runtime.setMessages, prompt, 'No vault loaded. Open a vault first.')
|
||||
}
|
||||
|
||||
function blockUnavailableAgent(runtime: AiAgentSessionRuntime, context: AgentExecutionContext, prompt: PendingUserPrompt): void {
|
||||
trackAiAgentMessageBlocked(context.agent, 'agent_unavailable')
|
||||
appendLocalResponse(
|
||||
runtime.setMessages,
|
||||
prompt,
|
||||
createMissingAgentResponse(context.agent),
|
||||
)
|
||||
}
|
||||
|
||||
async function streamWithSelectedTarget({
|
||||
context,
|
||||
formattedMessage,
|
||||
systemPrompt,
|
||||
callbacks,
|
||||
signal,
|
||||
}: SelectedTargetStreamRequest): Promise<void> {
|
||||
if (context.target?.kind === 'api_model') {
|
||||
await streamAiModel({
|
||||
provider: context.target.provider,
|
||||
model: context.target.model,
|
||||
message: formattedMessage,
|
||||
systemPrompt,
|
||||
vaultPath: context.vaultPath,
|
||||
vaultPaths: context.vaultPaths,
|
||||
callbacks,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await streamAiAgent({
|
||||
agent: context.agent,
|
||||
message: formattedMessage,
|
||||
systemPrompt,
|
||||
vaultPath: context.vaultPath,
|
||||
vaultPaths: context.vaultPaths,
|
||||
permissionMode: context.permissionMode,
|
||||
callbacks,
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
function stoppedResponseText(response: string, locale: AgentExecutionContext['locale']): string {
|
||||
const stopped = createTranslator(locale ?? 'en')('ai.panel.stoppedResponse')
|
||||
const partial = response.trim()
|
||||
return partial ? `${partial}\n\n${stopped}` : stopped
|
||||
}
|
||||
|
||||
export async function sendAgentMessage({
|
||||
runtime,
|
||||
context,
|
||||
prompt,
|
||||
}: SendAgentMessageOptions): Promise<void> {
|
||||
const currentStatus = runtime.statusRef.current
|
||||
const normalizedPrompt = normalizePrompt(prompt)
|
||||
|
||||
if (shouldIgnorePrompt(currentStatus, normalizedPrompt)) return
|
||||
|
||||
if (!context.vaultPath) {
|
||||
blockMissingVault(runtime, context, normalizedPrompt)
|
||||
return
|
||||
}
|
||||
|
||||
if (!context.ready) {
|
||||
blockUnavailableAgent(runtime, context, normalizedPrompt)
|
||||
return
|
||||
}
|
||||
|
||||
trackAiAgentMessageSent({
|
||||
agent: context.agent,
|
||||
permissionMode: context.permissionMode,
|
||||
hasContext: !!context.systemPromptOverride,
|
||||
referenceCount: normalizedPrompt.references?.length ?? 0,
|
||||
historyMessageCount: completedMessageCount(runtime.messagesRef.current),
|
||||
})
|
||||
|
||||
const controller = new AbortController()
|
||||
const abortState: AiAgentAbortState = {
|
||||
aborted: false,
|
||||
controller,
|
||||
}
|
||||
runtime.abortRef.current = abortState
|
||||
runtime.responseAccRef.current = ''
|
||||
runtime.toolInputMapRef.current = new Map()
|
||||
|
||||
const messageId = appendStreamingMessage(runtime.setMessages, normalizedPrompt)
|
||||
runtime.setStatus('thinking')
|
||||
const promptForAgent = {
|
||||
...normalizedPrompt,
|
||||
references: await hydrateNoteReferences(normalizedPrompt.references),
|
||||
}
|
||||
|
||||
const { formattedMessage, systemPrompt } = buildFormattedMessage(
|
||||
context,
|
||||
runtime.messagesRef.current,
|
||||
promptForAgent,
|
||||
)
|
||||
|
||||
const callbacks = createStreamCallbacks({
|
||||
agent: context.agent,
|
||||
responseSourceLabel: context.target?.label,
|
||||
locale: context.locale,
|
||||
messageId,
|
||||
vaultPath: context.vaultPath,
|
||||
setMessages: runtime.setMessages,
|
||||
setStatus: runtime.setStatus,
|
||||
abortRef: { current: abortState },
|
||||
responseAccRef: runtime.responseAccRef,
|
||||
toolInputMapRef: runtime.toolInputMapRef,
|
||||
fileCallbacksRef: runtime.fileCallbacksRef,
|
||||
})
|
||||
|
||||
await streamWithSelectedTarget({
|
||||
context,
|
||||
formattedMessage,
|
||||
systemPrompt,
|
||||
callbacks,
|
||||
signal: controller.signal,
|
||||
})
|
||||
}
|
||||
|
||||
export async function regenerateAgentMessage({
|
||||
runtime,
|
||||
context,
|
||||
messageId,
|
||||
}: RegenerateAgentMessageOptions): Promise<void> {
|
||||
const currentMessages = runtime.messagesRef.current
|
||||
const messageIndex = currentMessages.findIndex((message) => message.id === messageId)
|
||||
const message = currentMessages[messageIndex]
|
||||
if (!message || message.localMarker || runtime.statusRef.current === 'thinking' || runtime.statusRef.current === 'tool-executing') return
|
||||
|
||||
const preservedMessages = currentMessages.slice(0, messageIndex)
|
||||
runtime.abortRef.current = { aborted: false }
|
||||
runtime.responseAccRef.current = ''
|
||||
runtime.toolInputMapRef.current = new Map()
|
||||
runtime.messagesRef.current = preservedMessages
|
||||
runtime.statusRef.current = 'idle'
|
||||
runtime.setMessages(preservedMessages)
|
||||
runtime.setStatus('idle')
|
||||
|
||||
await sendAgentMessage({
|
||||
runtime,
|
||||
context,
|
||||
prompt: {
|
||||
text: message.userMessage,
|
||||
references: message.references,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addAgentLocalMarker(
|
||||
runtime: Pick<AiAgentSessionRuntime, 'setMessages'>,
|
||||
text: string,
|
||||
): void {
|
||||
appendLocalMarker(runtime.setMessages, text)
|
||||
}
|
||||
|
||||
export function clearAgentConversation(runtime: Pick<AiAgentSessionRuntime, 'abortRef' | 'responseAccRef' | 'toolInputMapRef' | 'setMessages' | 'setStatus'>): void {
|
||||
runtime.abortRef.current.aborted = true
|
||||
runtime.abortRef.current.controller?.abort()
|
||||
runtime.responseAccRef.current = ''
|
||||
runtime.toolInputMapRef.current = new Map()
|
||||
runtime.setMessages([])
|
||||
runtime.setStatus('idle')
|
||||
}
|
||||
|
||||
export function stopAgentMessage(
|
||||
runtime: AiAgentSessionRuntime,
|
||||
context: Pick<AgentExecutionContext, 'agent' | 'locale'>,
|
||||
): void {
|
||||
if (!runtime.abortRef.current.controller || runtime.abortRef.current.aborted) return
|
||||
|
||||
runtime.abortRef.current.aborted = true
|
||||
runtime.abortRef.current.controller.abort()
|
||||
const response = runtime.responseAccRef.current
|
||||
const toolCount = runtime.toolInputMapRef.current.size
|
||||
trackAiAgentResponseStopped(context.agent, response, toolCount)
|
||||
|
||||
runtime.setMessages((current) => current.map((message) => (
|
||||
message.isStreaming
|
||||
? {
|
||||
...message,
|
||||
isStreaming: false,
|
||||
reasoningDone: true,
|
||||
response: stoppedResponseText(response, context.locale),
|
||||
actions: message.actions.map((action) => (
|
||||
action.status === 'pending' ? { ...action, status: 'error' as const } : action
|
||||
)),
|
||||
}
|
||||
: message
|
||||
)))
|
||||
runtime.responseAccRef.current = ''
|
||||
runtime.toolInputMapRef.current = new Map()
|
||||
runtime.setStatus('idle')
|
||||
}
|
||||
Loading…
Reference in a new issue