5 /// Run turns until no more tool calls
6 /// Maximum number of context-limit compaction retries before giving up.
7 pub(super) const MAX_CONTEXT_LIMIT_RETRIES: u32 = 5;
8 pub(super) const MAX_INCOMPLETE_CONTINUATION_ATTEMPTS: u32 = 3;
9 /// Retries allowed when the provider returns an empty response right after
10 /// tool results. This is a transient provider hiccup, not a signal that the
11 /// task is finished, so a single retry is too few: one empty response
12 /// observed once in 43 turns silently ended a 20-hour benchmark run with the
13 /// task half-done. The counter is per turn-loop, so a genuinely finished
14 /// agent still exits promptly.
15 pub(crate) const MAX_EMPTY_POST_TOOL_CONTINUATION_ATTEMPTS: u32 = 5;
75 /// Reasoning-effort sentinel for the **deep task graph** mode: strongest model
76 /// reasoning AND the comprehensive DAG-first swarm workflow (decompose into a
77 /// validated task graph, critique/verify gates, typed artifact handoffs). Sits
78 /// one rung above [`SWARM_EFFORT`] on the effort ladder: `... xhigh`, `swarm`
79 /// (light fan-out), `swarm-deep` (deep task graph). Providers translate this to
80 /// their strongest real effort, while the UI/session keep the literal marker so
81 /// the agent knows to inject [`SWARM_DEEP_EFFORT_DIRECTIVE`].
82 pub const SWARM_DEEP_EFFORT: &str = "swarm-deep";
83
84 /// System-prompt directive injected when the active reasoning effort is
85 /// [`SWARM_EFFORT`]. Instructs the agent to lean on the swarm tooling.
86 pub const SWARM_EFFORT_DIRECTIVE: &str = "# Swarm Effort\n\nYou are running at the maximum reasoning effort with swarm orchestration enabled. For any non-trivial task, decompose the work and use the `swarm` tool to spawn and coordinate parallel agents (spawn workers with concrete prompts, assign tasks, and collect their reports) instead of doing everything yourself in one thread. Prefer parallelizing independent subtasks across swarm members, and use a coordinator/plan when the work has multiple stages. Only skip the swarm for trivial, single-step requests.";
87
88 /// System-prompt directive injected when the active reasoning effort is
89 /// [`SWARM_DEEP_EFFORT`]. Instructs the agent to run the comprehensive DAG-first
90 /// task-graph workflow.
91 pub const SWARM_DEEP_EFFORT_DIRECTIVE: &str = "# Deep Task Graph\n\nYou are running at maximum reasoning effort with the deep task-graph swarm workflow. Treat the task DAG as the primary object, not ad hoc agent chat. Workflow:\n\n1. Seed a graph with `swarm task_graph` using `mode: \"deep\"`: lay out nodes (kind explore|implement|verify|fix|synthesize) and `depends_on` edges instead of answering directly. (At this effort the server already defaults the plan to deep, but pass `mode: \"deep\"` explicitly anyway.) The engine auto-inserts a plan-wide root gate over your seed: the plan cannot finish until a final adversarial audit passes, and that audit can inject new top-level work.\n2. For any node that is too big, `swarm expand_node` to decompose it into a child sub-DAG (you become its planner/integrator). In deep mode a critique/verify gate is auto-inserted before a composite node can close. The graph is EXPECTED to outgrow its seed, often by several times: growth (expansions and gate-injected gaps) is the system working, not scope creep. plan_status reports seeded-vs-grown counts.\n3. Finish each node with `swarm complete_node` and a typed artifact: `findings`, `evidence` (file:line / commit refs), `validation`, `open_questions`, a required `confidence` (low|medium|high; report low honestly, it routes follow-up work to shore up that scope), and an honest `what_i_did_not_check`. Downstream nodes are hydrated with these artifacts automatically. There is no other way to close a deep node: a turn ending without expand_node/complete_node re-queues the node to a fresh worker and fails it on repeat.\n4. When a critique/verify gate finds gaps or failures, use `swarm inject_gap` to add new nodes; the parent cannot close until they drain. A passing gate artifact must account for EVERY node it audited by id (the server rejects rubber stamps), and cannot pass over a low-confidence sibling without addressing it explicitly, so treat low-confidence siblings as priority probe targets.\n5. Use `swarm run_plan` to drive the graph to completion. It returns immediately and drives the plan as a background task (progress card + wake on completion), so keep working or answer the user while it runs; check `swarm plan_status` or `bg` for progress. Deep mode fans out wide automatically (many workers run in parallel, bounded only by the swarm member cap), so prefer decomposing into MANY independent sibling nodes rather than a few serial ones: keep the ready set wide so run_plan can dispatch lots of agents at once. Only add `depends_on` edges for real data dependencies.\n\nComprehensiveness is structural: prefer decomposition + gates over a single thorough answer, so it is very unlikely any nook or cranny is missed.";
328 /// MultiProvider wraps multiple providers and allows seamless model switching
329 pub struct MultiProvider {
330 /// Claude Code CLI provider
331 claude: RwLock<Option<Arc<dyn Provider>>>,
332 /// Direct Anthropic API provider (no Python dependency)
333 anthropic: RwLock<Option<Arc<dyn Provider>>>,
334 openai: RwLock<Option<Arc<dyn Provider>>>,
335 /// GitHub Copilot API provider (direct API, hot-swappable after login).
336 /// Held as `dyn Provider`: the concrete runtime lives downstream in
337 /// `jcode-provider-copilot-runtime` and is instantiated through
338 /// `external::instantiate_external_provider`.
339 copilot_api: RwLock<Option<Arc<dyn Provider>>>,
340 /// Antigravity provider (direct HTTPS, hot-swappable after login). Held as
341 /// `dyn Provider`: the concrete runtime lives downstream in
342 /// `jcode-provider-antigravity-runtime` and is instantiated through
343 /// `external::instantiate_external_provider`.
344 antigravity: RwLock<Option<Arc<dyn Provider>>>,
345 /// Gemini provider (hot-swappable after login). Held as `dyn Provider`:
346 /// the concrete runtime lives downstream in `jcode-provider-gemini-runtime`
347 /// and is instantiated through `external::instantiate_external_provider`.
348 gemini: RwLock<Option<Arc<dyn Provider>>>,
349 /// Cursor provider (native/direct API, hot-swappable after login). Held as
350 /// `dyn Provider`: the concrete runtime lives downstream in
351 /// `jcode-provider-cursor-runtime` and is instantiated through
352 /// `external::instantiate_external_provider`.
… 11 lines omitted; exact range 328–374 …
364 openai_compatible_profiles: RwLock<HashMap<String, Arc<dyn Provider>>>,
365 active_openai_compatible_profile: RwLock<Option<String>>,
366 active: RwLock<ActiveProvider>,
367 /// Use Claude CLI instead of direct API (legacy mode)
368 use_claude_cli: bool,
369 /// Notifications generated during provider/account auto-selection.
370 /// The TUI should drain and display these on session start.
371 startup_notices: RwLock<Vec<String>>,
372 /// CLI/environment selection to use when creating fresh sessions. This is
373 /// only an initial preference and never restricts later model switches.
374 initial_provider: Option<ActiveProvider>,
128 /// Manages background compaction of conversation context.
129 ///
130 /// Does NOT own message data. The caller owns the messages and passes
131 /// references into methods that need them. After compaction, the manager
132 /// records `compacted_count` — the number of leading messages that have
133 /// been summarized and should be skipped when building API payloads.
134 pub struct CompactionManager {
135 /// Number of leading messages that have been compacted into the summary.
136 /// When building API messages, skip the first `compacted_count` messages.
137 compacted_count: usize,
138
139 /// Active summary (if we've compacted before)
140 active_summary: Option<Summary>,
141
142 /// Rolling char estimate for the active (non-compacted) message suffix.
143 ///
144 /// In the common append-only case this is maintained incrementally, so token
145 /// estimation does not need to rescan the entire active history every time.
146 /// Bundled with its own dirty flag so the value and staleness can never
147 /// drift apart (see [`ActiveCharEstimate`]).
148 active_chars: ActiveCharEstimate,
149
150 /// Background compaction task handle
151 pending_task: Option<JoinHandle<Result<CompactionResult>>>,
152
… 42 lines omitted; exact range 128–205 …
195 /// of that turn (truncated to EMBED_MAX_CHARS_PER_MSG for speed).
196 embedding_history: VecDeque<Vec<f32>>,
197
198 /// Local cache for semantic compaction embeddings keyed by truncated-text hash.
199 /// Stores both successful embeddings and failed lookups (`None`) so repeated
200 /// semantic scans do not redo the same work.
201 semantic_embed_cache: HashMap<u64, (Option<Vec<f32>>, u64)>,
202
203 /// Monotonic recency counter for the semantic embedding cache LRU.
204 semantic_embed_cache_counter: u64,
205 }
1 //! Persistent Memory Agent
2 //!
3 //! A dedicated Haiku-powered agent for memory management that runs alongside
4 //! the main agent. It has access to memory-specific tools only (no code execution).
5 //!
6 //! Architecture:
7 //! - Receives context updates from main agent via channel
8 //! - Uses embeddings for fast similarity search
9 //! - Uses Haiku LLM to decide what's relevant and dig deeper
10 //! - Surfaces relevant memories to main agent via PENDING_MEMORY
11
12 use anyhow::Result;
13 use chrono::Utc;
14 use std::collections::{HashMap, HashSet};
15 use std::sync::Arc;
16 use std::sync::Mutex;
17 use std::sync::atomic::{AtomicU64, Ordering};
18 use std::time::Instant;
19 use tokio::sync::mpsc;
20
21 use crate::embedding;
22 use crate::memory::{self, MemoryEntry, MemoryManager};
23 use crate::memory_graph::{ClusterEntry, EdgeKind, MemoryGraph};
24 use crate::memory_types::{MemoryEventKind, MemoryState, StepResult, StepStatus};
25 use crate::sidecar::Sidecar;
… 9 lines omitted; exact range 1–45 …
35 context_snippet: String,
36 }
37
38 /// Channel capacity for context updates
39 const CONTEXT_CHANNEL_CAPACITY: usize = 16;
40
41 /// Similarity threshold for topic change detection (lower = more different)
42 const TOPIC_CHANGE_THRESHOLD: f32 = 0.3;
43
44 /// Maximum memories to surface per turn
45 const MAX_MEMORIES_PER_TURN: usize = 5;
159 trait MemoryEntryEmbeddingExt {
160 fn ensure_embedding(&mut self) -> bool;
161 }
162
163 impl MemoryEntryEmbeddingExt for MemoryEntry {
164 /// Generate and set embedding if not already present.
165 /// Returns true if embedding was generated, false if already exists or failed.
166 fn ensure_embedding(&mut self) -> bool {
167 if self.embedding.is_some() {
168 return false;
169 }
170
171 match crate::embedding_backend::embed_passage_active(&self.content) {
172 Ok((embedding, model_id)) => {
173 // Tag with the ACTIVE backend's model id so dense search only
174 // compares vectors from the same model/vector space. Untagged
175 // legacy memories are treated as local MiniLM via
176 // effective_embedding_model().
177 self.set_embedding(Some(embedding), Some(model_id));
178 true
179 }
180 Err(err) => {
181 crate::logging::info(&format!("Failed to generate embedding: {err}"));
182 false
183 }
184 }
185 }
1 //! The destructive-command gate for the `bash` tool (issue #604).
2 //!
3 //! Kept in its own file so the policy seam is easy to find and review: this is
4 //! the only thing standing between a model's `rm -rf` and the user's data.
5
6 /// Apply the deterministic destructive-command gate, returning refusal text
7 /// when the command must not run as-issued.
8 ///
9 /// Stage 1 is a pure blast-radius assessment; stage 2 turns a `Confirm` verdict
10 /// into a reflection prompt that a blind retry cannot satisfy. Catastrophic
11 /// targets (`/`, `$HOME`, credential stores, device nodes) are denied outright.
12 /// See issue #604.
13 pub(super) fn destructive_command_refusal(
14 command: &str,
15 justification: Option<&str>,
16 working_dir: Option<std::path::PathBuf>,
17 ) -> Option<String> {
18 let risk_ctx = jcode_command_risk::RiskContext::from_env(working_dir);
19 let assessment = jcode_command_risk::assess(command, &risk_ctx);
20 if assessment.level.runs_immediately() {
21 return None;
22 }
23
24 let justification = jcode_command_risk::Justification {
25 text: justification.map(str::to_string),
… 3 lines omitted; exact range 1–39 …
29 jcode_command_risk::GateOutcome::Deny { reason } => {
30 crate::logging::warn(&format!("[bash] denied destructive command: {command}"));
31 Some(reason)
32 }
33 jcode_command_risk::GateOutcome::Reflect { prompt } => {
34 crate::logging::info(&format!(
35 "[bash] destructive command held for justification: {command}"
36 ));
37 Some(prompt)
38 }
39 }
1 //! MCP Manager - manages MCP server connections for a single session.
2 //!
3 //! In daemon mode with a shared pool, servers marked `shared: true` (the default)
4 //! are managed by the pool and reused across sessions. Servers marked `shared: false`
5 //! (e.g., Playwright with browser state) are spawned per-session.
6
7 use super::client::{McpClient, McpHandle};
8 use super::pool::SharedMcpPool;
9 use super::protocol::{McpConfig, McpServerConfig, McpToolDef, ToolCallResult};
10 use anyhow::{Context, Result};
11 use serde::Serialize;
12 use std::collections::HashMap;
13 use std::sync::Arc;
14 use tokio::sync::RwLock;
15
16 /// Bound on how long a tool call will wait for a not-yet-connected MCP server
17 /// to come up before failing with a clean tool error. Keeps a slow/hanging
18 /// server from blocking a single tool call forever (and never blocks spawn).
19 const CONNECT_ON_CALL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
20
21 /// Meter a completed tool call for partner-discovery provenance. No-op for
22 /// servers without discovery provenance (the overwhelmingly common case) and
23 /// whenever `sponsors.enabled` is false. Counts only; never content.
24 fn meter_provenance_call(server: &str, result: &Result<ToolCallResult>) {
25 let is_error = match result {
… 23 lines omitted; exact range 1–59 …
49 pool: Option<Arc<SharedMcpPool>>,
50 /// Handles from the shared pool (shared servers)
51 pool_handles: RwLock<HashMap<String, McpHandle>>,
52 /// Per-session owned clients (non-shared / stateful servers)
53 owned_clients: RwLock<HashMap<String, McpClient>>,
54 config: McpConfig,
55 session_id: String,
56 /// Project directory used to resolve project-local MCP config. `None`
57 /// loads only global config and never consults the process working directory.
58 project_dir: Option<std::path::PathBuf>,
59 }
335 pub(super) async fn tool_definitions(&mut self) -> Vec<ToolDefinition> {
336 if self.session.is_canary {
337 self.registry.register_selfdev_tools().await;
338 }
339
340 // Return locked tools if available (prevents cache invalidation from
341 // tools arriving asynchronously after the first API request).
342 //
343 // Exception: MCP servers connect on a background task and register
344 // `mcp__*` tools seconds after the session starts — typically *after*
345 // the first turn has already locked the snapshot. We deliberately do
346 // NOT block the first turn on MCP connection: servers can be slow or
347 // hang, and we want the user to be able to talk to the agent the moment
348 // the session spawns. The price is that the first locked snapshot is
349 // missing MCP tools, and the only other unlock path fires when the model
350 // calls the `mcp` management tool — which it cannot do without first
351 // seeing MCP tools (#206).
352 //
353 // So, exactly once per locked snapshot, if MCP tools have since appeared
354 // in the registry, we rebuild. This is a single intentional provider
355 // prompt-cache miss (the turn MCP tools first appear). The
356 // `mcp_late_register_resolved` flag makes this a one-shot check so we do
357 // not rescan the registry on every subsequent turn.
358 if let Some(ref locked) = self.locked_tools {
359 if self.mcp_late_register_resolved {
… 23 lines omitted; exact range 335–393 …
383
384 let tools = self.build_filtered_tool_definitions().await;
385
386 // Lock the tool list to prevent cache invalidation when more tools
387 // arrive asynchronously mid-session.
388 logging::info(&format!(
389 "Locking tool list at {} tools for cache stability",
390 tools.len()
391 ));
392 self.locked_tools = Some(tools.clone());
393 tools
26 /// Attempt to recover complete entries from a journal line that failed the
27 /// strict one-entry-per-line parse.
28 ///
29 /// If a writer died mid-append (torn line without a trailing newline), the
30 /// next successful append starts writing on the same line, producing
31 /// `<torn json><complete entry json>\n` or `<entry json><entry json>\n`.
32 /// Serialized entries always begin with `{"meta":` (struct field order), so
33 /// scan for candidate starts and stream-parse consecutive complete entries
34 /// from the first position that yields any.
35 fn salvage_glued_journal_entries(line: &str, mut apply: impl FnMut(SessionJournalEntry)) -> usize {
36 const ENTRY_START: &str = "{\"meta\":";
37 let mut salvaged = 0usize;
38 let mut search_from = 0usize;
39 while let Some(rel) = line
40 .get(search_from..)
41 .and_then(|rest| rest.find(ENTRY_START))
42 {
43 let candidate_start = search_from + rel;
44 let mut stream = serde_json::Deserializer::from_str(&line[candidate_start..])
45 .into_iter::<SessionJournalEntry>();
46 let mut parsed = Vec::new();
47 for item in &mut stream {
48 match item {
49 Ok(entry) => parsed.push(entry),
50 Err(_) => break,
… 67 lines omitted; exact range 26–128 …
118 vec![
119 ("phase", "journal_replay_corruption".to_string()),
120 ("path", journal_path.display().to_string()),
121 ("entries_replayed", stats.entries.to_string()),
122 ("lines_skipped", stats.skipped_lines.to_string()),
123 ("entries_salvaged", stats.salvaged_entries.to_string()),
124 ],
125 );
126 }
127
128 Ok(stats)