Download src/utils/hooks/sessionHooks.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/hooks/sessionHooks.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/hooks/sessionHooks.ts
-
curl -L -o sessionHooks.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/hooks/sessionHooks.ts
12.1 kB
| import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js' | |
| import type { AppState } from 'src/state/AppState.js' | |
| import type { Message } from 'src/types/message.js' | |
| import { logForDebugging } from '../debug.js' | |
| import type { AggregatedHookResult } from '../hooks.js' | |
| import type { HookCommand } from '../settings/types.js' | |
| import { isHookEqual } from './hooksSettings.js' | |
| type OnHookSuccess = ( | |
| hook: HookCommand | FunctionHook, | |
| result: AggregatedHookResult, | |
| ) => void | |
| /** Function hook callback - returns true if check passes, false to block */ | |
| export type FunctionHookCallback = ( | |
| messages: Message[], | |
| signal?: AbortSignal, | |
| ) => boolean | Promise<boolean> | |
| /** | |
| * Function hook type with callback embedded. | |
| * Session-scoped only, cannot be persisted to settings.json. | |
| */ | |
| export type FunctionHook = { | |
| type: 'function' | |
| id?: string // Optional unique ID for removal | |
| timeout?: number | |
| callback: FunctionHookCallback | |
| errorMessage: string | |
| statusMessage?: string | |
| } | |
| type SessionHookMatcher = { | |
| matcher: string | |
| skillRoot?: string | |
| hooks: Array<{ | |
| hook: HookCommand | FunctionHook | |
| onHookSuccess?: OnHookSuccess | |
| }> | |
| } | |
| export type SessionStore = { | |
| hooks: { | |
| [event in HookEvent]?: SessionHookMatcher[] | |
| } | |
| } | |
| /** | |
| * Map (not Record) so .set/.delete don't change the container's identity. | |
| * Mutator functions mutate the Map and return prev unchanged, letting | |
| * store.ts's Object.is(next, prev) check short-circuit and skip listener | |
| * notification. Session hooks are ephemeral per-agent runtime callbacks, | |
| * never reactively read (only getAppState() snapshots in the query loop). | |
| * Same pattern as agentControllers on LocalWorkflowTaskState. | |
| * | |
| * This matters under high-concurrency workflows: parallel() with N | |
| * schema-mode agents fires N addFunctionHook calls in one synchronous | |
| * tick. With a Record + spread, each call cost O(N) to copy the growing | |
| * map (O(N²) total) plus fired all ~30 store listeners. With Map: .set() | |
| * is O(1), return prev means zero listener fires. | |
| */ | |
| export type SessionHooksState = Map<string, SessionStore> | |
| /** | |
| * Add a command or prompt hook to the session. | |
| * Session hooks are temporary, in-memory only, and cleared when session ends. | |
| */ | |
| export function addSessionHook( | |
| setAppState: (updater: (prev: AppState) => AppState) => void, | |
| sessionId: string, | |
| event: HookEvent, | |
| matcher: string, | |
| hook: HookCommand, | |
| onHookSuccess?: OnHookSuccess, | |
| skillRoot?: string, | |
| ): void { | |
| addHookToSession( | |
| setAppState, | |
| sessionId, | |
| event, | |
| matcher, | |
| hook, | |
| onHookSuccess, | |
| skillRoot, | |
| ) | |
| } | |
| /** | |
| * Add a function hook to the session. | |
| * Function hooks execute TypeScript callbacks in-memory for validation. | |
| * @returns The hook ID (for removal) | |
| */ | |
| export function addFunctionHook( | |
| setAppState: (updater: (prev: AppState) => AppState) => void, | |
| sessionId: string, | |
| event: HookEvent, | |
| matcher: string, | |
| callback: FunctionHookCallback, | |
| errorMessage: string, | |
| options?: { | |
| timeout?: number | |
| id?: string | |
| }, | |
| ): string { | |
| const id = options?.id || `function-hook-${Date.now()}-${Math.random()}` | |
| const hook: FunctionHook = { | |
| type: 'function', | |
| id, | |
| timeout: options?.timeout || 5000, | |
| callback, | |
| errorMessage, | |
| } | |
| addHookToSession(setAppState, sessionId, event, matcher, hook) | |
| return id | |
| } | |
| /** | |
| * Remove a function hook by ID from the session. | |
| */ | |
| export function removeFunctionHook( | |
| setAppState: (updater: (prev: AppState) => AppState) => void, | |
| sessionId: string, | |
| event: HookEvent, | |
| hookId: string, | |
| ): void { | |
| setAppState(prev => { | |
| const store = prev.sessionHooks.get(sessionId) | |
| if (!store) { | |
| return prev | |
| } | |
| const eventMatchers = store.hooks[event] || [] | |
| // Remove the hook with matching ID from all matchers | |
| const updatedMatchers = eventMatchers | |
| .map(matcher => { | |
| const updatedHooks = matcher.hooks.filter(h => { | |
| if (h.hook.type !== 'function') return true | |
| return h.hook.id !== hookId | |
| }) | |
| return updatedHooks.length > 0 | |
| ? { ...matcher, hooks: updatedHooks } | |
| : null | |
| }) | |
| .filter((m): m is SessionHookMatcher => m !== null) | |
| const newHooks = | |
| updatedMatchers.length > 0 | |
| ? { ...store.hooks, [event]: updatedMatchers } | |
| : Object.fromEntries( | |
| Object.entries(store.hooks).filter(([e]) => e !== event), | |
| ) | |
| prev.sessionHooks.set(sessionId, { hooks: newHooks }) | |
| return prev | |
| }) | |
| logForDebugging( | |
| `Removed function hook ${hookId} for event ${event} in session ${sessionId}`, | |
| ) | |
| } | |
| /** | |
| * Internal helper to add a hook to session state | |
| */ | |
| function addHookToSession( | |
| setAppState: (updater: (prev: AppState) => AppState) => void, | |
| sessionId: string, | |
| event: HookEvent, | |
| matcher: string, | |
| hook: HookCommand | FunctionHook, | |
| onHookSuccess?: OnHookSuccess, | |
| skillRoot?: string, | |
| ): void { | |
| setAppState(prev => { | |
| const store = prev.sessionHooks.get(sessionId) ?? { hooks: {} } | |
| const eventMatchers = store.hooks[event] || [] | |
| // Find existing matcher or create new one | |
| const existingMatcherIndex = eventMatchers.findIndex( | |
| m => m.matcher === matcher && m.skillRoot === skillRoot, | |
| ) | |
| let updatedMatchers: SessionHookMatcher[] | |
| if (existingMatcherIndex >= 0) { | |
| // Add to existing matcher | |
| updatedMatchers = [...eventMatchers] | |
| const existingMatcher = updatedMatchers[existingMatcherIndex]! | |
| updatedMatchers[existingMatcherIndex] = { | |
| matcher: existingMatcher.matcher, | |
| skillRoot: existingMatcher.skillRoot, | |
| hooks: [...existingMatcher.hooks, { hook, onHookSuccess }], | |
| } | |
| } else { | |
| // Create new matcher | |
| updatedMatchers = [ | |
| ...eventMatchers, | |
| { | |
| matcher, | |
| skillRoot, | |
| hooks: [{ hook, onHookSuccess }], | |
| }, | |
| ] | |
| } | |
| const newHooks = { ...store.hooks, [event]: updatedMatchers } | |
| prev.sessionHooks.set(sessionId, { hooks: newHooks }) | |
| return prev | |
| }) | |
| logForDebugging( | |
| `Added session hook for event ${event} in session ${sessionId}`, | |
| ) | |
| } | |
| /** | |
| * Remove a specific hook from the session | |
| * @param setAppState The function to update the app state | |
| * @param sessionId The session ID | |
| * @param event The hook event | |
| * @param hook The hook command to remove | |
| */ | |
| export function removeSessionHook( | |
| setAppState: (updater: (prev: AppState) => AppState) => void, | |
| sessionId: string, | |
| event: HookEvent, | |
| hook: HookCommand, | |
| ): void { | |
| setAppState(prev => { | |
| const store = prev.sessionHooks.get(sessionId) | |
| if (!store) { | |
| return prev | |
| } | |
| const eventMatchers = store.hooks[event] || [] | |
| // Remove the hook from all matchers | |
| const updatedMatchers = eventMatchers | |
| .map(matcher => { | |
| const updatedHooks = matcher.hooks.filter( | |
| h => !isHookEqual(h.hook, hook), | |
| ) | |
| return updatedHooks.length > 0 | |
| ? { ...matcher, hooks: updatedHooks } | |
| : null | |
| }) | |
| .filter((m): m is SessionHookMatcher => m !== null) | |
| const newHooks = | |
| updatedMatchers.length > 0 | |
| ? { ...store.hooks, [event]: updatedMatchers } | |
| : { ...store.hooks } | |
| if (updatedMatchers.length === 0) { | |
| delete newHooks[event] | |
| } | |
| prev.sessionHooks.set(sessionId, { ...store, hooks: newHooks }) | |
| return prev | |
| }) | |
| logForDebugging( | |
| `Removed session hook for event ${event} in session ${sessionId}`, | |
| ) | |
| } | |
| // Extended hook matcher that includes optional skillRoot for skill-scoped hooks | |
| export type SessionDerivedHookMatcher = { | |
| matcher: string | |
| hooks: HookCommand[] | |
| skillRoot?: string | |
| } | |
| /** | |
| * Convert session hook matchers to regular hook matchers | |
| * @param sessionMatchers The session hook matchers to convert | |
| * @returns Regular hook matchers (with optional skillRoot preserved) | |
| */ | |
| function convertToHookMatchers( | |
| sessionMatchers: SessionHookMatcher[], | |
| ): SessionDerivedHookMatcher[] { | |
| return sessionMatchers.map(sm => ({ | |
| matcher: sm.matcher, | |
| skillRoot: sm.skillRoot, | |
| // Filter out function hooks - they can't be persisted to HookMatcher format | |
| hooks: sm.hooks | |
| .map(h => h.hook) | |
| .filter((h): h is HookCommand => h.type !== 'function'), | |
| })) | |
| } | |
| /** | |
| * Get all session hooks for a specific event (excluding function hooks) | |
| * @param appState The app state | |
| * @param sessionId The session ID | |
| * @param event Optional event to filter by | |
| * @returns Hook matchers for the event, or all hooks if no event specified | |
| */ | |
| export function getSessionHooks( | |
| appState: AppState, | |
| sessionId: string, | |
| event?: HookEvent, | |
| ): Map<HookEvent, SessionDerivedHookMatcher[]> { | |
| const store = appState.sessionHooks.get(sessionId) | |
| if (!store) { | |
| return new Map() | |
| } | |
| const result = new Map<HookEvent, SessionDerivedHookMatcher[]>() | |
| if (event) { | |
| const sessionMatchers = store.hooks[event] | |
| if (sessionMatchers) { | |
| result.set(event, convertToHookMatchers(sessionMatchers)) | |
| } | |
| return result | |
| } | |
| for (const evt of HOOK_EVENTS) { | |
| const sessionMatchers = store.hooks[evt] | |
| if (sessionMatchers) { | |
| result.set(evt, convertToHookMatchers(sessionMatchers)) | |
| } | |
| } | |
| return result | |
| } | |
| type FunctionHookMatcher = { | |
| matcher: string | |
| hooks: FunctionHook[] | |
| } | |
| /** | |
| * Get all session function hooks for a specific event | |
| * Function hooks are kept separate because they can't be persisted to HookMatcher format. | |
| * @param appState The app state | |
| * @param sessionId The session ID | |
| * @param event Optional event to filter by | |
| * @returns Function hook matchers for the event | |
| */ | |
| export function getSessionFunctionHooks( | |
| appState: AppState, | |
| sessionId: string, | |
| event?: HookEvent, | |
| ): Map<HookEvent, FunctionHookMatcher[]> { | |
| const store = appState.sessionHooks.get(sessionId) | |
| if (!store) { | |
| return new Map() | |
| } | |
| const result = new Map<HookEvent, FunctionHookMatcher[]>() | |
| const extractFunctionHooks = ( | |
| sessionMatchers: SessionHookMatcher[], | |
| ): FunctionHookMatcher[] => { | |
| return sessionMatchers | |
| .map(sm => ({ | |
| matcher: sm.matcher, | |
| hooks: sm.hooks | |
| .map(h => h.hook) | |
| .filter((h): h is FunctionHook => h.type === 'function'), | |
| })) | |
| .filter(m => m.hooks.length > 0) | |
| } | |
| if (event) { | |
| const sessionMatchers = store.hooks[event] | |
| if (sessionMatchers) { | |
| const functionMatchers = extractFunctionHooks(sessionMatchers) | |
| if (functionMatchers.length > 0) { | |
| result.set(event, functionMatchers) | |
| } | |
| } | |
| return result | |
| } | |
| for (const evt of HOOK_EVENTS) { | |
| const sessionMatchers = store.hooks[evt] | |
| if (sessionMatchers) { | |
| const functionMatchers = extractFunctionHooks(sessionMatchers) | |
| if (functionMatchers.length > 0) { | |
| result.set(evt, functionMatchers) | |
| } | |
| } | |
| } | |
| return result | |
| } | |
| /** | |
| * Get the full hook entry (including callbacks) for a specific session hook | |
| */ | |
| export function getSessionHookCallback( | |
| appState: AppState, | |
| sessionId: string, | |
| event: HookEvent, | |
| matcher: string, | |
| hook: HookCommand | FunctionHook, | |
| ): | |
| | { | |
| hook: HookCommand | FunctionHook | |
| onHookSuccess?: OnHookSuccess | |
| } | |
| | undefined { | |
| const store = appState.sessionHooks.get(sessionId) | |
| if (!store) { | |
| return undefined | |
| } | |
| const eventMatchers = store.hooks[event] | |
| if (!eventMatchers) { | |
| return undefined | |
| } | |
| // Find the hook in the matchers | |
| for (const matcherEntry of eventMatchers) { | |
| if (matcherEntry.matcher === matcher || matcher === '') { | |
| const hookEntry = matcherEntry.hooks.find(h => isHookEqual(h.hook, hook)) | |
| if (hookEntry) { | |
| return hookEntry | |
| } | |
| } | |
| } | |
| return undefined | |
| } | |
| /** | |
| * Clear all session hooks for a specific session | |
| * @param setAppState The function to update the app state | |
| * @param sessionId The session ID | |
| */ | |
| export function clearSessionHooks( | |
| setAppState: (updater: (prev: AppState) => AppState) => void, | |
| sessionId: string, | |
| ): void { | |
| setAppState(prev => { | |
| prev.sessionHooks.delete(sessionId) | |
| return prev | |
| }) | |
| logForDebugging(`Cleared all session hooks for session ${sessionId}`) | |
| } | |