Files
BlackBean/pigo/internal/cli/run/hooks_driver.go
T
2026-08-14 23:41:57 +08:00

65 lines
3.0 KiB
Go

// This file provides the single convergence entry point every driver calls to
// wire hooks into its run (#425, FR-16). Before this, each of the six RunConfig
// assembly sites (repl/goal/btw/tui/headless/subagent_rpc) built the loop config
// independently, which was the main risk of a hook point being silently dropped
// in one mode but not another. Routing them all through InstallDriverHooks makes
// "which hook points a run has" a single decision rather than six.
//
// InstallDriverHooks resolves the trust-gated hook set (FR-14), installs the
// tool-execution and Stop seams via InstallHooks, dispatches SessionStart inline
// so injected context reaches turn one (#423), and chains the observer notifier
// (SessionEnd/PreCompact, #424) onto the driver's event seam. It returns the
// Dispatcher so a caller can additionally run UserPromptSubmit (prompt entry) or
// InstallSubagentStop (sub-agent), and the possibly-wrapped event handler to
// install on whatever OnEvent seam the driver owns (HeadlessConfig.OnEvent for
// headless, the DrainStream handler for the REPL/TUI).
package run
import (
"context"
"github.com/smallnest/pigo/internal/agentcore"
"github.com/smallnest/pigo/internal/hooks"
"github.com/smallnest/pigo/internal/runtime"
)
// InstallDriverHooks is the uniform hook-wiring seam for every driver. It
// installs the tool-execution + Stop hook points onto cfg, dispatches
// SessionStart (registering any additionalContext as a one-shot reminder so it
// lands in turn one), and chains the SessionEnd/PreCompact observer onto onEvent.
//
// set is the already-resolved, trust-gated hook set (see ResolveHookSet). When
// it is empty NewDispatcher returns nil, so this is a no-op that returns
// (nil, onEvent) and the hot path pays nothing (FR-18): the run behaves exactly
// as it did before hooks existed.
//
// The returned dispatcher (nil when no hooks) lets the caller wire the remaining
// prompt-scoped / sub-agent hooks. The returned handler is onEvent unchanged when
// there are no hooks, or onEvent with the notifier chained after it otherwise —
// so the driver installs one handler regardless.
func InstallDriverHooks(ctx context.Context, cfg *runtime.RunConfig, set hooks.HookSet, deps HookDeps, source string, onEvent func(agentcore.AgentEvent)) (*hooks.Dispatcher, func(agentcore.AgentEvent)) {
d := InstallHooks(cfg, set, deps)
if d == nil {
return nil, onEvent
}
DispatchSessionStart(ctx, d, cfg, deps, source)
n := hooks.NewHookNotifier(d, deps.SessionID, deps.ProjectDir)
return d, chainEvent(onEvent, n.Handle)
}
// chainEvent composes two AgentEvent observers into one that calls prev then
// next. A nil operand is identity, so chaining onto an unset seam returns the
// other unchanged (and returns nil when both are nil, keeping the seam unset).
func chainEvent(prev, next func(agentcore.AgentEvent)) func(agentcore.AgentEvent) {
if prev == nil {
return next
}
if next == nil {
return prev
}
return func(ev agentcore.AgentEvent) {
prev(ev)
next(ev)
}
}