| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | |
| 6 | "reasonix/internal/agent" |
| 7 | "reasonix/internal/billing" |
| 8 | "reasonix/internal/checkpoint" |
| 9 | "reasonix/internal/command" |
| 10 | "reasonix/internal/config" |
| 11 | "reasonix/internal/event" |
| 12 | "reasonix/internal/evidence" |
| 13 | "reasonix/internal/hook" |
| 14 | "reasonix/internal/jobs" |
| 15 | "reasonix/internal/memory" |
| 16 | "reasonix/internal/plugin" |
| 17 | "reasonix/internal/provider" |
| 18 | "reasonix/internal/sandbox" |
| 19 | "reasonix/internal/session" |
| 20 | "reasonix/internal/skill" |
| 21 | ) |
| 22 | |
| 23 | // This file defines the driving port: the typed, segregated interface surface |
| 24 | // that frontends (cli, desktop, bot, acp, serve) consume instead of coupling to |
| 25 | // the concrete *Controller and its ~99 methods. Each frontend depends only on |
| 26 | // the sub-ports it actually uses (interface segregation), so e.g. the bot never |
| 27 | // sees checkpoint or memory methods. |
| 28 | // |
| 29 | // The sub-ports are also the intended decomposition boundary for Controller |
| 30 | // itself: the port comes first and gives the later collaborator splits a spec to |
| 31 | // follow. *Controller implements every sub-port (asserted below). The full |
| 32 | // SessionAPI composition will accrete here as the remaining frontends migrate. |
| 33 | |
| 34 | // Lifecycle covers a session's identity and lifecycle: minting, resuming, |
| 35 | // clearing, and locating the active session. |
| 36 | type Lifecycle interface { |
| 37 | NewSession() error |
| 38 | ClearSession() error |
| 39 | Resume(s *agent.Session, path string) |
| 40 | SetSessionPath(p string) |
| 41 | SessionPath() string |
| 42 | SessionDir() string |
| 43 | Label() string |
| 44 | ModelRef() string |
| 45 | WorkspaceRoot() string |
| 46 | Close() |
| 47 | } |
| 48 | |
| 49 | // IdentityLifecycle is the final session-id based lifecycle. Frontends may |
| 50 | // type-assert it while legacy read/import DTOs remain available; new execution |
| 51 | // commands must use this surface instead of manufacturing transcript paths. |
| 52 | type IdentityLifecycle interface { |
| 53 | SessionRef() (session.SessionRef, bool) |
| 54 | SessionService() *session.Service |
| 55 | UsesExclusiveSession() bool |
| 56 | BindFreshSession(context.Context, string) (session.SessionRef, error) |
| 57 | OpenSession(context.Context, session.SessionRef) (session.SessionRef, error) |
| 58 | ContinueLegacySession(context.Context, string, string) (session.SessionRef, error) |
| 59 | ContinuePrototypeSession(context.Context, string) (session.SessionRef, error) |
| 60 | } |
| 61 | |
| 62 | // IdentityCreateLifecycle is the header-aware creation extension used by the |
| 63 | // Desktop Workspace registry. Other frontends may continue using |
| 64 | // IdentityLifecycle.BindFreshSession while they do not own Workspace metadata. |
| 65 | type IdentityCreateLifecycle interface { |
| 66 | BindFreshSessionWithOptions(context.Context, session.CreateOptions) (session.SessionRef, error) |
| 67 | ContinueLegacySessionWithOptions(context.Context, string, string, session.CreateOptions) (session.SessionRef, error) |
| 68 | } |
| 69 | |
| 70 | // TurnControl covers driving a model turn and observing its run state: the |
| 71 | // various submit/run entry points, cancellation, steering, and status reads. |
| 72 | type TurnControl interface { |
| 73 | Submit(input string) |
| 74 | SubmitDisplay(display, input string) |
| 75 | SubmitFinalReadinessRecovery(display, input string) |
| 76 | SubmitDeliveryRecovery(display, input string) |
| 77 | SubmitInvocationDisplay(display, input string, invocations []InvocationRequest) |
| 78 | SubmitEditedDisplay(display, input, original string) |
| 79 | SubmitHTTP(input string) |
| 80 | SubmitHTTPFormat(input, format string) |
| 81 | SubmitUserTurn(input, display string) |
| 82 | Send(input string) |
| 83 | SendWithRaw(input, raw string) |
| 84 | Run(ctx context.Context, input string) error |
| 85 | RunTurn(ctx context.Context, input string) error |
| 86 | RunFinalReadinessRecovery(ctx context.Context, input string) error |
| 87 | RunShell(command string) |
| 88 | Cancel() |
| 89 | Steer(text string) |
| 90 | SteerConsumed() bool |
| 91 | Running() bool |
| 92 | CancelRequested() bool |
| 93 | RuntimeStatus() RuntimeStatus |
| 94 | Turn() int |
| 95 | History() []provider.Message |
| 96 | ToolResult(toolID string) *ToolResultData |
| 97 | } |
| 98 | |
| 99 | // Approvals covers tool-approval and ask prompts plus the runtime approval |
| 100 | // posture (ask/auto/yolo). It mirrors the approvalManager surface. |
| 101 | type Approvals interface { |
| 102 | Approve(id string, allow, session, persist bool) |
| 103 | ResolveApproval(id string, allow bool, scope sandbox.ApprovalScope) error |
| 104 | ResolvePlanDecision(id string, action PlanDecisionAction) error |
| 105 | ResolvePlanDecisionWithFeedback(id string, action PlanDecisionAction, feedback string) error |
| 106 | // ResolveRecovery rejects retired Auto Guard actions with recovery_retired. |
| 107 | ResolveRecovery(id string, action agent.RecoveryAction, feedback string) error |
| 108 | AnswerMCPInteraction(id, action string, content map[string]any) |
| 109 | AnswerQuestion(id string, answers []event.AskAnswer) |
| 110 | AnswerQuestionChecked(id string, answers []event.AskAnswer) error |
| 111 | // AnswerMCPInteractionChecked resolves an mcp_interaction prompt after its |
| 112 | // durable transition (serve /mcp-interaction, desktop bridge). |
| 113 | AnswerMCPInteractionChecked(id, action string, content map[string]any) error |
| 114 | Ask(ctx context.Context, questions []event.AskQuestion) ([]event.AskAnswer, error) |
| 115 | ReplayPendingPrompts() |
| 116 | ReplayPendingPromptsTo(sink event.Sink) |
| 117 | ReplayPendingPromptsWith(sinkFactory func() event.Sink) |
| 118 | PendingPrompt() bool |
| 119 | EnableInteractiveApproval() |
| 120 | ToolApprovalMode() string |
| 121 | SetToolApprovalMode(mode string) |
| 122 | AutoApproveTools() bool |
| 123 | SetAutoApproveTools(on bool) |
| 124 | Bypass() bool |
| 125 | SetBypass(on bool) |
| 126 | SetMode(plan, autoApproveTools bool) |
| 127 | } |
| 128 | |
| 129 | // Goals covers the active-goal FSM and plan mode. |
| 130 | type Goals interface { |
| 131 | Goal() string |
| 132 | GoalStatus() string |
| 133 | SetGoal(goal string) |
| 134 | // SetGoalDurable updates the Goal only after its backing session accepts |
| 135 | // the lifecycle mutation. Hosts must use this method before publishing UI |
| 136 | // metadata or starting a provider turn. |
| 137 | SetGoalDurable(goal string) error |
| 138 | // EditGoalDurable changes an existing Goal in place. It preserves the Goal |
| 139 | // identity and admitted round count while advancing its CAS revision. |
| 140 | EditGoalDurable(objective string, maxGoalRounds *uint64) error |
| 141 | // SetGoalWithResearchMode is retained for deprecated CLI budget flags. The |
| 142 | // mode is translated at the boundary and is not stored in the Goal runtime. |
| 143 | SetGoalWithResearchMode(goal string, researchMode GoalResearchMode) |
| 144 | ResumeGoal() bool |
| 145 | PauseGoal() bool |
| 146 | GoalRuntime() GoalRuntimeView |
| 147 | GoalStrict(strict bool) |
| 148 | ClearGoal() |
| 149 | ResetPlannerSession() |
| 150 | PlanMode() bool |
| 151 | SetPlanMode(v bool) |
| 152 | // AgentPreset is the session role setting (standard|delivery), derived |
| 153 | // from the quality floor. |
| 154 | AgentPreset() string |
| 155 | // SetAgentPreset updates the role setting for subsequent turns without |
| 156 | // rebuilding the controller. |
| 157 | SetAgentPreset(preset string) |
| 158 | // QualityFloor is the session delivery floor (standard|delivery). |
| 159 | QualityFloor() string |
| 160 | // SetQualityFloor updates the session delivery floor for subsequent |
| 161 | // turns without rebuilding the controller. |
| 162 | SetQualityFloor(floor string) error |
| 163 | } |
| 164 | |
| 165 | // SessionHistory covers checkpoint/rewind, branch/fork, and the log-restructuring |
| 166 | // operations (compact, summarize). |
| 167 | type SessionHistory interface { |
| 168 | Checkpoints() []checkpoint.Meta |
| 169 | CheckpointFileState(path string) (checkpoint.FileState, bool) |
| 170 | CheckpointTurnChanges(turn int) *checkpoint.TurnChanges |
| 171 | CheckpointTurnsByMessageIndex() map[int]int |
| 172 | CheckpointHasBoundary(turn int) bool |
| 173 | Rewind(turn int, scope RewindScope) error |
| 174 | PrepareRewind(turn int, scope RewindScope) (checkpoint.RewindPlan, error) |
| 175 | CommitRewind(planID string) (checkpoint.RewindResult, error) |
| 176 | CommitRewindInPlace(planID string) (checkpoint.RewindResult, error) |
| 177 | SessionHead() (agent.HeadRef, bool) |
| 178 | UndoRewind(transactionID string) (checkpoint.RewindResult, error) |
| 179 | PrepareFileRevert(path string) (checkpoint.RewindPlan, error) |
| 180 | CommitFileRevert(planID string, resolution checkpoint.ConflictResolution) (checkpoint.RewindResult, error) |
| 181 | Fork(turn int) (string, error) |
| 182 | ForkNamed(turn int, name string) (string, error) |
| 183 | ForkSession(turn int, name string) (string, error) |
| 184 | Branch(name string) (string, error) |
| 185 | Branches() ([]agent.BranchInfo, error) |
| 186 | BranchTreeText() string |
| 187 | CurrentBranchID() string |
| 188 | SwitchBranch(ref string) (agent.BranchInfo, error) |
| 189 | Compact(ctx context.Context, instructions string) error |
| 190 | CompactRatio() float64 |
| 191 | ContextReport() (summary, detail string) |
| 192 | SummarizeFrom(ctx context.Context, turn int) error |
| 193 | SummarizeUpTo(ctx context.Context, turn int) error |
| 194 | } |
| 195 | |
| 196 | // MemoryControl covers session/project memory reads and mutations. |
| 197 | type MemoryControl interface { |
| 198 | Memory() *memory.Set |
| 199 | QuickAdd(scope memory.Scope, note string) (string, error) |
| 200 | SaveDoc(path, body string) (string, error) |
| 201 | SaveMemory(m memory.Memory) (string, error) |
| 202 | ForgetMemory(name string) error |
| 203 | QueueMemory(note string) |
| 204 | MemoryRevisions(ref string) []memory.Memory |
| 205 | RestoreMemory(ref string, revision int) (memory.Memory, error) |
| 206 | RestoreArchivedMemory(archivePath string) (memory.Memory, error) |
| 207 | LastMemoryRecall() memory.RecallResult |
| 208 | } |
| 209 | |
| 210 | // Capabilities covers the session's pluggable surface — MCP servers, skills, |
| 211 | // slash commands, hooks — and resolving prompt/command/skill inputs. |
| 212 | type Capabilities interface { |
| 213 | Host() *plugin.Host |
| 214 | Commands() []command.Command |
| 215 | ReloadCommands(ctx context.Context) error |
| 216 | Skills() []skill.Skill |
| 217 | SlashSkills() []skill.Skill |
| 218 | AllSkills() []skill.Skill |
| 219 | LoadSkill(name string) (skill.Skill, bool) |
| 220 | DisabledSkills() []skill.Skill |
| 221 | SkillEnabled(name string) bool |
| 222 | SetSkillEnabled(name string, enabled bool) error |
| 223 | CreateSkill(name string, scope skill.Scope, content string) (string, error) |
| 224 | UpdateSkill(name string, scope skill.Scope, content string) error |
| 225 | DeleteSkill(name string, scope skill.Scope) error |
| 226 | HookRunner() *hook.Runner |
| 227 | CustomCommand(input string) (sent string, found bool) |
| 228 | MCPPrompt(ctx context.Context, input string) (sent string, found bool, err error) |
| 229 | // MCPCapabilityViews returns the host's four-layer capability matrix |
| 230 | // (Protocol Connection, Core Host, Interactive Host, Apps Host) as |
| 231 | // read-only diagnostics for MCP status surfaces. |
| 232 | MCPCapabilityViews() []plugin.CapabilityView |
| 233 | RunSkill(input string) (sent string, found bool) |
| 234 | AddMCPServer(e config.PluginEntry) (int, error) |
| 235 | ConnectMCPServer(e config.PluginEntry) (int, error) |
| 236 | RegisterMCPServerOnDemand(e config.PluginEntry) (int, error) |
| 237 | ConnectConfiguredMCPServer(name string) (int, error) |
| 238 | DisconnectMCPServer(name string) bool |
| 239 | RemoveMCPServer(name string) (disconnected bool, err error) |
| 240 | ConfiguredMCPNames() []string |
| 241 | DisconnectedMCPNames() []string |
| 242 | UnregisterMCPServerTools(name string) bool |
| 243 | ImportMCPEntries(entries []config.PluginEntry) (total, added, updated, connected, failed, skipped int, err error) |
| 244 | // Extension UI (stage 8a): enumerate handshake-declared extension actions |
| 245 | // and invoke one by its public /<plugin>:<action> name. Nil hub → empty / |
| 246 | // error; the stage-8b slash dispatch resolves these. |
| 247 | ExtensionActions() []ExtensionActionView |
| 248 | InvokeExtensionAction(ctx context.Context, name string, args map[string]string) (string, error) |
| 249 | // ProviderCatalog is the session's merged provider catalog — config/broker |
| 250 | // base plus sidecar-declared extension providers (plugin/... refs). Nil |
| 251 | // when no extension declared providers; frontends merge it into their |
| 252 | // model pickers and skip nil. |
| 253 | ProviderCatalog() []provider.Descriptor |
| 254 | } |
| 255 | |
| 256 | // Status covers read-only run/usage/billing telemetry and task list state. |
| 257 | type Status interface { |
| 258 | ContextSnapshot() (int, int) |
| 259 | ContextMaintenanceSnapshot() agent.ContextMaintenanceSnapshot |
| 260 | LastUsage() *provider.Usage |
| 261 | Balance(ctx context.Context) (*billing.Balance, error) |
| 262 | Jobs() []jobs.View |
| 263 | Todos() []evidence.TodoItem |
| 264 | // BoundShell reports the interpreter this controller generation bound at |
| 265 | // build time, so hosts can distinguish the live session's shell from what |
| 266 | // a reload would resolve now. |
| 267 | BoundShell() sandbox.Shell |
| 268 | } |
| 269 | |
| 270 | // SessionPersistence covers snapshotting a session and tearing down its on-disk |
| 271 | // state. |
| 272 | type SessionPersistence interface { |
| 273 | Snapshot() error |
| 274 | SnapshotForShutdown() error |
| 275 | SnapshotActivity() error |
| 276 | // SessionHasUnsavedChanges reports whether the in-memory transcript is |
| 277 | // newer than the durable session file. Frontends use this to avoid |
| 278 | // replacing a failed/contended save with stale disk history. |
| 279 | SessionHasUnsavedChanges() bool |
| 280 | SessionCache() (hit, miss int) |
| 281 | BeginDestroySession(sessionPath string) SessionDestroyHandle |
| 282 | CloseAfterDestroy() |
| 283 | IsDestroyingSession(sessionPath string) bool |
| 284 | ReleaseResources() |
| 285 | } |
| 286 | |
| 287 | // Input covers composing a turn's text (plan/goal/memory injection) and |
| 288 | // resolving @-references before submission. |
| 289 | type Input interface { |
| 290 | Compose(text string) string |
| 291 | ComposeSynthetic(text string) string |
| 292 | ResolveRefs(ctx context.Context, line string) (block string, errs []string) |
| 293 | HasRefs(line string) bool |
| 294 | ImageInputEnabled() bool |
| 295 | RegisterExternalFolderRef(path string) (token, displayPath string, err error) |
| 296 | } |
| 297 | |
| 298 | // Settings covers runtime session settings that don't fit a richer domain. |
| 299 | type Settings interface { |
| 300 | SetResponseLanguage(lang string) |
| 301 | SetReasoningLanguage(lang string) |
| 302 | SetDisplayRecorder(fn func(content, display string)) |
| 303 | ApplyComposerProfile(plan bool, toolApprovalMode, goal string) ([]string, error) |
| 304 | SystemPrompt() string |
| 305 | } |
| 306 | |
| 307 | // SessionAPI is the full driving port — the composition of every sub-port. A |
| 308 | // rich frontend (the HTTP server, the desktop app, the TUI) depends on this; |
| 309 | // leaner frontends (bot, acp) depend on just the sub-ports they use. |
| 310 | type SessionAPI interface { |
| 311 | Lifecycle |
| 312 | TurnControl |
| 313 | Approvals |
| 314 | Goals |
| 315 | SessionHistory |
| 316 | MemoryControl |
| 317 | Capabilities |
| 318 | Status |
| 319 | SessionPersistence |
| 320 | Input |
| 321 | Settings |
| 322 | Inbox |
| 323 | } |
| 324 | |
| 325 | // Compile-time proof that the concrete controller satisfies each sub-port and |
| 326 | // the full port, so frontend migrations to the interfaces are mechanical and can |
| 327 | // never silently drift from the implementation. |
| 328 | var ( |
| 329 | _ Lifecycle = (*Controller)(nil) |
| 330 | _ IdentityLifecycle = (*Controller)(nil) |
| 331 | _ TurnControl = (*Controller)(nil) |
| 332 | _ Approvals = (*Controller)(nil) |
| 333 | _ Goals = (*Controller)(nil) |
| 334 | _ SessionHistory = (*Controller)(nil) |
| 335 | _ MemoryControl = (*Controller)(nil) |
| 336 | _ Capabilities = (*Controller)(nil) |
| 337 | _ Status = (*Controller)(nil) |
| 338 | _ SessionPersistence = (*Controller)(nil) |
| 339 | _ Input = (*Controller)(nil) |
| 340 | _ Settings = (*Controller)(nil) |
| 341 | _ Inbox = (*Controller)(nil) |
| 342 | _ SessionAPI = (*Controller)(nil) |
| 343 | ) |
| 344 |