Current Limitation
taskflow/orchestrator/TaskManager is a single struct that flattens two domain layers from the package doc into one type:
- Layer 2 (Task / micro-journey):
StartTask, HandleTaskCompletion, plus the presentation/read methods GetTaskRenderInfo, GetAllTasks.
- Layer 3 (SubTask / interaction steps):
StartSubTask, CompleteTaskStep.
Because everything lives on one struct, every consumer must construct the full TaskManager with all ~7 dependencies (db, registry, pluginsRegistry, taskWorkflowManager, onTaskCompleted, renderer, extensionsRegistry), even when it only operates at one layer. Each method actually uses only a subset:
| Method |
db |
registry |
pluginsRegistry |
taskWorkflowManager |
onTaskCompleted |
renderer |
extensionsRegistry |
StartTask |
✓ |
✓ |
|
✓ |
|
|
|
HandleTaskCompletion |
✓ |
|
|
|
✓ |
|
|
GetTaskRenderInfo |
✓ |
|
|
|
|
✓ |
|
GetAllTasks |
✓ |
|
|
|
|
|
|
StartSubTask |
✓ |
✓ |
✓ |
|
|
|
|
CompleteTaskStep |
✓ |
|
|
✓ |
|
|
✓ |
This makes the API harder to integrate against and couples unrelated concerns (e.g. a parent-workflow consumer drags in the plugin/extension registries it never touches).
Suggested Improvement
Split TaskManager into two managers grouped by domain layer (sharing the store.TaskStore spine — no duplicated state, pure move-the-methods refactor with no behavior change):
TaskManager (Layer 2 — Task + presentation)
- Methods:
StartTask, HandleTaskCompletion, GetTaskRenderInfo, GetAllTasks
- Deps:
db, registry, taskWorkflowManager, onTaskCompleted, renderer
SubTaskManager (Layer 3 — SubTask / interaction steps)
- Methods:
StartSubTask, CompleteTaskStep
- Deps:
db, registry, pluginsRegistry, taskWorkflowManager, extensionsRegistry
Why this axis:
- Mirrors the documented Workflow → Task → SubTask hierarchy, so the code structure is self-explanatory.
- Keeps
StartSubTask + CompleteTaskStep together — both mutate the active-subtask coordinates on the record (SubTaskNodeID, ActiveOutputNamespace, ActiveExtensions), a shared invariant that should not be split across structs.
- Isolates
pluginsRegistry and extensionsRegistry cleanly to Layer 3.
- Shrinks per-consumer wiring from ~7 deps to ~5: a Layer 2 consumer drops
pluginsRegistry/extensionsRegistry; a Layer 3 consumer drops onTaskCompleted/renderer.
Acceptance criteria
Version
main @ cca024f
Additional Context
- Naming:
ExecuteTask in discussion == the existing CompleteTaskStep method (no rename planned in this issue).
- Scope note: This is a structural refactor only. Two follow-ups are explicitly out of scope: (1) optionally exposing narrow interfaces (e.g. a
StepCompleter) so call-sites depend on 1–2 methods rather than the concrete struct — a further wiring win; (2) if GetTaskRenderInfo later gains the plugin-driven render-metadata fetching its doc comment aspirationally describes, the read side would re-acquire a pluginsRegistry dependency.
- Alternative axis considered: a read/write (CQRS-ish) split would give rendering a
{db, renderer}-only home (minimal wiring for API consumers) but would separate the active-subtask invariant and not mirror the domain doc. The layer axis was chosen for conceptual clarity.
Current Limitation
taskflow/orchestrator/TaskManageris a single struct that flattens two domain layers from the package doc into one type:StartTask,HandleTaskCompletion, plus the presentation/read methodsGetTaskRenderInfo,GetAllTasks.StartSubTask,CompleteTaskStep.Because everything lives on one struct, every consumer must construct the full
TaskManagerwith all ~7 dependencies (db, registry, pluginsRegistry, taskWorkflowManager, onTaskCompleted, renderer, extensionsRegistry), even when it only operates at one layer. Each method actually uses only a subset:StartTaskHandleTaskCompletionGetTaskRenderInfoGetAllTasksStartSubTaskCompleteTaskStepThis makes the API harder to integrate against and couples unrelated concerns (e.g. a parent-workflow consumer drags in the plugin/extension registries it never touches).
Suggested Improvement
Split
TaskManagerinto two managers grouped by domain layer (sharing thestore.TaskStorespine — no duplicated state, pure move-the-methods refactor with no behavior change):TaskManager(Layer 2 — Task + presentation)StartTask,HandleTaskCompletion,GetTaskRenderInfo,GetAllTasksdb, registry, taskWorkflowManager, onTaskCompleted, rendererSubTaskManager(Layer 3 — SubTask / interaction steps)StartSubTask,CompleteTaskStepdb, registry, pluginsRegistry, taskWorkflowManager, extensionsRegistryWhy this axis:
StartSubTask+CompleteTaskSteptogether — both mutate the active-subtask coordinates on the record (SubTaskNodeID,ActiveOutputNamespace,ActiveExtensions), a shared invariant that should not be split across structs.pluginsRegistryandextensionsRegistrycleanly to Layer 3.pluginsRegistry/extensionsRegistry; a Layer 3 consumer dropsonTaskCompleted/renderer.Acceptance criteria
SubTaskManagerintroduced withStartSubTask+CompleteTaskStepand only its required depsTaskManagerretainsStartTask,HandleTaskCompletion,GetTaskRenderInfo,GetAllTaskssetNestedKey,payloadKeys,deepcopy/runExtensionsusage) placed so both managers can use them without duplicationVersion
main @
cca024fAdditional Context
ExecuteTaskin discussion == the existingCompleteTaskStepmethod (no rename planned in this issue).StepCompleter) so call-sites depend on 1–2 methods rather than the concrete struct — a further wiring win; (2) ifGetTaskRenderInfolater gains the plugin-driven render-metadata fetching its doc comment aspirationally describes, the read side would re-acquire apluginsRegistrydependency.{db, renderer}-only home (minimal wiring for API consumers) but would separate the active-subtask invariant and not mirror the domain doc. The layer axis was chosen for conceptual clarity.