@@ -30,6 +30,7 @@ import {
3030 applyManageTasks ,
3131 hasActiveTasks ,
3232 parseManageTasksArgs ,
33+ TaskSchema ,
3334 type Task ,
3435} from "./tasks.js" ;
3536import { createCorbitsRetryPolicy } from "./retry-policy.js" ;
@@ -412,7 +413,7 @@ function isCodeFile(path: string): boolean {
412413// Returns null when the call is not manage_tasks or its arguments don't
413414// parse, so callers can distinguish "no valid manage_tasks call here" from
414415// "a valid call that happened to be a no-op" — the latter still counts as an
415- // update for onTasksChange purposes.
416+ // update for tasks-changed event purposes.
416417function applyManageTasksToolCall (
417418 tasks : Task [ ] ,
418419 block : { name : string ; arguments : unknown } ,
@@ -422,15 +423,27 @@ function applyManageTasksToolCall(
422423 return taskArgs !== null ? applyManageTasks ( tasks , taskArgs ) : null ;
423424}
424425
426+ // Reactor events the chat director emits in place of host closures. Hosts
427+ // (TUI, exec) subscribe on the agent stream: task-list changes replace the
428+ // former onTasksChange callback, tool activation replaces onActivateTools.
429+ // Neither namespace collides with the reactor's reserved prefixes
430+ // (inference., tool., reactor., fork.).
431+ export const CHAT_TASKS_CHANGED_EVENT = "custom.chat.tasks.changed" ;
432+ export const CHAT_TOOLS_ACTIVATE_EVENT = "custom.chat.tools.activate" ;
433+ export const ChatTasksChangedDataSchema = type ( {
434+ tasks : TaskSchema . array ( ) ,
435+ } ) ;
436+ export const ChatToolsActivateDataSchema = type ( {
437+ names : "string[]" ,
438+ } ) ;
439+
425440export interface ChatDirectorOptions {
426441 taskClassifier ?:
427442 | ( ( message : string , metadata : SessionMetadata ) => Promise < TaskBoundary > )
428443 | undefined ;
429- onActivateTools ?: ( ( names : string [ ] ) => void ) | undefined ;
430444 inactivityTimeoutMs ?: number | undefined ;
431445 totalTimeoutMs ?: number | undefined ;
432446 workflowCoordinator ?: WorkflowCoordinator | undefined ;
433- onTasksChange : ( tasks : Task [ ] ) => void ;
434447 requestContinuation ?: ( ( ) => void ) | undefined ;
435448 provider ?: { providerName : string ; model ?: string } | undefined ;
436449 /**
@@ -464,7 +477,6 @@ class ChatDirectorImpl extends DefaultDirector {
464477 > ( ) ;
465478 private readonly lspTriggerCalls = new Set < string > ( ) ;
466479 private readonly askOperatorCalls = new Set < string > ( ) ;
467- private readonly onActivateTools : ( ( names : string [ ] ) => void ) | undefined ;
468480 private readonly taskClassifier :
469481 | ( ( message : string , metadata : SessionMetadata ) => Promise < TaskBoundary > )
470482 | undefined ;
@@ -481,7 +493,6 @@ class ChatDirectorImpl extends DefaultDirector {
481493 private lastInferenceTurnHadContent = false ;
482494 private operatorJustResponded = false ;
483495 private tasks : Task [ ] = [ ] ;
484- private readonly onTasksChange : ( ( tasks : Task [ ] ) => void ) | undefined ;
485496 private turnCount = 0 ;
486497 private currentTaskLabel : string | undefined ;
487498 private lastTaskSummary : string | undefined ;
@@ -500,6 +511,11 @@ class ChatDirectorImpl extends DefaultDirector {
500511 private toolOnlyStreak = 0 ;
501512 private toolOnlyNudgeFired = false ;
502513 private pendingToolOnlyNudge = false ;
514+ // Reactor events queued while scanning the current inbound event. Drained
515+ // in decide() and appended to whatever the turn returns, so task/tool
516+ // notifications ride along with every terminal action list (emit is
517+ // composable with all other actions).
518+ private pendingEmits : ReactorAction [ ] = [ ] ;
503519
504520 constructor (
505521 systemPrompt : string ,
@@ -527,9 +543,7 @@ class ChatDirectorImpl extends DefaultDirector {
527543 this . inactivityTimeoutMs = options . inactivityTimeoutMs ;
528544 this . totalTimeoutMs = options . totalTimeoutMs ;
529545 this . taskClassifier = options . taskClassifier ;
530- this . onActivateTools = options . onActivateTools ;
531546 this . workflowCoordinator = options . workflowCoordinator ;
532- this . onTasksChange = options . onTasksChange ;
533547 this . compaction = createCompactionGovernor (
534548 options . requestContinuation ,
535549 composedPrompt ,
@@ -559,10 +573,11 @@ class ChatDirectorImpl extends DefaultDirector {
559573 // A resumed session's task list lives in the transcript, not in the freshly
560574 // constructed director. Without this the chrome panel would read an empty
561575 // list until the model happened to call manage_tasks again, disagreeing
562- // with the task block already painted in the transcript.
576+ // with the task block already painted in the transcript. The host emits
577+ // the tasks-changed reactor event after calling this (the director cannot
578+ // emit outside decide()).
563579 restoreTasks ( tasks : Task [ ] ) : void {
564580 this . tasks = [ ...tasks ] ;
565- this . onTasksChange ?.( this . tasks ) ;
566581 }
567582
568583 // The status bar's context meter falls back to this when a provider omits
@@ -671,7 +686,11 @@ class ChatDirectorImpl extends DefaultDirector {
671686 await this . decideInner ( event , state , capabilities ) ,
672687 capabilities ,
673688 ) ;
674- return this . withCurrentTools ( settled ) ;
689+ const withTools = this . withCurrentTools ( settled ) ;
690+ if ( this . pendingEmits . length === 0 ) return withTools ;
691+ const emits = this . pendingEmits ;
692+ this . pendingEmits = [ ] ;
693+ return [ ...( Array . isArray ( withTools ) ? withTools : [ withTools ] ) , ...emits ] ;
675694 }
676695
677696 private async decideInner (
@@ -867,7 +886,11 @@ class ChatDirectorImpl extends DefaultDirector {
867886 const next = applyManageTasksToolCall ( this . tasks , block ) ;
868887 if ( next !== null ) {
869888 this . tasks = next ;
870- this . onTasksChange ?.( this . tasks ) ;
889+ this . pendingEmits . push (
890+ capabilities . emit ( CHAT_TASKS_CHANGED_EVENT , {
891+ tasks : this . tasks ,
892+ } ) ,
893+ ) ;
871894 }
872895 } else if ( block . name === "read_file" || block . name === "edit_file" ) {
873896 const pathResult = PathArgSchema ( block . arguments ) ;
@@ -915,7 +938,10 @@ class ChatDirectorImpl extends DefaultDirector {
915938 this . lspTriggerCalls . has ( event . result . callId )
916939 ) {
917940 this . lspTriggerCalls . delete ( event . result . callId ) ;
918- if ( ! event . result . isError ) this . onActivateTools ?.( [ "lsp" ] ) ;
941+ if ( ! event . result . isError )
942+ this . pendingEmits . push (
943+ capabilities . emit ( CHAT_TOOLS_ACTIVATE_EVENT , { names : [ "lsp" ] } ) ,
944+ ) ;
919945 }
920946
921947 if ( event . type === "tool.done" ) {
0 commit comments