返回 CodeWhale
ops.rs
根目录 / crates / tui / src / core / ops.rs
1 //! Operations submitted by the UI to the core engine.
2 //!
3 //! These operations flow from the TUI to the engine via a channel,
4 //! allowing the UI to remain responsive while the engine processes requests.
5
6 use crate::compaction::CompactionConfig;
7 use crate::config::ProviderKind;
8 use crate::route_runtime::ResolvedRuntimeRoute;
9 use crate::tools::goal::GoalStatus;
10 use codewhale_config::AppMode;
11 use codewhale_execpolicy::ApprovalMode;
12 use codewhale_models::{Message, SystemPrompt};
13 use codewhale_protocol::runtime::DynamicToolSpec;
14 use std::path::PathBuf;
15
16 /// Prefix used for tool-call ids created by local composer shell shortcuts.
17 pub const USER_SHELL_TOOL_ID_PREFIX: &str = "user_shell_";
18
19 /// Snapshot of session state for saving to disk.
20 /// Returned by `Op::GetSessionSnapshot` via a oneshot channel.
21 #[derive(Debug, Clone, PartialEq, serde::Serialize)]
22 pub struct SessionSnapshot {
23 /// The live conversation id this engine session is running under.
24 ///
25 /// Every workspace snapshot the conversation takes is tagged with it. A
26 /// Runtime thread's engine runs under the thread's own id; a save that
27 /// names no document persists under this id, so one conversation keeps
28 /// one document. Runtime snapshot ownership is the receipts recorded on
29 /// the thread's turns, not this binding (see `patch_undo_workspace_files`).
30 pub session_id: String,
31 pub messages: Vec<Message>,
32 pub total_tokens: u64,
33 pub model: String,
34 /// Generic provider kind retained for serialized compatibility.
35 pub model_provider: String,
36 /// Exact non-secret configured provider key.
37 pub model_provider_id: Option<String>,
38 pub workspace: PathBuf,
39 pub system_prompt: Option<SystemPrompt>,
40 pub mode: String,
41 }
42
43 /// Live context-window posture for one thread, computed where the session
44 /// state actually lives. Returned by `Op::GetContextBudget` via a oneshot
45 /// channel so HTTP clients (GPUI usage panel) never re-derive the engine's
46 /// token math or route limits at the API boundary.
47 #[derive(Debug, Clone)]
48 pub struct SessionContextBudget {
49 /// Total context window for the active route (input + output), in tokens.
50 pub window_tokens: u64,
51 /// Estimated input tokens on the same basis the visible context meter
52 /// and the auto-compaction gate use (`estimate_input_tokens_for_pressure`,
53 /// without the overflow guard's 1.5x inflation). This is the number a
54 /// "context filling up" indicator shows.
55 pub input_tokens: u64,
56 /// Provider-billed prompt tokens from the most recent parent-route
57 /// request that still describes the live message list. `None` when no
58 /// provider count exists yet (fresh session) — never a fabricated zero.
59 pub billed_input_tokens: Option<u64>,
60 /// Output tokens reserved for the turn after route clamps.
61 pub output_cap_tokens: u64,
62 /// Spendable input ceiling (`window - output_cap - headroom`, intersected
63 /// with any provider-published hard input limit).
64 pub input_budget_ceiling: u64,
65 /// Input tokens still available before the reserved boundary.
66 pub available_input_tokens: u64,
67 /// Input level at which compaction is suggested.
68 pub compaction_trigger_tokens: u64,
69 /// `input_tokens / window_tokens` as a percentage (0..=100).
70 pub usage_percent: f64,
71 /// Coarse pressure label (`low`/`moderate`/`high`/`critical`).
72 pub pressure: &'static str,
73 /// Route identity the budget was computed for.
74 pub model: String,
75 pub provider: String,
76 pub model_provider_id: Option<String>,
77 }
78
79 /// Provider request runtime state surfaced by `/provider`.
80 /// Returned by `Op::GetProviderRuntimeStatus` via a oneshot channel.
81 #[derive(Debug, Clone, PartialEq, Eq)]
82 pub struct ProviderRuntimeStatus {
83 pub provider: ProviderKind,
84 pub request_concurrency_limit: Option<usize>,
85 pub active_provider_requests: usize,
86 }
87
88 /// Idle Engine snapshot used by a one-shot host before shutting down.
89 /// The completion inbox belongs to the Engine; a terminal worker alone does
90 /// not prove that its parent has consumed the handback.
91 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
92 pub struct SubAgentSettlement {
93 pub running_children: usize,
94 /// A Workflow can still be coordinating between child phases.
95 pub running_workflows: usize,
96 pub pending_completions: usize,
97 }
98
99 impl SubAgentSettlement {
100 #[must_use]
101 pub fn is_settled(self) -> bool {
102 self.running_children == 0 && self.running_workflows == 0 && self.pending_completions == 0
103 }
104 }
105
106 /// Engine-owned MCP snapshot plus the exact event generation it supersedes.
107 /// The TUI uses the receipt to reject already-queued boot events even when it
108 /// had not rendered that generation before the direct `/mcp` action.
109 #[derive(Debug, Clone)]
110 pub struct McpManagerUpdate {
111 pub snapshot: crate::mcp::McpManagerSnapshot,
112 pub generation: u64,
113 }
114
115 /// Result of rebuilding the engine-owned MCP pool in process.
116 pub type McpReloadResult = Result<McpManagerUpdate, String>;
117
118 /// Result of the one-shot boot connection pass for the engine-owned MCP pool.
119 ///
120 /// This shares the reload result shape while remaining a separate operation:
121 /// boot may fill an empty live pool, but it must not force a config reload or
122 /// invalidate already-ready connections.
123 pub type McpBootstrapResult = Result<McpManagerUpdate, String>;
124
125 /// Origin of text being introduced as a user-role turn.
126 ///
127 /// Chat providers force several runtime/control-plane signals through
128 /// `role = "user"` for compatibility, so role alone is not authority.
129 #[allow(dead_code)] // Some origins are reserved for ingestion sites landing after the first gate.
130 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
131 pub enum UserInputProvenance {
132 /// Text typed or submitted through the active UI/API input boundary.
133 ExternalUser,
134 /// Runtime-generated continuation, diagnostic, or tool feedback.
135 Runtime,
136 /// Completion/event text from a child worker or sub-agent handoff.
137 SubAgentHandoff,
138 /// A bounded, typed Agent Mail envelope delivered by the durable runtime.
139 /// Provider protocols still receive a user-role projection, but this
140 /// provenance can never inherit external-user authority.
141 AgentMail,
142 /// Text restored from a saved/imported transcript.
143 ImportedTranscript,
144 /// Text recalled from memory or another persisted source.
145 MemoryRecall,
146 /// Assistant-authored text that is shaped like a user response.
147 AssistantGenerated,
148 }
149
150 impl UserInputProvenance {
151 pub fn as_str(self) -> &'static str {
152 match self {
153 Self::ExternalUser => "external_user",
154 Self::Runtime => "runtime",
155 Self::SubAgentHandoff => "subagent_handoff",
156 Self::AgentMail => "agent_mail",
157 Self::ImportedTranscript => "imported_transcript",
158 Self::MemoryRecall => "memory_recall",
159 Self::AssistantGenerated => "assistant_generated",
160 }
161 }
162
163 pub fn can_authorize_work(self) -> bool {
164 matches!(self, Self::ExternalUser)
165 }
166 }
167
168 /// Per-turn authority payload carried by [`Op::SendMessage`]. Extracted from
169 /// the enum arm so new per-turn fields accrete here instead of widening the
170 /// variant; the serializable twin is `codewhale_protocol::op::TurnSpec`.
171 #[derive(Debug)]
172 pub struct TurnSpec {
173 /// Admitted allowance for this turn only; never changes session settings.
174 pub max_output_tokens: Option<std::num::NonZeroU32>,
175 pub content: String,
176 /// Inline image bytes validated by Runtime admission; no file references.
177 pub images: Vec<codewhale_protocol::runtime::RuntimeImageInput>,
178 pub mode: AppMode,
179 /// Exact, structurally resolved route authority for this turn. The
180 /// engine activates its client before mutating turn state; injected
181 /// engines may use their already-supplied client with the same receipt.
182 pub route: Box<ResolvedRuntimeRoute>,
183 /// Compaction policy derived from the same provider route. Carrying it
184 /// atomically avoids a model/limit mismatch before `SendMessage`.
185 pub compaction: Box<CompactionConfig>,
186 /// Auxiliary provider calls completed while planning this exact turn
187 /// (currently Auto's classifier), bounded and paired with their own
188 /// immutable routes. The engine folds their tokens into total usage
189 /// only; they never enter the parent route's billing aggregate.
190 pub initial_routed_usage: Box<crate::cost_status::RuntimeUsageBatch>,
191 pub goal_objective: Option<String>,
192 pub goal_token_budget: Option<u32>,
193 pub goal_status: GoalStatus,
194 /// Reasoning-effort tier: `"off" | "low" | "medium" | "high" | "max"`.
195 /// `None` lets the provider apply its default.
196 pub reasoning_effort: Option<String>,
197 /// True when the user selected auto thinking, even though the UI sends
198 /// a concrete per-turn value to the model API.
199 pub reasoning_effort_auto: bool,
200 /// True when the user selected auto model routing.
201 pub auto_model: bool,
202 pub allow_shell: bool,
203 pub trust_mode: bool,
204 pub auto_approve: bool,
205 pub approval_mode: ApprovalMode,
206 pub translation_enabled: bool,
207 /// Tool restriction from custom slash command frontmatter.
208 /// `None` means the current turn may use the normal tool set.
209 pub allowed_tools: Option<Vec<String>>,
210 /// Runtime-supplied tools available only for this turn.
211 pub dynamic_tools: Vec<DynamicToolSpec>,
212 /// Hook executor for control-plane hooks.
213 /// `ToolCallBefore` hooks may deny a tool call with exit code 2.
214 pub hook_executor: Option<std::sync::Arc<crate::hooks::HookExecutor>>,
215 pub verbosity: Option<String>,
216 /// Structural input origin. This gates whether the turn may inherit
217 /// YOLO/auto-approval authority; user-shaped text is not enough.
218 pub provenance: UserInputProvenance,
219 /// Host-supplied correlation token for this submission, echoed verbatim on
220 /// the turn's `Event::TurnStarted`. Hosts that arm submit→`TurnStarted`
221 /// window actions (e.g. a deferred stop replay) use the echo to bind those
222 /// actions to the turn that actually started: every engine self-started
223 /// turn (idle sub-agent completion, background shell wake, goal
224 /// continuation) and the composer shell command turn carry no token, so
225 /// their start cannot be mistaken for a pending submission even when it
226 /// overtakes it in the event stream. `None` for callers that do not
227 /// correlate. The token only survives the in-process engine path: the
228 /// wire op projection and the durable-runtime submission path carry none,
229 /// so hosts submitting over those channels cannot correlate.
230 pub submission_id: Option<String>,
231 }
232
233 /// Operations that can be submitted to the engine.
234 #[derive(Debug)]
235 pub enum Op {
236 /// Send a message to the AI
237 SendMessage(TurnSpec),
238
239 /// Re-check and dispatch an interactive goal continuation when this
240 /// operation reaches the front of the engine queue. Keeping this distinct
241 /// from `SendMessage` prevents a queued `/goal pause` or `/goal clear`
242 /// from being overwritten by a stale synthetic Active snapshot.
243 ContinueGoal {
244 /// Runtime-supplied tools remain available across the synthetic turn
245 /// that continues the same logical goal run.
246 dynamic_tools: Vec<DynamicToolSpec>,
247 /// Opaque identity for an engine-owned synthetic continuation. Direct
248 /// callers use `None`; the engine uses `Some` to coalesce one token
249 /// across capacity-waiting, enqueued, and running-adjacent states.
250 engine_schedule_id: Option<u64>,
251 },
252
253 /// Execute a user-submitted composer shell command (`! <command>`) without
254 /// sending a model turn. This still routes through `exec_shell`, approval,
255 /// sandbox, and command-safety handling.
256 RunShellCommand {
257 command: String,
258 mode: AppMode,
259 allow_shell: bool,
260 trust_mode: bool,
261 auto_approve: bool,
262 approval_mode: ApprovalMode,
263 },
264
265 /// Set the runtime goal status without dispatching a model turn. Used by
266 /// `/goal pause`, `/goal resume`, `/goal clear`, etc. so the engine's
267 /// `SharedGoalState` learns the new status immediately and a queued
268 /// continuation doesn't overwrite it back to Active.
269 SetGoalStatus {
270 status: GoalStatus,
271 /// When `true`, clear the objective entirely (`/goal clear`).
272 clear: bool,
273 /// Accepted control revision; None lets direct callers mint it.
274 goal_id: Option<String>,
275 },
276
277 /// Set (or replace) the active goal objective and immediately start goal
278 /// work through the runtime's continuation steering. `/goal <objective>`
279 /// is the caller; the objective is never echoed as a raw user message.
280 SetGoalObjective {
281 objective: String,
282 token_budget: Option<u32>,
283 /// Accepted control revision; None lets direct callers mint it.
284 goal_id: Option<String>,
285 },
286
287 /// Describe the exact request the next turn would send, without
288 /// sending it (`/preview-request`, #1004).
289 ///
290 /// Handled by the engine because only the engine can rebuild the current
291 /// tool catalog, MCP state, mode, gates, permission posture, and resolved
292 /// route. Pure inspection: it adds no message, no turn, and no tool call.
293 PreviewOutboundRequest {
294 inputs: Box<crate::core::engine::preview::PreviewRequestInputs>,
295 /// Render the manifest as JSON instead of the human-readable table.
296 json: bool,
297 /// Explicit disclosure of the base prompt only; effective system text
298 /// remains protected behind hashes.
299 base_prompt_only: bool,
300 },
301
302 /// List current sub-agents and their status
303 ListSubAgents,
304
305 /// Inspect child settlement at the Engine's idle operation boundary.
306 /// This does not drain the completion inbox or dispatch a second loop.
307 GetSubAgentSettlement {
308 tx: std::sync::Arc<
309 std::sync::Mutex<Option<tokio::sync::oneshot::Sender<SubAgentSettlement>>>,
310 >,
311 },
312
313 /// Cancel a running sub-agent by id or session name.
314 CancelSubAgent { agent_id: String },
315
316 /// Deliver an operator follow-up to one child on its own fork: live
317 /// delivery to a running child, or a checkpoint continuation (new agent
318 /// id) for an interrupted or completed child. Terminal failed/cancelled
319 /// children answer with a receipt explaining why they cannot continue.
320 FollowUpSubAgent { agent_id: String, text: String },
321
322 /// Change the operating mode
323 ChangeMode {
324 mode: AppMode,
325 allow_shell: bool,
326 trust_mode: bool,
327 auto_approve: bool,
328 approval_mode: ApprovalMode,
329 configured_sandbox_mode: Option<String>,
330 },
331
332 /// Update the model being used and refresh stable prompt context.
333 SetModel {
334 model: String,
335 mode: AppMode,
336 route_limits: Option<codewhale_config::route::RouteLimits>,
337 },
338
339 /// Update auto-compaction settings
340 SetCompaction { config: CompactionConfig },
341
342 /// Update the SSE idle timeout used for subsequent streamed turns.
343 SetStreamChunkTimeout { timeout_secs: u64 },
344
345 /// Update sub-agent runtime controls for subsequent turns.
346 SetSubagentRuntimeConfig {
347 enabled: bool,
348 max_subagents: usize,
349 launch_concurrency: usize,
350 max_spawn_depth: u32,
351 api_timeout_secs: u64,
352 heartbeat_timeout_secs: u64,
353 },
354
355 /// Update the web-search backend for subsequent tool calls.
356 SetSearchProvider {
357 provider: crate::config::SearchProvider,
358 },
359
360 /// Replace the engine's merged Fleet roster after the setup wizard saves a
361 /// project or personal profile. Subsequent turns can use the new role
362 /// immediately instead of requiring an application restart.
363 SetFleetRoster {
364 roster: std::sync::Arc<crate::fleet::roster::FleetRoster>,
365 },
366
367 /// Sync engine session state (used for resume/load)
368 SyncSession {
369 session_id: Option<String>,
370 messages: Vec<Message>,
371 system_prompt: Option<SystemPrompt>,
372 system_prompt_override: bool,
373 model: String,
374 workspace: PathBuf,
375 mode: AppMode,
376 },
377
378 /// Rewind only the exact conversation observed by the caller. The Engine
379 /// compares the full expected state before changing history or caches.
380 /// A rejected rewind returns None and performs no mutation or inference.
381 RewindConversation {
382 expected: Box<SessionSnapshot>,
383 messages: Vec<Message>,
384 tx: tokio::sync::oneshot::Sender<Option<SessionSnapshot>>,
385 },
386
387 /// Run context compaction on one exact, structurally resolved provider
388 /// route with policy derived from that same descriptor.
389 CompactContext {
390 /// Stable request identity allocated before the operation enters the
391 /// bounded mailbox. Cancellation uses this id even when the provider
392 /// future has not started yet.
393 id: String,
394 route: Box<ResolvedRuntimeRoute>,
395 compaction: Box<CompactionConfig>,
396 },
397
398 /// Cancel one exact queued or running context-compaction request.
399 CancelCompaction { id: String },
400
401 /// Get a snapshot of the current session state (messages, tokens, etc.)
402 /// for saving to disk. Returns the result via the oneshot sender so
403 /// the caller doesn't have to compete with the SSE event stream.
404 GetSessionSnapshot {
405 tx: std::sync::Arc<std::sync::Mutex<Option<tokio::sync::oneshot::Sender<SessionSnapshot>>>>,
406 },
407
408 /// Get the live context-window budget for this session's route. Computed
409 /// on the engine so `active_route_limits`, the memoized token estimate,
410 /// and the last billed prompt size all come from one authority.
411 GetContextBudget {
412 tx: std::sync::Arc<
413 std::sync::Mutex<Option<tokio::sync::oneshot::Sender<Option<SessionContextBudget>>>>,
414 >,
415 },
416
417 /// Get active provider request concurrency state for readiness surfaces.
418 GetProviderRuntimeStatus {
419 tx: std::sync::Arc<
420 std::sync::Mutex<Option<tokio::sync::oneshot::Sender<ProviderRuntimeStatus>>>,
421 >,
422 },
423
424 /// Populate the engine-owned MCP pool once at UI boot and return a
425 /// snapshot from that exact pool. This is not a config reload and never
426 /// constructs a UI-owned discovery pool. Optional servers never block
427 /// the first model turn: that turn snapshots currently-ready tools.
428 BootstrapMcp {
429 tx: std::sync::Arc<
430 std::sync::Mutex<Option<tokio::sync::oneshot::Sender<McpBootstrapResult>>>,
431 >,
432 },
433
434 /// Retry one failed MCP server on the existing engine pool and return a
435 /// full snapshot. Ready siblings are never invalidated or reconnected.
436 RetryMcpServer {
437 name: String,
438 tx: std::sync::Arc<
439 std::sync::Mutex<Option<tokio::sync::oneshot::Sender<McpBootstrapResult>>>,
440 >,
441 },
442
443 /// Force the engine-owned MCP config/catalog to reload and reconnect.
444 /// The returned snapshot is taken from that same live pool.
445 ReloadMcp {
446 config_path: PathBuf,
447 tx: std::sync::Arc<std::sync::Mutex<Option<tokio::sync::oneshot::Sender<McpReloadResult>>>>,
448 },
449
450 /// Run agent-driven context purging.
451 PurgeContext,
452
453 /// Edit the last user message: remove the last user+assistant exchange
454 /// from the session, then re-send with the new content.
455 #[cfg_attr(not(test), expect(dead_code))]
456 EditLastTurn {
457 new_message: String,
458 /// Host-supplied correlation token, echoed on the replayed turn's
459 /// `Event::TurnStarted` (see `TurnSpec::submission_id`).
460 submission_id: Option<String>,
461 },
462
463 /// Enable or disable the background advisor watcher for this session.
464 /// When enabled, a fire-and-forget background task runs after each turn
465 /// that contained tool calls and emits an `Event::AdvisoryNote` with
466 /// concise observations. (#3982)
467 SetAdvisorEnabled { enabled: bool },
468
469 /// Shutdown the engine
470 Shutdown,
471 }
472
472 lines RUST