@@ -4426,17 +4426,17 @@ impl Session {
44264426 /// without requiring explicit registration on each sub-agent session.
44274427 ///
44284428 /// @param hookId - Unique hook identifier
4429- /// @param eventType - Event type: "pre_tool_use", "post_tool_use", "generate_start",
4430- /// "generate_end", "session_start", "session_end", "skill_load", "skill_unload",
4431- /// "pre_prompt", "post_response", "on_error"
4429+ /// @param eventType - Event type such as "pre_tool_use", "post_tool_use",
4430+ /// "pre_prompt", "post_response", "pre_planning", or "post_planning".
44324431 /// @param matcher - Optional matcher: { tool?, pathPattern?, commandPattern?, sessionId?, skill? }
44334432 /// @param config - Optional config: { priority?, timeoutMs?, asyncExecution?, maxRetries? }
4434- /// @param handler - Optional callback `(event: any) => { action: 'continue' | 'block' | 'skip',
4435- /// reason?: string } | null`. When provided, the function is called for every matching event
4436- /// and its return value controls execution. Return `{ action: 'block', reason: '...' }` to
4437- /// cancel the operation, `{ action: 'skip' }` to skip remaining hooks, or `null`/`undefined`
4438- /// for continue. Hooks with no handler still fire (observable via stream events) but always
4439- /// continue.
4433+ /// @param handler - Optional callback `(event: any) => { action: 'continue' | 'block' | 'skip' | 'retry',
4434+ /// reason?: string, delayMs?: number, modified?: any } | null`. When provided, the function is
4435+ /// called for every matching event and its return value controls execution. Return
4436+ /// `{ action: 'block', reason: '...' }` to cancel the operation, `{ action: 'skip' }` to skip
4437+ /// remaining hooks, `{ action: 'retry', delayMs: 1000 }` to request a retry, or
4438+ /// `{ action: 'continue', modified: {...} }` to continue with modified data. Hooks with no
4439+ /// handler still fire (observable via stream events) but always continue.
44404440 #[ napi]
44414441 pub fn register_hook (
44424442 & self ,
@@ -4445,7 +4445,7 @@ impl Session {
44454445 matcher : Option < HookMatcherObject > ,
44464446 config : Option < HookConfigObject > ,
44474447 #[ napi(
4448- ts_arg_type = "((event: Record<string, unknown>) => { action: string; reason?: string } | null | undefined) | null | undefined"
4448+ ts_arg_type = "((event: Record<string, unknown>) => { action: string; reason?: string; delayMs?: number; modified?: any } | null | undefined) | null | undefined"
44494449 ) ]
44504450 handler : Option < napi:: JsFunction > ,
44514451 ) -> napi:: Result < ( ) > {
@@ -5465,10 +5465,25 @@ fn parse_hook_event_type(event_type: &str) -> napi::Result<RustHookEventType> {
54655465 "pre_prompt" => Ok ( RustHookEventType :: PrePrompt ) ,
54665466 "post_response" => Ok ( RustHookEventType :: PostResponse ) ,
54675467 "on_error" => Ok ( RustHookEventType :: OnError ) ,
5468+ "pre_context_perception" => Ok ( RustHookEventType :: PreContextPerception ) ,
5469+ "post_context_perception" => Ok ( RustHookEventType :: PostContextPerception ) ,
5470+ "on_success" => Ok ( RustHookEventType :: OnSuccess ) ,
5471+ "pre_memory_recall" => Ok ( RustHookEventType :: PreMemoryRecall ) ,
5472+ "post_memory_recall" => Ok ( RustHookEventType :: PostMemoryRecall ) ,
5473+ "pre_planning" => Ok ( RustHookEventType :: PrePlanning ) ,
5474+ "post_planning" => Ok ( RustHookEventType :: PostPlanning ) ,
5475+ "pre_reasoning" => Ok ( RustHookEventType :: PreReasoning ) ,
5476+ "post_reasoning" => Ok ( RustHookEventType :: PostReasoning ) ,
5477+ "on_rate_limit" => Ok ( RustHookEventType :: OnRateLimit ) ,
5478+ "on_confirmation" => Ok ( RustHookEventType :: OnConfirmation ) ,
5479+ "intent_detection" => Ok ( RustHookEventType :: IntentDetection ) ,
54685480 _ => Err ( napi:: Error :: from_reason ( format ! (
54695481 "Invalid hook event type: '{}'. Expected one of: pre_tool_use, post_tool_use, \
54705482 generate_start, generate_end, session_start, session_end, skill_load, \
5471- skill_unload, pre_prompt, post_response, on_error",
5483+ skill_unload, pre_prompt, post_response, on_error, pre_context_perception, \
5484+ post_context_perception, on_success, pre_memory_recall, post_memory_recall, \
5485+ pre_planning, post_planning, pre_reasoning, post_reasoning, on_rate_limit, \
5486+ on_confirmation, intent_detection",
54725487 event_type
54735488 ) ) ) ,
54745489 }
@@ -5526,6 +5541,7 @@ impl RustHookHandler for NodeCallbackHandler {
55265541/// - `{ action: 'block', reason: '…' }` → block
55275542/// - `{ action: 'skip' }` → skip
55285543/// - `{ action: 'retry', delayMs: N }` → retry after N ms
5544+ /// - `{ action: 'continue', modified: {...} }` → continue with modified data
55295545fn parse_js_hook_response ( val : napi:: JsUnknown ) -> napi:: Result < RustHookResponse > {
55305546 use napi:: { JsObject , ValueType } ;
55315547
@@ -5558,14 +5574,68 @@ fn parse_js_hook_response(val: napi::JsUnknown) -> napi::Result<RustHookResponse
55585574 . unwrap_or ( 1000 ) as u64 ;
55595575 Ok ( RustHookResponse :: retry ( delay_ms) )
55605576 }
5561- // "continue" or any other value → continue
5562- _ => Ok ( RustHookResponse :: continue_ ( ) ) ,
5577+ // "continue" or any other value → continue, optionally with modified data.
5578+ _ => {
5579+ if let Ok ( modified) = obj. get_named_property :: < napi:: JsUnknown > ( "modified" ) {
5580+ if !matches ! ( modified. get_type( ) ?, ValueType :: Null | ValueType :: Undefined ) {
5581+ let modified = js_unknown_to_json ( modified) ?;
5582+ return Ok ( RustHookResponse :: continue_with ( modified) ) ;
5583+ }
5584+ }
5585+ Ok ( RustHookResponse :: continue_ ( ) )
5586+ }
55635587 }
55645588 }
55655589 _ => Ok ( RustHookResponse :: continue_ ( ) ) ,
55665590 }
55675591}
55685592
5593+ fn js_unknown_to_json ( value : napi:: JsUnknown ) -> napi:: Result < serde_json:: Value > {
5594+ use napi:: { JsBoolean , JsNumber , JsObject , JsString , ValueType } ;
5595+
5596+ match value. get_type ( ) ? {
5597+ ValueType :: Null | ValueType :: Undefined => Ok ( serde_json:: Value :: Null ) ,
5598+ ValueType :: Boolean => {
5599+ let value = unsafe { value. cast :: < JsBoolean > ( ) } ;
5600+ Ok ( serde_json:: Value :: Bool ( value. get_value ( ) ?) )
5601+ }
5602+ ValueType :: Number => {
5603+ let value = unsafe { value. cast :: < JsNumber > ( ) } ;
5604+ let number = serde_json:: Number :: from_f64 ( value. get_double ( ) ?)
5605+ . unwrap_or_else ( || serde_json:: Number :: from ( 0 ) ) ;
5606+ Ok ( serde_json:: Value :: Number ( number) )
5607+ }
5608+ ValueType :: String => {
5609+ let value = unsafe { value. cast :: < JsString > ( ) } ;
5610+ Ok ( serde_json:: Value :: String ( value. into_utf8 ( ) ?. into_owned ( ) ?) )
5611+ }
5612+ ValueType :: Object => {
5613+ let object = unsafe { value. cast :: < JsObject > ( ) } ;
5614+ if object. is_array ( ) ? {
5615+ let mut values = Vec :: new ( ) ;
5616+ for index in 0 ..object. get_array_length ( ) ? {
5617+ let item = object. get_element :: < napi:: JsUnknown > ( index) ?;
5618+ values. push ( js_unknown_to_json ( item) ?) ;
5619+ }
5620+ return Ok ( serde_json:: Value :: Array ( values) ) ;
5621+ }
5622+
5623+ let names = object. get_property_names ( ) ?;
5624+ let mut map = serde_json:: Map :: new ( ) ;
5625+ for index in 0 ..names. get_array_length ( ) ? {
5626+ let key = names
5627+ . get_element :: < JsString > ( index) ?
5628+ . into_utf8 ( ) ?
5629+ . into_owned ( ) ?;
5630+ let item = object. get_named_property :: < napi:: JsUnknown > ( & key) ?;
5631+ map. insert ( key, js_unknown_to_json ( item) ?) ;
5632+ }
5633+ Ok ( serde_json:: Value :: Object ( map) )
5634+ }
5635+ _ => Ok ( serde_json:: Value :: Null ) ,
5636+ }
5637+ }
5638+
55695639// ============================================================================
55705640// SkillInfo
55715641// ============================================================================
@@ -6043,6 +6113,30 @@ mod tests {
60436113 assert ! ( matches!( opts. planning_mode, RustPlanningMode :: Enabled ) ) ;
60446114 }
60456115
6116+ #[ test]
6117+ fn hook_event_type_parser_accepts_harness_control_points ( ) {
6118+ assert ! ( matches!(
6119+ parse_hook_event_type( "pre_planning" ) . unwrap( ) ,
6120+ RustHookEventType :: PrePlanning
6121+ ) ) ;
6122+ assert ! ( matches!(
6123+ parse_hook_event_type( "post_planning" ) . unwrap( ) ,
6124+ RustHookEventType :: PostPlanning
6125+ ) ) ;
6126+ assert ! ( matches!(
6127+ parse_hook_event_type( "pre_memory_recall" ) . unwrap( ) ,
6128+ RustHookEventType :: PreMemoryRecall
6129+ ) ) ;
6130+ assert ! ( matches!(
6131+ parse_hook_event_type( "intent_detection" ) . unwrap( ) ,
6132+ RustHookEventType :: IntentDetection
6133+ ) ) ;
6134+
6135+ let error = parse_hook_event_type ( "planning" ) . unwrap_err ( ) . to_string ( ) ;
6136+ assert ! ( error. contains( "pre_planning" ) ) ;
6137+ assert ! ( error. contains( "post_planning" ) ) ;
6138+ }
6139+
60466140 #[ test]
60476141 fn session_options_maps_parallel_delegation_controls ( ) {
60486142 let opts = js_session_options_to_rust ( Some ( SessionOptions {
0 commit comments