Harness · Coding Agent Book13 / JCode
研究总览
M13 · SOURCE-GROUNDED TUTORIAL

JCode
从源码学会它怎么工作

恢复、memory 与 swarm 设计非常激进;安全边界偏应用层,默认没有 OS 沙箱。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。

TypeScript · Recoverable Memory + Swarm AgentMIT119301ac9d6030 个结论 · 78 处引用
这门课怎么读

先建立直觉,再沿一条任务链钻进代码

参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 JCode 的 30 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。

你会得到一张可复述的架构地图一次完整任务的链路追踪能迁移到自研 Harness 的设计判断
你不会得到把 README 功能当成已验证事实把 prompt 约束说成 OS 沙箱把一次双模型调用夸成多 Agent 平台
M00 · MAP

先看全景:这个 Agent 的控制面在哪里

下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。

架构图全屏打开 ↗
一轮执行链路全屏打开 ↗
核心机制

先写盘;断流撤销半截再重播;多类止损上限

上下文

prefix/tail 缓存;cursor compact;三策略压缩;后台 memory

适用建设

研究型自研 Harness、复杂 swarm、长期记忆

M00.5 · TRACE

跟踪一个任务:从输入到交付

把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。

读图提醒

箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。

M01 · ORIENTATION

先把 Agent 看成一台会交付的机器

如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。

先用一个生活比喻

把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 1

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M02 · LOOP

主循环:模型为什么会继续动

一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?

先用一个生活比喻

像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。

这套实现先回答了什么?

模型还没开口,用户输入已经落账;即使后面 API 或工具出错,恢复时也不会连问题本身都丢掉。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
每个用户 turn 先写盘,再进入可恢复的流式循环crates/jcode-app-core/src/agent/turn_execution.rs:4模型还没开口,用户输入已经落账;即使后面 API 或工具出错,恢复时也不会连问题本身都丢掉。
循环在每次请求前修复工具配对并重建稳定快照crates/jcode-app-core/src/agent/turn_loops.rs:17每次再问模型前先查账:工具有没有开单不回执、旧历史是否已折叠、这一轮工具箱和提示词是否稳定。
中途断流先撤销半截状态再完整重播crates/jcode-app-core/src/agent/turn_loops.rs:455网络半路断了,不把前半句和前半个工具调用留在账上;先橡皮擦掉,再从头重放。
上下文、截断回复和工具后空回复各有独立止损上限crates/jcode-app-core/src/agent/turn_loops.rs:5不同故障不共用一个模糊的“最多重试 N 次”:箱子装不下、回答被截断、工具后失语分别计数。
01
L1 · fact · jcode-loop-001

每个用户 turn 先写盘,再进入可恢复的流式循环

先看源码事实

run_once/run_once_capture 先把用户消息加入 Session 并 save,再调用 run_turn;远程流入口还先注入跨 Agent 通知、图片和本轮 system reminder,并在 turn 前后触发 observer hooks。

翻译成白话

模型还没开口,用户输入已经落账;即使后面 API 或工具出错,恢复时也不会连问题本身都丢掉。

为什么这对自研重要

耐崩溃性强,但每轮和工具结果频繁保存会增加本地 I/O,需要 journal 快路径配合。

固定提交源码摘录
    4  impl Agent {
    5      /// Run a single turn with the given user message
    6      pub async fn run_once(&mut self, user_message: &str) -> Result<()> {
    7          self.add_message(
    8              Role::User,
    9              vec![ContentBlock::Text {
   10                  text: user_message.to_string(),
   11                  cache_control: None,
   12              }],
   13          );
   14          self.session.save()?;
   15          if trace_enabled() {
   16              eprintln!("[trace] session_id {}", self.session.id);
   17          }
   18          let _ = self.run_turn(true).await?;
   19          Ok(())
   20      }
   21  
   22      pub async fn run_once_capture(&mut self, user_message: &str) -> Result<String> {
   23          self.add_message(
   24              Role::User,
   25              vec![ContentBlock::Text {
   26                  text: user_message.to_string(),
   27                  cache_control: None,
   28              }],
   29          );
   30          self.session.save()?;
   31          if trace_enabled() {
   32              eprintln!("[trace] session_id {}", self.session.id);
   33          }
   34          self.run_turn(false).await
   35      }
为什么相信这条结论?查看 2 处证据
02
L1 · fact · jcode-loop-002

循环在每次请求前修复工具配对并重建稳定快照

先看源码事实

run_turn 注册 streaming/cancel guard;每圈先补齐缺失 tool outputs,再取得可能已压缩的 provider messages,锁定 tool definitions,取上一轮准备好的 memory,并构造静态/动态分离的 prompt。

翻译成白话

每次再问模型前先查账:工具有没有开单不回执、旧历史是否已折叠、这一轮工具箱和提示词是否稳定。

为什么这对自研重要

把异常历史修复、缓存稳定和请求生命周期放在一个明确关口。

固定提交源码摘录
   17      pub(super) async fn run_turn(&mut self, print_output: bool) -> Result<String> {
   18          self.set_log_context();
   19          crate::session_metrics::record_turn(&self.session.id);
   20          // Mark this session as actively streaming for presence UIs (e.g. the
   21          // macOS menu bar indicator). Cleared automatically on every exit path.
   22          let _streaming_guard = crate::session::StreamingGuard::new(self.session.id.clone());
   23          // Register this turn's cancel signal so session-level cancels reach
   24          // this in-flight turn even through stale control handles (issue #428).
   25          let _turn_cancel_guard = crate::turn_cancel_registry::register_active_turn(
   26              &self.session.id,
   27              self.graceful_shutdown.clone(),
   28          );
   29          let mut final_text = String::new();
   30          let trace = trace_enabled();
   31          let mut context_limit_retries = 0u32;
   32          let mut incomplete_continuations = 0u32;
   33          let mut empty_post_tool_continuations = 0u32;
   34  
   35          loop {
   36              let repaired = self.repair_missing_tool_outputs();
   37              if repaired > 0 {
   38                  logging::warn(&format!(
   39                      "Recovered {} missing tool output(s) before API call",
   40                      repaired
   41                  ));
      … 16 lines omitted; exact range 17–68 …
   58                  }
   59              }
   60  
   61              let tools = self.tool_definitions().await;
   62              let messages: std::sync::Arc<[Message]> = messages.into();
   63              // Non-blocking memory: uses pending result from last turn, spawns check for next turn
   64              let memory_pending =
   65                  self.build_memory_prompt_nonblocking_shared(std::sync::Arc::clone(&messages), None);
   66              // Use split prompt for better caching - static content cached, dynamic not
   67              let split_prompt = self.build_system_prompt_split(None);
   68              self.log_prompt_prefix_accounting(&split_prompt, &tools);
为什么相信这条结论?查看 1 处证据
03
L1 · fact · jcode-loop-003

中途断流先撤销半截状态再完整重播

先看源码事实

Provider 发出 RetryRollback 后,Agent 清空该次尝试的文本、tool calls、SDK results、reasoning 与 native compaction;Copilot runtime 仅在已向消费者发过可见输出且错误可重试时发送该事件。

翻译成白话

网络半路断了,不把前半句和前半个工具调用留在账上;先橡皮擦掉,再从头重放。

为什么这对自研重要

显著降低重试导致的重复文本和重复副作用;普通 stdout 无法擦除,只会显示断点标记。

固定提交源码摘录
  455                      StreamEvent::RetryRollback { attempt, max } => {
  456                          // Transient transport fault mid-stream; the provider is
  457                          // replaying the request. Discard this attempt's partial
  458                          // output so the replay doesn't duplicate it in history.
  459                          logging::warn(&format!(
  460                              "Mid-stream retry rollback (attempt {}/{}): discarding partial output ({} text chars, {} tool calls)",
  461                              attempt,
  462                              max,
  463                              text_content.len(),
  464                              tool_calls.len(),
  465                          ));
  466                          if print_output && !text_content.is_empty() {
  467                              // Already-printed text can't be unprinted on a plain
  468                              // stdout stream; mark the discontinuity instead.
  469                              println!("\n[connection interrupted, retrying response from the top]");
  470                              io::stdout().flush()?;
  471                          }
  472                          text_content.clear();
  473                          tool_calls.clear();
  474                          current_tool = None;
  475                          current_tool_input.clear();
  476                          sdk_tool_results.clear();
  477                          generated_image_contexts.clear();
  478                          reasoning_content.clear();
  479                          reasoning_signature.clear();
  480                          openai_reasoning_items.clear();
  481                          openai_native_compaction = None;
  482                          saw_message_end = false;
  483                          stop_reason = None;
  484                      }
为什么相信这条结论?查看 3 处证据
04
L1 · fact · jcode-loop-004

上下文、截断回复和工具后空回复各有独立止损上限

先看源码事实

上下文自动压缩重试最多 5 次,不完整回复 continuation 最多 3 次,工具结果后的空白 final 最多 5 次;工具后空白会写入一条明确要求给 final answer 的 user message。

翻译成白话

不同故障不共用一个模糊的“最多重试 N 次”:箱子装不下、回答被截断、工具后失语分别计数。

为什么这对自研重要

长任务韧性好,也避免无限循环;自动补写的 user message会成为真实历史。

固定提交源码摘录
    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;
为什么相信这条结论?查看 2 处证据
05
L1 · fact · jcode-collab-001

轻量 swarm 先让协调者规划 2–4 个任务,再并发 fork Provider

先看源码事实

run_swarm_message 用当前 Agent 生成 JSON task plan,对每项创建 child Session、fork provider、继承模型/认证/cwd,移除 subagent/task/todo 等递归工具,并用 try_join_all 并发;最终把所有输出交给协调者综合。

翻译成白话

先拆成几张独立工单,让多个复制了同一模型线路的工人同时做,最后由原会话汇总。

为什么这对自研重要

简单易用;worker 共享 registry 与工作目录,文件级冲突仍需任务切分和外部 git/worktree 纪律。

固定提交源码摘录
 1528  pub(super) async fn run_swarm_task(
 1529      agent: Arc<Mutex<Agent>>,
 1530      description: &str,
 1531      subagent_type: &str,
 1532      prompt: &str,
 1533  ) -> Result<String> {
 1534      let started = Instant::now();
 1535      let (provider, registry, session_id, working_dir, coordinator_model, provider_key, route) = {
 1536          let agent = agent.lock().await;
 1537          (
 1538              agent.provider_fork(),
 1539              agent.registry(),
 1540              agent.session_id().to_string(),
 1541              agent.working_dir().map(PathBuf::from),
 1542              agent.provider_model(),
 1543              agent.session_provider_key(),
 1544              agent.session_route_api_method(),
 1545          )
 1546      };
 1547      let parent_session_id = session_id.clone();
 1548      let mut session = Session::create(
 1549          Some(session_id),
 1550          Some(format!("{} (@{} swarm)", description, subagent_type)),
 1551      );
 1552      let child_session_id = session.id.clone();
      … 50 lines omitted; exact range 1528–1613 …
 1603                      ("parent_session_id", parent_session_id),
 1604                      ("child_session_id", child_session_id),
 1605                      ("subagent_type", subagent_type.to_string()),
 1606                      ("error", error.to_string()),
 1607                      ("elapsed_ms", started.elapsed().as_millis().to_string()),
 1608                  ],
 1609              );
 1610              Err(error)
 1611          }
 1612      }
 1613  }
为什么相信这条结论?查看 2 处证据
06
L1 · fact · jcode-collab-002

deep swarm 把协作升级成可增长 DAG、强制 artifact 与审计 gate

先看源码事实

swarm-deep 动态指令要求 task_graph、depends_on、expand_node、complete_node typed artifact、critique/verify gate 与 inject_gap;worker 未 expand/complete 会重排队,gate 必须按 audited node id 覆盖范围并优先处理低置信 artifact。

翻译成白话

不是“多叫几个人聊天”,而是把任务画成图:每个节点要交证据包,审计员可以发现缺口后现场加新节点,图没验完就不能宣布完成。

为什么这对自研重要

非常接近研究/工程编排 Harness;复杂度、成本和并发写冲突风险也显著提高。

固定提交源码摘录
   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.";
为什么相信这条结论?查看 4 处证据
07
L1 · fact · jcode-collab-003

Swarm 有持久成员树、频道、heartbeat 和死亡任务回收

先看源码事实

SwarmMemberRecord 保存 parent/report-back、角色、状态、working_dir 和 report;ChannelIndex 双向索引订阅。worker 死亡时非终态任务按 reclaim cap 重新排队,超上限转 failed,并持久化/broadcast/通知协调者。

翻译成白话

每个工人有户口、上级、频道和心跳;工人挂了,手里的活不会静默卡死,会有限次数重新派发,反复致命才明确失败。

为什么这对自研重要

协作是耐久调度系统而非内存 future;需要持续治理 member cap、staleness 和 replay idempotency。

固定提交源码摘录
  213  /// Durable, persistable portion of a swarm member.
  214  #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
  215  pub struct SwarmMemberRecord {
  216      pub session_id: String,
  217      pub working_dir: Option<PathBuf>,
  218      pub swarm_id: Option<String>,
  219      pub swarm_enabled: bool,
  220      pub status: SwarmLifecycleStatus,
  221      pub detail: Option<String>,
  222      /// Stable label of the task/role this member was spawned or assigned for.
  223      #[serde(default, skip_serializing_if = "Option::is_none")]
  224      pub task_label: Option<String>,
  225      pub friendly_name: Option<String>,
  226      pub report_back_to_session_id: Option<String>,
  227      #[serde(default, skip_serializing_if = "Option::is_none")]
  228      pub latest_completion_report: Option<String>,
  229      pub role: SwarmRole,
  230      pub is_headless: bool,
  231  }
  232  
  233  /// Bidirectional index for swarm channel subscriptions.
  234  #[derive(Clone, Debug, Default, PartialEq, Eq)]
  235  pub struct ChannelIndex {
  236      pub by_swarm_channel: HashMap<String, HashMap<String, HashSet<String>>>,
  237      pub by_session: HashMap<String, HashMap<String, HashSet<String>>>,
      … 5 lines omitted; exact range 213–253 …
  243              .entry(swarm_id.to_string())
  244              .or_default()
  245              .entry(channel.to_string())
  246              .or_default()
  247              .insert(session_id.to_string());
  248          self.by_session
  249              .entry(session_id.to_string())
  250              .or_default()
  251              .entry(swarm_id.to_string())
  252              .or_default()
  253              .insert(channel.to_string());
为什么相信这条结论?查看 3 处证据
小练习 2

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M03 · MODEL

模型调用:流式输出如何变成可执行步骤

模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?

先用一个生活比喻

模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。

这套实现先回答了什么?

它不是只把 URL 换掉;连“谁付费、用哪条线路、能不能续上服务端会话、工具由谁执行、压缩由谁做”都在同一接口里。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
Provider 契约不仅抽象生成,还抽象认证、路由、transport 与原生能力crates/jcode-provider-core/src/lib.rs:76它不是只把 URL 换掉;连“谁付费、用哪条线路、能不能续上服务端会话、工具由谁执行、压缩由谁做”都在同一接口里。
MultiProvider 同时容纳九类 runtime 与兼容端点 profilecrates/jcode-base/src/provider/mod.rs:328像一个总机:同一 Agent 可以接订阅 CLI、官方 API、Copilot、Gemini、Bedrock 或自定义兼容网关,并保留各自认证身份。
服务端会话、原生工具和原生压缩都能穿过统一事件流crates/jcode-app-core/src/agent/turn_loops.rs:485底层 Provider 可以说“以后用这个会话号续聊”“我已经压缩过了”或“替我本地跑这个工具”,上层都能接住。
08
L2 · fact · jcode-provider-001

Provider 契约不仅抽象生成,还抽象认证、路由、transport 与原生能力

先看源码事实

Provider trait 提供 complete/complete_split、模型/route/effort/tier/transport 切换、认证解析、context window、fork、native tool result sender 和 native compaction;fork 明确要求独立可变状态。

翻译成白话

它不是只把 URL 换掉;连“谁付费、用哪条线路、能不能续上服务端会话、工具由谁执行、压缩由谁做”都在同一接口里。

为什么这对自研重要

多后端能力完整,但 trait 面积很大,新 Provider 的一致性测试成本高。

固定提交源码摘录
   76  pub trait Provider: Send + Sync {
   77      /// Send messages and get a streaming response.
   78      /// resume_session_id: Optional session ID to resume a previous conversation (provider-specific).
   79      async fn complete(
   80          &self,
   81          messages: &[Message],
   82          tools: &[ToolDefinition],
   83          system: &str,
   84          resume_session_id: Option<&str>,
   85      ) -> Result<EventStream>;
   86  
   87      /// Send messages with split system prompt for better caching.
   88      async fn complete_split(
   89          &self,
   90          messages: &[Message],
   91          tools: &[ToolDefinition],
   92          system_static: &str,
   93          system_dynamic: &str,
   94          resume_session_id: Option<&str>,
   95      ) -> Result<EventStream> {
   96          let dynamic_messages = messages_with_dynamic_system_context(messages, system_dynamic);
   97          self.complete(&dynamic_messages, tools, system_static, resume_session_id)
   98              .await
   99      }
  100  
      … 15 lines omitted; exact range 76–126 …
  116      /// selected at runtime instead of a fixed aggregator label.
  117      fn display_name(&self) -> String {
  118          self.name().to_string()
  119      }
  120  
  121      /// Get the model identifier being used.
  122      fn model(&self) -> String {
  123          "unknown".to_string()
  124      }
  125  
  126      /// Human-readable description of the auth method the active provider will
为什么相信这条结论?查看 2 处证据
09
L1 · fact · jcode-provider-002

MultiProvider 同时容纳九类 runtime 与兼容端点 profile

先看源码事实

MultiProvider 持有 Claude CLI、Anthropic API、OpenAI、Copilot、Antigravity、Gemini、Cursor、Bedrock、OpenRouter,以及独立 keyed OpenAI-compatible profiles;active provider 可热切换。

翻译成白话

像一个总机:同一 Agent 可以接订阅 CLI、官方 API、Copilot、Gemini、Bedrock 或自定义兼容网关,并保留各自认证身份。

为什么这对自研重要

模型路由覆盖面极广;OpenRouter 槽位兼容多 profile,代码中有大量防止 profile 身份丢失的分支。

固定提交源码摘录
  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>,
为什么相信这条结论?查看 1 处证据
10
L1 · fact · jcode-provider-003

服务端会话、原生工具和原生压缩都能穿过统一事件流

先看源码事实

StreamEvent::SessionId 会保存 provider resumable session;Compaction 可保存 OpenAI encrypted content;NativeToolCall 经本地 registry 执行后通过 provider native_result_sender 回传 SDK bridge。

翻译成白话

底层 Provider 可以说“以后用这个会话号续聊”“我已经压缩过了”或“替我本地跑这个工具”,上层都能接住。

为什么这对自研重要

兼容 provider-native agent runtime,而非强迫所有后端退化成纯文本 function calling。

固定提交源码摘录
  485                      StreamEvent::MessageEnd {
  486                          stop_reason: reason,
  487                      } => {
  488                          saw_message_end = true;
  489                          if reason.is_some() {
  490                              stop_reason = reason;
  491                          }
  492                          // Don't break yet - wait for SessionId which comes after MessageEnd
  493                          // (but stream close will also end the loop for providers without SessionId)
  494                      }
  495                      StreamEvent::SessionId(sid) => {
  496                          if trace {
  497                              eprintln!("[trace] session_id {}", sid);
  498                          }
  499                          self.provider_session_id = Some(sid.clone());
  500                          self.session.provider_session_id = Some(sid);
  501                          // We've received session_id, can exit the loop now
  502                          if saw_message_end {
  503                              break;
  504                          }
  505                      }
  506                      StreamEvent::UpstreamProvider { provider } => {
  507                          // Log upstream provider for local trace output
  508                          if trace {
  509                              eprintln!("[trace] upstream_provider={}", provider);
      … 27 lines omitted; exact range 485–547 …
  537                          if print_output {
  538                              let tokens_str = pre_tokens
  539                                  .map(|t| format!(" ({} tokens)", t))
  540                                  .unwrap_or_default();
  541                              crate::terminal_println!(
  542                                  "📦 Context compacted ({}){}",
  543                                  trigger,
  544                                  tokens_str
  545                              );
  546                          }
  547                      }
为什么相信这条结论?查看 2 处证据
小练习 3

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M04 · TOOLS

工具系统:Agent 的手脚怎样被注册和调度

模型看见的工具说明,和真正执行工具的代码,是不是同一个东西?并发、编辑和失败结果怎么处理?

先用一个生活比喻

工具系统像机场:模型提交登机牌,注册表确认航班,权限闸机检查证件,调度器决定跑道,最后才允许真正起飞。

这套实现先回答了什么?

每件工具都交同一种说明书和回执;“为什么调用”不是每个工具自己记得就写,而是总装配线强制补上。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
工具是 typed registry,定义顺序与 intent 字段集中标准化crates/jcode-tool-core/src/lib.rs:9每件工具都交同一种说明书和回执;“为什么调用”不是每个工具自己记得就写,而是总装配线强制补上。
普通模型工具调用串行,显式 batch 最多并发十个crates/jcode-app-core/src/agent/turn_loops.rs:878模型一次开几张普通工单时按顺序做;只有明确使用 batch 托盘,才会同时跑最多十件事。
工具输出有双预算闸门,前台命令超时可升级后台crates/jcode-app-core/src/tool/mod.rs:627一份巨型日志不能吃掉整本上下文;长命令如果只是前台等不及,会转到后台继续,而显式后台设置的超时才负责杀进程组。
11
L1 · fact · jcode-tools-001

工具是 typed registry,定义顺序与 intent 字段集中标准化

先看源码事实

Tool trait 统一 name/description/JSON Schema/async execute;to_definition 集中给所有 object schema(含 MCP proxy)加入必填 intent。Registry 内建读写编辑、shell、浏览器、web、swarm、memory、goal、gmail、schedule、自开发等工具,并按名称排序 definitions。

翻译成白话

每件工具都交同一种说明书和回执;“为什么调用”不是每个工具自己记得就写,而是总装配线强制补上。

为什么这对自研重要

便于 UI 显示与审计,稳定排序提高 prompt cache hit。

固定提交源码摘录
    9  pub const TOOL_INTENT_DESCRIPTION: &str = concat!(
   10      "Short natural-language label explaining why this tool call is being made. ",
   11      "Used for compact UI display only. Required on every call; do not use this instead of required tool parameters."
   12  );
   13  
   14  pub fn intent_schema_property() -> Value {
   15      serde_json::json!({
   16          "type": "string",
   17          "description": TOOL_INTENT_DESCRIPTION,
   18      })
   19  }
   20  
   21  /// Ensure a tool parameter schema declares the shared `intent` property and
   22  /// marks it required. Applied centrally when converting tools to provider
   23  /// definitions so every tool (including MCP proxies) asks the model for an
   24  /// intent without each tool wiring it manually.
   25  pub fn ensure_intent_in_schema(mut schema: Value) -> Value {
   26      let Some(object) = schema.as_object_mut() else {
   27          return schema;
   28      };
   29      // Only touch object-shaped parameter schemas.
   30      let is_object_schema = object
   31          .get("type")
   32          .and_then(|t| t.as_str())
   33          .map(|t| t == "object")
      … 21 lines omitted; exact range 9–65 …
   55          }
   56          _ => {
   57              object.insert(
   58                  "required".to_string(),
   59                  Value::Array(vec![Value::String("intent".to_string())]),
   60              );
   61          }
   62      }
   63  
   64      schema
   65  }
为什么相信这条结论?查看 4 处证据
12
L1 · fact · jcode-tools-002

普通模型工具调用串行,显式 batch 最多并发十个

先看源码事实

run_turn 对 tool_calls 逐个 await registry.execute 并逐条持久化结果;BatchTool 禁止递归 batch,最多 10 个 subcalls,用 FuturesUnordered 并发执行、实时发布 progress,最终恢复原调用顺序。

翻译成白话

模型一次开几张普通工单时按顺序做;只有明确使用 batch 托盘,才会同时跑最多十件事。

为什么这对自研重要

默认副作用顺序可预测;性能并行需要模型主动选择 batch。

固定提交源码摘录
  878              // Execute tools and add results
  879              let mut tool_results_dirty = false;
  880              for tc in tool_calls {
  881                  let message_id = assistant_message_id
  882                      .clone()
  883                      .unwrap_or_else(|| self.session.id.clone());
  884  
  885                  if let Some(error_msg) = tc.validation_error() {
  886                      logging::warn(&error_msg);
  887                      Bus::global().publish(BusEvent::ToolUpdated(ToolEvent {
  888                          session_id: self.session.id.clone(),
  889                          message_id: message_id.clone(),
  890                          tool_call_id: tc.id.clone(),
  891                          tool_name: tc.name.clone(),
  892                          status: ToolStatus::Error,
  893                          intent: tc.intent.clone(),
  894                          title: None,
  895                      }));
  896                      if print_output {
  897                          println!("\n  → {}", error_msg);
  898                      }
  899                      self.add_message(
  900                          Role::User,
  901                          vec![ContentBlock::ToolResult {
  902                              tool_use_id: tc.id,
      … 4 lines omitted; exact range 878–917 …
  907                      tool_results_dirty = true;
  908                      continue;
  909                  }
  910  
  911                  self.validate_tool_allowed(&tc.name)?;
  912  
  913                  let is_native_tool = JCODE_NATIVE_TOOLS.contains(&tc.name.as_str());
  914  
  915                  // Check if SDK already executed this tool
  916                  if let Some((sdk_content, sdk_is_error)) = sdk_tool_results.remove(&tc.id) {
  917                      // For native tools, ignore SDK errors and execute locally
为什么相信这条结论?查看 3 处证据
13
L1 · fact · jcode-tools-003

工具输出有双预算闸门,前台命令超时可升级后台

先看源码事实

Registry 在输出将超过 context 90% 或单个输出超过 budget 30% 时截断。Bash 默认前台 timeout 120s、上限 10 分钟;前台超时不会杀进程,而是把 JoinHandle 交给 background manager。显式后台模式按 timeout 杀整个 Unix process group 并写 output/status file。

翻译成白话

一份巨型日志不能吃掉整本上下文;长命令如果只是前台等不及,会转到后台继续,而显式后台设置的超时才负责杀进程组。

为什么这对自研重要

特别适合长 build/test;用户必须理解前台 timeout 是“停止等待”而不是“停止执行”。

固定提交源码摘录
  627          // Context overflow guard: check if this output would push us over the limit
  628          output = self.guard_context_overflow(name, output).await;
  629  
  630          let mut fields = Self::tool_lifecycle_fields("done", name, resolved_name, &input, &ctx);
  631          fields.push(("elapsed_ms".to_string(), latency_ms.to_string()));
  632          fields.push(("output_bytes".to_string(), output.output.len().to_string()));
  633          fields.push((
  634              "output_chars".to_string(),
  635              output.output.chars().count().to_string(),
  636          ));
  637          fields.push(("image_count".to_string(), output.images.len().to_string()));
  638          crate::logging::event_info("TOOL_LIFECYCLE", fields);
  639  
  640          Ok(output)
  641      }
  642  
  643      /// Check if a tool output would overflow the context window and truncate if needed.
  644      /// Returns the (possibly truncated) output.
  645      async fn guard_context_overflow(&self, tool_name: &str, output: ToolOutput) -> ToolOutput {
  646          let compaction = self.compaction.read().await;
  647          let budget = compaction.token_budget();
  648          if budget == 0 {
  649              return output;
  650          }
  651  
      … 16 lines omitted; exact range 627–678 …
  668          // Calculate how many tokens we can afford for this output
  669          let remaining = if current_tokens < threshold_tokens {
  670              threshold_tokens - current_tokens
  671          } else {
  672              // Already over threshold — allow a small amount for the error message
  673              budget / 50 // ~2% of budget for the truncation notice
  674          };
  675          let max_tokens = remaining.min(single_max_tokens);
  676  
  677          // Convert token limit back to approximate character limit
  678          let max_chars = max_tokens * 4;
为什么相信这条结论?查看 4 处证据
小练习 4

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M05 · CONTEXT

上下文:有限窗口怎样装下长任务

当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?

先用一个生活比喻

上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。

这套实现先回答了什么?

不常变的说明书放书脊,记忆和本轮提醒贴在最后一页;这样改便签不会让整本书的缓存失效。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
静态前缀与每轮动态上下文分离,memory 放尾部保缓存crates/jcode-base/src/prompt.rs:451不常变的说明书放书脊,记忆和本轮提醒贴在最后一页;这样改便签不会让整本书的缓存失效。
压缩器不复制历史,只记录被摘要的前缀游标crates/jcode-base/src/compaction.rs:128原始账本仍归 Session 管,压缩器只记“前 N 条已折叠成摘要”,不会偷偷维护第二份容易漂移的历史。
压缩支持 reactive、趋势预测和语义换题三种策略crates/jcode-base/src/compaction.rs:456可以等箱子快满再整理,也可以看增长速度提前整理,还可以在话题换挡时整理;但都会先检查“现在整理是否值得”。
上下文溢出与请求体过大走不同紧急恢复路径crates/jcode-app-core/src/agent/compaction.rs:90“字太多”“图片字节太大”“服务端压缩包本身太大”不是同一种病,分别治疗。
14
L1 · fact · jcode-context-001

静态前缀与每轮动态上下文分离,memory 放尾部保缓存

先看源码事实

system prompt 把 base prompt、AGENTS、overlay、preferred-tools、skills 放 static_part;memory、active skill、reminder 与 swarm effort 放 dynamic_part。run_turn 在 client cache 检查后把 memory 作为末尾 user message注入。

翻译成白话

不常变的说明书放书脊,记忆和本轮提醒贴在最后一页;这样改便签不会让整本书的缓存失效。

为什么这对自研重要

明显以 prompt-cache economics 为一等设计目标。

固定提交源码摘录
  451  /// Build system prompt split into static (cacheable) and dynamic parts
  452  /// This improves cache hit rate by keeping frequently-changing content separate
  453  pub fn build_system_prompt_split(
  454      skill_prompt: Option<&str>,
  455      available_skills: &[SkillInfo],
  456      is_selfdev: bool,
  457      memory_prompt: Option<&str>,
  458      working_dir: Option<&Path>,
  459  ) -> (SplitSystemPrompt, ContextInfo) {
  460      build_system_prompt_split_with_capabilities(
  461          skill_prompt,
  462          available_skills,
  463          is_selfdev,
  464          memory_prompt,
  465          working_dir,
  466          PromptCapabilities::current(),
  467      )
  468  }
  469  
  470  pub fn build_system_prompt_split_with_capabilities(
  471      skill_prompt: Option<&str>,
  472      available_skills: &[SkillInfo],
  473      is_selfdev: bool,
  474      memory_prompt: Option<&str>,
  475      working_dir: Option<&Path>,
      … 71 lines omitted; exact range 451–557 …
  547      let dynamic_part = dynamic_parts.join("\n\n");
  548      info.total_chars = static_part.len() + dynamic_part.len();
  549  
  550      (
  551          SplitSystemPrompt {
  552              static_part,
  553              dynamic_part,
  554          },
  555          info,
  556      )
  557  }
为什么相信这条结论?查看 2 处证据
15
L1 · fact · jcode-context-002

压缩器不复制历史,只记录被摘要的前缀游标

先看源码事实

CompactionManager 不拥有 messages,只存 compacted_count、active_summary、active suffix char estimate 与 pending background task;恢复旧会话时先抑制压缩,直到真正的新消息加入。

翻译成白话

原始账本仍归 Session 管,压缩器只记“前 N 条已折叠成摘要”,不会偷偷维护第二份容易漂移的历史。

为什么这对自研重要

所有权清楚且便于 rewind;游标、摘要和 session persistence 必须原子同步。

固定提交源码摘录
  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  }
为什么相信这条结论?查看 3 处证据
16
L1 · fact · jcode-context-003

压缩支持 reactive、趋势预测和语义换题三种策略

先看源码事实

reactive 在固定阈值触发;proactive 用 token history 的 EWMA 增长投影;semantic 用 turn embeddings 检测 topic shift,无 embedding 时回退 proactive。统一 anti-signals 防止重复、过早、停滞期和 cooldown 内压缩。

翻译成白话

可以等箱子快满再整理,也可以看增长速度提前整理,还可以在话题换挡时整理;但都会先检查“现在整理是否值得”。

为什么这对自研重要

上下文策略比多数单摘要 Agent 丰富,语义模式依赖本地 embedding 可用性和阈值调优。

固定提交源码摘录
  456      // ── Anti-signal guard (shared by proactive + semantic) ──────────────────
  457  
  458      /// Returns `true` when any anti-signal fires and we should NOT compact
  459      /// proactively right now.
  460      ///
  461      /// Anti-signals are universal guards applied before the mode-specific
  462      /// trigger logic. They prevent wasted work and respect user intent.
  463      fn anti_signals_block(&self, all_messages: &[Message]) -> bool {
  464          let cfg = &self.compaction_config;
  465  
  466          // 1. Already compacting — never double-trigger.
  467          if self.pending_task.is_some() {
  468              return true;
  469          }
  470  
  471          // 2. Context below the proactive floor — too early regardless of trend.
  472          let usage = self.context_usage_with(all_messages);
  473          if usage < cfg.proactive_floor {
  474              return true;
  475          }
  476  
  477          // 3. Not enough token history to project from.
  478          if self.token_history.len() < cfg.min_samples {
  479              return true;
  480          }
      … 52 lines omitted; exact range 456–543 …
  533          for i in 2..snapshots.len() {
  534              let delta = ((snapshots[i] as f64) - (snapshots[i - 1] as f64)).max(0.0);
  535              ewma_delta = alpha * delta + (1.0 - alpha) * ewma_delta;
  536          }
  537          let Some(current) = snapshots.last().copied().map(|value| value as f64) else {
  538              return false;
  539          };
  540          let projected = current + ewma_delta * cfg.lookahead_turns as f64;
  541  
  542          crate::logging::info(&format!(
  543              "[compaction/proactive] current={:.0} ewma_delta={:.1}/turn projected@{}turns={:.0} threshold={:.0}",
为什么相信这条结论?查看 2 处证据
17
L1 · fact · jcode-context-004

上下文溢出与请求体过大走不同紧急恢复路径

先看源码事实

context-limit error 触发同步 hard compact 并重置 cache/tool/provider session;HTTP 413 类 byte-size 错误先从持久历史剥离超大 inline images;超长 OpenAI encrypted compaction 则丢 native state 改文本 fallback。

翻译成白话

“字太多”“图片字节太大”“服务端压缩包本身太大”不是同一种病,分别治疗。

为什么这对自研重要

避免对 413 做无效 token 摘要;紧急路径会改写持久 transcript,报告需明确这是有损恢复。

固定提交源码摘录
   90      fn is_context_limit_error(error: &str) -> bool {
   91          let lower = error.to_lowercase();
   92          lower.contains("context length")
   93              || lower.contains("context window")
   94              || lower.contains("maximum context")
   95              || lower.contains("max context")
   96              || lower.contains("token limit")
   97              || lower.contains("too many tokens")
   98              || lower.contains("prompt is too long")
   99              || lower.contains("input is too long")
  100              || lower.contains("request too large")
  101              || lower.contains("length limit")
  102              || lower.contains("maximum tokens")
  103              || (lower.contains("exceeded") && lower.contains("tokens"))
  104      }
  105  
  106      /// Best-effort emergency recovery after a context-limit error.
  107      ///
  108      /// Performs a synchronous hard compaction and resets provider session state,
  109      /// allowing the caller to retry the same turn immediately.
  110      pub(super) fn try_auto_compact_after_context_limit(&mut self, error: &str) -> bool {
  111          if crate::provider::openai_request::is_openai_encrypted_content_too_large_error(error)
  112              && self.try_recover_oversized_openai_native_compaction()
  113          {
  114              return true;
      … 57 lines omitted; exact range 90–182 …
  172                  "auto_compaction_applied",
  173                  "context_limit_auto_compaction",
  174              )
  175              .with_session_id(self.session.id.clone())
  176              .with_detail(format!(
  177                  "dropped_messages={dropped},usage_pct={usage_pct:.1}"
  178              ))
  179              .force_attribution(),
  180          );
  181  
  182          true
为什么相信这条结论?查看 3 处证据
18
L1 · fact · jcode-memory-001

跨会话 memory 是独立后台 Agent,不阻塞主 turn

先看源码事实

MemoryAgent 通过容量 16 的 channel 接收主 Agent context,使用 embedding 检索、Haiku/sidecar relevance judge 和 memory-only tools;结果放入 pending memory,下一次新 user turn 才消费。

翻译成白话

主 Agent 回答时,旁边有个只管记忆的小秘书;它来不及的结果不会卡住当前回答,而是下一轮再递纸条。

为什么这对自研重要

延迟低且职责隔离,但 memory freshness 天生滞后一轮,channel 满时 try_send 可能丢更新。

固定提交源码摘录
    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;
为什么相信这条结论?查看 3 处证据
19
L1 · fact · jcode-memory-002

memory 分 project/global,带向量空间标识与重复抑制

先看源码事实

MemoryManager 按 working directory hash 存 project graph,也支持 global;embedding 写入 model id,避免跨向量空间比较。pending 注入按 prompt signature、memory set overlap 和 TTL 抑制重复,并把已从本会话抽取的 memory 标为 known。

翻译成白话

不同项目各有笔记本;换了 embedding 模型不会把两种坐标硬比。刚提过的记忆也不会每轮重复唠叨。

为什么这对自研重要

比单纯向量 top-k 多了去重与 provenance;路径 hash 迁移/重命名会形成新的 project memory scope。

固定提交源码摘录
  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      }
为什么相信这条结论?查看 3 处证据
小练习 5

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M06 · SECURITY

权限与沙箱:能做什么,在哪里做

审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?

先用一个生活比喻

审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。

这套实现先回答了什么?

先看这把工具是否在本会话工具箱里,再给企业自定义门卫一次否决机会;门卫自己坏了时默认放行。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
工具边界由 session allow/disable 与外部 pre_tool gate 双层控制crates/jcode-app-core/src/tool/mod.rs:543先看这把工具是否在本会话工具箱里,再给企业自定义门卫一次否决机会;门卫自己坏了时默认放行。
Bash 有确定性危险命令 gate,灾难目标直接拒绝crates/jcode-app-core/src/tool/bash_destructive_gate.rs:1不是让模型自己判断 rm 是否危险:命令先过代码规则;有些可解释后重试,有些目标永远不给过。
默认执行环境不是 OS 级沙箱crates/jcode-app-core/src/tool/bash.rs:724它有门禁和刹车,但不是把命令关进另一个房间;一旦获准,进程仍在你的机器权限下跑。
20
L1 · fact · jcode-security-001

工具边界由 session allow/disable 与外部 pre_tool gate 双层控制

先看源码事实

Registry.execute 先解析 alias,再检查 session allowed_tools/disabled_tools;若配置 pre_tool hook,退出 2 阻止调用并把 stderr 反馈模型,其他非零、超时或启动失败均 fail-open。

翻译成白话

先看这把工具是否在本会话工具箱里,再给企业自定义门卫一次否决机会;门卫自己坏了时默认放行。

为什么这对自研重要

可扩展治理强,但 pre_tool 的 fail-open 语义不适合要求故障关闭的高风险环境。

固定提交源码摘录
  543      /// Execute a tool by name
  544      pub async fn execute(&self, name: &str, input: Value, ctx: ToolContext) -> Result<ToolOutput> {
  545          let tools = self.tools.read().await;
  546          let resolved_name = Self::resolve_tool_name(name);
  547          if let Some(policy) = session_tool_policy(&ctx.session_id) {
  548              if let Some(allowed) = policy.allowed_tools.as_ref()
  549                  && !allowed.contains(resolved_name)
  550              {
  551                  return Err(anyhow::anyhow!("Tool '{}' is not allowed", resolved_name));
  552              }
  553              if policy.disabled_tools.contains(resolved_name) {
  554                  return Err(anyhow::anyhow!("Tool '{}' is disabled", resolved_name));
  555              }
  556          }
  557          let tool = match tools.get(resolved_name) {
  558              Some(tool) => tool.clone(),
  559              None => {
  560                  // List available tools so the model can recover instead of
  561                  // spiraling through hallucinated names like "ToolSearch" (#104).
  562                  let mut available: Vec<&str> = tools.keys().map(|k| k.as_str()).collect();
  563                  available.sort_unstable();
  564                  let suggestions = Self::closest_tool_names(name, &available);
  565                  let mut msg = format!("Unknown tool: {name}.");
  566                  if !suggestions.is_empty() {
  567                      msg.push_str(&format!(" Did you mean: {}?", suggestions.join(", ")));
      … 23 lines omitted; exact range 543–601 …
  591              .await;
  592              if let crate::hooks::GateDecision::Block { reason } = decision {
  593                  let mut fields =
  594                      Self::tool_lifecycle_fields("blocked", name, resolved_name, &input, &ctx);
  595                  fields.push(("block_reason".to_string(), reason.clone()));
  596                  crate::logging::event_warn("TOOL_LIFECYCLE", fields);
  597                  return Err(anyhow::anyhow!(
  598                      "Tool call blocked by pre_tool hook: {reason}"
  599                  ));
  600              }
  601          }
为什么相信这条结论?查看 2 处证据
21
L1 · fact · jcode-security-002

Bash 有确定性危险命令 gate,灾难目标直接拒绝

先看源码事实

所有前后台 Bash 在 dispatch 前调用 jcode_command_risk::assess/gate;普通危险命令要求模型带针对已拒绝调用的 justification 反思后再发,根目录、HOME、凭证库、设备节点等 catastrophic targets 直接 deny。

翻译成白话

不是让模型自己判断 rm 是否危险:命令先过代码规则;有些可解释后重试,有些目标永远不给过。

为什么这对自研重要

比纯提示词可靠,但命令解析器仍需要覆盖 shell 展开、别名、子 shell 和平台差异。

固定提交源码摘录
    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      }
为什么相信这条结论?查看 2 处证据
22
L1 · limitation · jcode-security-003

默认执行环境不是 OS 级沙箱

先看源码事实

Bash 直接用 bash/cmd.exe 子进程并继承宿主环境,在 session working_dir 下运行;代码提供 kill_on_drop、process group、timeout、策略 gate 与 hook,但审计范围内未见 bubblewrap、Seatbelt、容器或 restricted token 作为默认执行层。

翻译成白话

它有门禁和刹车,但不是把命令关进另一个房间;一旦获准,进程仍在你的机器权限下跑。

为什么这对自研重要

企业部署应外包给容器/VM/OS sandbox 或增加 capability-based executor,不能把危险命令 gate 等同隔离。

边界与风险
  • 这是对所审提交的内建 Bash 执行路径结论;用户可在 jcode 外层自行运行容器或受限账户。
固定提交源码摘录
  724          // Foreground execution with stdin detection
  725          self.execute_foreground(&params, &ctx).await
  726      }
  727  }
  728  
  729  impl BashTool {
  730      async fn execute_foreground(
  731          &self,
  732          params: &BashInput,
  733          ctx: &ToolContext,
  734      ) -> Result<ToolOutput> {
  735          #[cfg(unix)]
  736          if self.supports_reload_persistence(ctx) {
  737              return self
  738                  .execute_reload_persistable_foreground(params, ctx)
  739                  .await;
  740          }
  741  
  742          let timeout_ms = params.timeout.unwrap_or(DEFAULT_TIMEOUT_MS).min(600000);
  743          let timeout_duration = Duration::from_millis(timeout_ms);
  744  
  745          let has_stdin_channel = ctx.stdin_request_tx.is_some();
  746  
  747          let mut command = build_shell_command(&params.command);
  748          command
      … 1 lines omitted; exact range 724–760 …
  750              .stdout(Stdio::piped())
  751              .stderr(Stdio::piped());
  752  
  753          if has_stdin_channel {
  754              command.stdin(Stdio::piped());
  755          }
  756  
  757          if let Some(ref dir) = ctx.working_dir {
  758              command.current_dir(dir);
  759          }
  760          let mut child = command.spawn()?;
为什么相信这条结论?查看 2 处证据
小练习 6

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M07 · ECOSYSTEM

指令、MCP、Skills 与插件:能力如何接进来

一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?

先用一个生活比喻

这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。

这套实现先回答了什么?

无状态工具服务像公共电梯,多会话共用;带浏览器状态的服务像独立房间,每个会话一套。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
MCP 区分共享池与 session-owned clientcrates/jcode-base/src/mcp/manager.rs:1无状态工具服务像公共电梯,多会话共用;带浏览器状态的服务像独立房间,每个会话一套。
MCP schema 晚到只允许一次 cache miss,JSON-RPC 请求按 ID 隔离crates/jcode-app-core/src/agent/turn_execution.rs:335先让用户马上说话,不为插件启动卡住;插件工具后来到时只付一次缓存失效成本。多会话共用服务时每张工单都有号码。
指令层有稳定优先级:base、AGENTS、overlay、preferred tools、skills、动态 memorycrates/jcode-base/src/prompt.rs:374通用规则、用户规则、项目规则、工具偏好、技能和临时记忆各有自己的插槽,不是拼成一团不知谁覆盖谁。
Skills 跨 Jcode/Agents/Claude/Codex 生态并做 per-workspace overlaycrates/jcode-base/src/skill.rs:11它会借用其他 Agent 生态的技能包,但项目私有技能只在当前工作区叠加,不会污染另一个项目。
23
L1 · fact · jcode-mcp-001

MCP 区分共享池与 session-owned client

先看源码事实

daemon 中 shared:true 默认服务器复用 SharedMcpPool,shared:false 状态型服务器每 session 单独启动;并行 connect_all 不阻塞 agent spawn,第一次真实调用尚未连接的 server 时最多等待 30 秒。

翻译成白话

无状态工具服务像公共电梯,多会话共用;带浏览器状态的服务像独立房间,每个会话一套。

为什么这对自研重要

降低重复进程与连接成本,同时避免 stateful connector 串会话。

固定提交源码摘录
    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  }
为什么相信这条结论?查看 3 处证据
24
L1 · fact · jcode-mcp-002

MCP schema 晚到只允许一次 cache miss,JSON-RPC 请求按 ID 隔离

先看源码事实

首轮不等待慢 MCP;tool list 锁定后若发现新 mcp__ tools,仅重建一次 snapshot。McpHandle 用 atomic request id、pending oneshot map 与 30s request timeout,使共享 server 的并发响应互不串线。

翻译成白话

先让用户马上说话,不为插件启动卡住;插件工具后来到时只付一次缓存失效成本。多会话共用服务时每张工单都有号码。

为什么这对自研重要

启动延迟与 prompt cache 之间做了明确折中。

固定提交源码摘录
  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
为什么相信这条结论?查看 2 处证据
25
L1 · fact · jcode-instructions-001

指令层有稳定优先级:base、AGENTS、overlay、preferred tools、skills、动态 memory

先看源码事实

prompt builder 编译内建 base prompt,加载 global/project AGENTS.md,再读 ~/.jcode 与项目 .jcode overlay/preferred-tools,列出可用 skills;memory 和 active skill 进入动态部分。Session context 还冻结日期、OS、架构、版本、cwd 与 git 摘要。

翻译成白话

通用规则、用户规则、项目规则、工具偏好、技能和临时记忆各有自己的插槽,不是拼成一团不知谁覆盖谁。

为什么这对自研重要

可解释性好;用户应能在 UI 查看最终合成与各部分 token 占用。

固定提交源码摘录
  374  pub fn build_system_prompt_full_with_capabilities(
  375      skill_prompt: Option<&str>,
  376      available_skills: &[SkillInfo],
  377      is_selfdev: bool,
  378      memory_prompt: Option<&str>,
  379      working_dir: Option<&Path>,
  380      capabilities: PromptCapabilities,
  381  ) -> (String, ContextInfo) {
  382      let mut parts = base_system_prompt_parts(capabilities);
  383      let mut info = ContextInfo {
  384          system_prompt_chars: parts.join("\n\n").len(),
  385          ..Default::default()
  386      };
  387  
  388      // Add self-dev guidance only in active self-dev sessions. Normal sessions
  389      // learn about the on-ramp from the mode-aware `selfdev` tool schema.
  390      if is_selfdev {
  391          let selfdev_prompt = build_selfdev_prompt_for_working_dir(working_dir);
  392          info.selfdev_chars = selfdev_prompt.len();
  393          parts.push(selfdev_prompt);
  394      }
  395  
  396      // Add AGENTS.md instructions with tracking (from working_dir or cwd)
  397      let (md_content, md_info) = load_agents_md_files_from_dir(working_dir);
  398      if let Some(content) = md_content {
      … 39 lines omitted; exact range 374–448 …
  438      }
  439  
  440      // Add active skill prompt
  441      if let Some(skill) = skill_prompt {
  442          parts.push(format!("# Active Skill\n\n{}", skill));
  443      }
  444  
  445      let prompt = parts.join("\n\n");
  446      info.total_chars = prompt.len();
  447  
  448      (prompt, info)
为什么相信这条结论?查看 2 处证据
26
L1 · fact · jcode-instructions-002

Skills 跨 Jcode/Agents/Claude/Codex 生态并做 per-workspace overlay

先看源码事实

SkillRegistry 解析 SKILL.md frontmatter 的 name/description/allowed-tools;global 来源含 Claude plugins、~/.jcode/skills、~/.agents/skills,首跑可导入 Claude/Codex;project overlay 每次从 .jcode/.agents/.claude skills 读取且 project 同名覆盖 global,不进入共享 registry。

翻译成白话

它会借用其他 Agent 生态的技能包,但项目私有技能只在当前工作区叠加,不会污染另一个项目。

为什么这对自研重要

兼容性与热更新强;递归扫描 plugin root 有深度上限 5,复杂布局可能漏检。

固定提交源码摘录
   11  /// A skill definition from SKILL.md
   12  #[derive(Debug, Clone)]
   13  pub struct Skill {
   14      pub name: String,
   15      pub description: String,
   16      pub allowed_tools: Option<Vec<String>>,
   17      pub content: String,
   18      pub path: PathBuf,
   19      search_text: String,
   20  }
   21  
   22  #[derive(Debug, Deserialize)]
   23  struct SkillFrontmatter {
   24      name: String,
   25      description: String,
   26      #[serde(rename = "allowed-tools")]
   27      allowed_tools: Option<String>,
   28  }
   29  
   30  /// Registry of available skills
   31  #[derive(Debug, Default, Clone)]
   32  pub struct SkillRegistry {
   33      skills: HashMap<String, Skill>,
   34  }
   35  
      … 14 lines omitted; exact range 11–60 …
   50  
   51  impl SkillRegistry {
   52      /// Process-wide shared mutable registry used by both `skill_manage` and
   53      /// direct slash invocation paths. Keeping a single registry prevents slash
   54      /// commands from seeing a stale startup-only skill snapshot after reloads.
   55      ///
   56      /// Holds GLOBAL skills only (plugins, `~/.jcode/skills/`,
   57      /// `~/.agents/skills/`). Project-local skills are a per-session overlay
   58      /// composed at read time from the session's workspace root (issue #457);
   59      /// they must never enter this shared registry, and the daemon's startup
   60      /// cwd must never influence its contents.
为什么相信这条结论?查看 3 处证据
小练习 7

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M08 · COLLABORATION

子 Agent:把一个大任务拆成可治理的协作

什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?

先用一个生活比喻

不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 8

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M09 · STATE

会话、持久化与观测:让一次运行变成可追溯事实

如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?

先用一个生活比喻

内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。

这套实现先回答了什么?

平时只往流水账追加新变化,偶尔把整本账重抄成快照;这样频繁保存不会每次重写全部历史。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
Session 用完整 snapshot 加 append-only JSONL journalcrates/jcode-base/src/session/persistence.rs:307平时只往流水账追加新变化,偶尔把整本账重抄成快照;这样频繁保存不会每次重写全部历史。
损坏 journal 跳过坏行、挽救 glued entries,并在下次保存自愈crates/jcode-base/src/session/persistence.rs:26流水账中间坏一行,不会把后面所有对话一起判死;能捞的条目先捞出来,再重写一本干净快照。
观测覆盖结构化 lifecycle、实时 session metrics 与可选择遥测crates/jcode-app-core/src/tool/mod.rs:603本地能看每个工人最近是否真在干活、花了多少 token、工具跑多久;发往服务端的匿名统计能关闭,而对话内容默认不发。
关键故障边界有源码级回归测试,许可证为 MITcrates/jcode-base/src/compaction_tests.rs:388不只是代码注释说“应该安全”:断流、损坏账本、深度协作契约等都有回归用例钉住。
27
L1 · fact · jcode-persist-001

Session 用完整 snapshot 加 append-only JSONL journal

先看源码事实

save 比较 metadata/vector dirty state:需要全量时 checkpoint snapshot 并删 journal,否则只追加 messages/env snapshots/memory injections/replay events delta;journal 超上限或 append 失败再回退 snapshot。

翻译成白话

平时只往流水账追加新变化,偶尔把整本账重抄成快照;这样频繁保存不会每次重写全部历史。

为什么这对自研重要

兼顾耐久与 I/O;多写者必须依靠更上层会话所有权避免交错 append。

固定提交源码摘录
  307      pub fn save(&mut self) -> Result<()> {
  308          self.updated_at = Utc::now();
  309          let path = session_path(&self.id)?;
  310          let journal_path = session_journal_path_from_snapshot(&path);
  311          let start = std::time::Instant::now();
  312          let snapshot_bytes_before = file_len_or_zero(&path);
  313          let journal_bytes_before = file_len_or_zero(&journal_path);
  314          let current_meta = self.journal_meta();
  315          let metadata_needs_snapshot = self
  316              .persist_state
  317              .last_meta
  318              .as_ref()
  319              .is_some_and(|prev| metadata_requires_snapshot(prev, &current_meta));
  320          let vectors_need_snapshot = !self.persist_state.snapshot_exists
  321              || self.persist_state.messages_mode == PersistVectorMode::Full
  322              || self.persist_state.env_snapshots_mode == PersistVectorMode::Full
  323              || self.persist_state.memory_injections_mode == PersistVectorMode::Full
  324              || self.persist_state.replay_events_mode == PersistVectorMode::Full
  325              || self.messages.len() < self.persist_state.messages_len
  326              || self.env_snapshots.len() < self.persist_state.env_snapshots_len
  327              || self.memory_injections.len() < self.persist_state.memory_injections_len
  328              || self.replay_events.len() < self.persist_state.replay_events_len;
  329  
  330          let delta_messages = self
  331              .messages
      … 53 lines omitted; exact range 307–395 …
  385              match append_result {
  386                  Ok(()) => {
  387                      self.reset_persist_state(true);
  388                      let journal_stat_start = Instant::now();
  389                      let journal_bytes_after = file_len_or_zero(&journal_path);
  390                      let journal_stat_ms = journal_stat_start.elapsed().as_millis();
  391                      if journal_bytes_after > MAX_SESSION_JOURNAL_BYTES {
  392                          let checkpoint_start = Instant::now();
  393                          let result = self.checkpoint_snapshot(&path, &journal_path);
  394                          let checkpoint_ms = checkpoint_start.elapsed().as_millis();
  395                          let journal_bytes_after = file_len_or_zero(&journal_path);
为什么相信这条结论?查看 2 处证据
28
L1 · fact · jcode-persist-002

损坏 journal 跳过坏行、挽救 glued entries,并在下次保存自愈

先看源码事实

replay 不在首个 parse error 停止,而是继续后续行;对 torn append 与下一条粘在同一行的情况扫描结构起点并 stream-parse。发现腐坏后备份 corrupt.jsonl,强制下次 checkpoint 把已挽救状态固化。

翻译成白话

流水账中间坏一行,不会把后面所有对话一起判死;能捞的条目先捞出来,再重写一本干净快照。

为什么这对自研重要

长会话恢复质量高,且保留法证副本。

固定提交源码摘录
   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)
为什么相信这条结论?查看 3 处证据
29
L1 · fact · jcode-observe-001

观测覆盖结构化 lifecycle、实时 session metrics 与可选择遥测

先看源码事实

工具/MCP/Swarm/Session/Provider 路径记录带 session/message/tool id、latency、bytes、状态的结构化事件;独立 lock-free-ish session registry 维护 60 秒 token churn、turns 和 last activity。匿名 telemetry 默认启用,可用 JCODE_NO_TELEMETRY、DO_NOT_TRACK 或 marker 关闭;prompt/transcript 内容分享另行显式 opt-in。

翻译成白话

本地能看每个工人最近是否真在干活、花了多少 token、工具跑多久;发往服务端的匿名统计能关闭,而对话内容默认不发。

为什么这对自研重要

可观测维度很丰富;隐私评审需同时检查 usage telemetry、content-sharing 和 sponsor provenance 三条数据面。

固定提交源码摘录
  603          crate::logging::event_info(
  604              "TOOL_LIFECYCLE",
  605              Self::tool_lifecycle_fields("start", name, resolved_name, &input, &ctx),
  606          );
  607  
  608          let started_at = std::time::Instant::now();
  609          let result = tool.execute(input.clone(), ctx.clone()).await;
  610          let latency_ms = started_at.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
  611  
  612          crate::telemetry::record_tool_execution(resolved_name, &input, result.is_ok(), latency_ms);
  613          Self::fire_post_tool_hook(resolved_name, &ctx, &result, latency_ms);
  614  
  615          let mut output = match result {
  616              Ok(output) => output,
  617              Err(error) => {
  618                  let mut fields =
  619                      Self::tool_lifecycle_fields("error", name, resolved_name, &input, &ctx);
  620                  fields.push(("elapsed_ms".to_string(), latency_ms.to_string()));
  621                  fields.push(("error".to_string(), crate::util::format_error_chain(&error)));
  622                  crate::logging::event_warn("TOOL_LIFECYCLE", fields);
  623                  return Err(error);
  624              }
  625          };
  626  
  627          // Context overflow guard: check if this output would push us over the limit
  628          output = self.guard_context_overflow(name, output).await;
  629  
  630          let mut fields = Self::tool_lifecycle_fields("done", name, resolved_name, &input, &ctx);
  631          fields.push(("elapsed_ms".to_string(), latency_ms.to_string()));
  632          fields.push(("output_bytes".to_string(), output.output.len().to_string()));
  633          fields.push((
  634              "output_chars".to_string(),
  635              output.output.chars().count().to_string(),
  636          ));
  637          fields.push(("image_count".to_string(), output.images.len().to_string()));
  638          crate::logging::event_info("TOOL_LIFECYCLE", fields);
为什么相信这条结论?查看 4 处证据
30
L3 · fact · jcode-maturity-001

关键故障边界有源码级回归测试,许可证为 MIT

先看源码事实

仓库对 compaction restore/hard threshold、mid-stream rollback、session journal corruption、MCP management、hook gate、Swarm prompt/assignment/report-back 和 tool schema 等关键路径有测试;LICENSE 为 MIT。

翻译成白话

不只是代码注释说“应该安全”:断流、损坏账本、深度协作契约等都有回归用例钉住。

为什么这对自研重要

成熟度高,但 1,198 个 Rust 文件的系统仍需 CI 测试矩阵覆盖多 Provider、三平台和远程 daemon。

固定提交源码摘录
  388  async fn test_guard_at_95_triggers_hard_compact() {
  389      let mut manager = CompactionManager::new().with_budget(1_000);
  390      let mut messages = Vec::new();
  391      for i in 0..20 {
  392          messages.push(make_text_message(
  393              Role::User,
  394              &format!("message {} with padding {}", i, "x".repeat(50)),
  395          ));
  396          manager.notify_message_added();
  397      }
  398      // 96% usage — above critical threshold
  399      manager.update_observed_input_tokens(960);
  400  
  401      let provider: Arc<dyn Provider> = Arc::new(MockSummaryProvider);
  402      let action = manager.ensure_context_fits(&messages, provider);
  403      assert!(
  404          matches!(action, CompactionAction::HardCompacted(_)),
  405          "SHOULD hard-compact at 96%"
  406      );
  407      assert!(
  408          manager.compacted_count > 0,
  409          "compacted_count should increase after hard compact"
  410      );
  411      assert!(
  412          manager.active_summary.is_some(),
      … 28 lines omitted; exact range 388–451 …
  441          "API messages should be fewer after hard compact"
  442      );
  443      // First message should be the emergency summary
  444      match &api_messages[0].content[0] {
  445          ContentBlock::Text { text, .. } => {
  446              assert!(text.contains("Previous Conversation Summary"));
  447              assert!(text.contains("Emergency compaction"));
  448          }
  449          _ => panic!("expected text summary block"),
  450      }
  451  }
为什么相信这条结论?查看 4 处证据
小练习 9

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M10 · ENGINEERING

测试、恢复与工程取舍:把漂亮机制变成可靠产品

哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?

先用一个生活比喻

这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 10

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M11 · PRACTICE

把读懂变成会判断

下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。

Q1

每个用户 turn 先写盘,再进入可恢复的流式循环

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

耐崩溃性强,但每轮和工具结果频繁保存会增加本地 I/O,需要 journal 快路径配合。

证据:crates/jcode-app-core/src/agent/turn_execution.rs:4
Q2

循环在每次请求前修复工具配对并重建稳定快照

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

把异常历史修复、缓存稳定和请求生命周期放在一个明确关口。

证据:crates/jcode-app-core/src/agent/turn_loops.rs:17
Q3

中途断流先撤销半截状态再完整重播

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

显著降低重试导致的重复文本和重复副作用;普通 stdout 无法擦除,只会显示断点标记。

证据:crates/jcode-app-core/src/agent/turn_loops.rs:455
Q4

上下文、截断回复和工具后空回复各有独立止损上限

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

长任务韧性好,也避免无限循环;自动补写的 user message会成为真实历史。

证据:crates/jcode-app-core/src/agent/turn_loops.rs:5
Q5

Provider 契约不仅抽象生成,还抽象认证、路由、transport 与原生能力

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

多后端能力完整,但 trait 面积很大,新 Provider 的一致性测试成本高。

证据:crates/jcode-provider-core/src/lib.rs:76
APPENDIX · SOURCE INDEX

本课读过的实现文件

文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。

  1. 01crates/jcode-app-core/src/agent/turn_execution.rsL4–35, L37–90, L335–393
  2. 02crates/jcode-app-core/src/agent/turn_loops.rsL17–68, L455–484, L5–15, L798–825, L485–547, L548–585, L63–92, L878–917
  3. 03crates/jcode-provider-copilot-runtime/src/lib.rsL615–659
  4. 04crates/jcode-provider-openrouter-runtime/src/openrouter_tests.rsL2629–2718
  5. 05crates/jcode-provider-core/src/lib.rsL76–126, L293–436
  6. 06crates/jcode-base/src/provider/mod.rsL328–374
  7. 07crates/jcode-base/src/prompt.rsL451–557, L374–448, L615–643, L75–91
  8. 08crates/jcode-base/src/compaction.rsL128–205, L283–345, L456–543, L552–605
  9. 09crates/jcode-base/src/compaction_tests.rsL61–95, L388–451
  10. 10crates/jcode-app-core/src/agent/compaction.rsL90–182, L185–241, L244–283
  11. 11crates/jcode-base/src/memory_agent.rsL1–45, L234–275
  12. 12crates/jcode-app-core/src/agent/prompting.rsL20–55
  13. 13crates/jcode-base/src/memory.rsL159–185, L248–275
  14. 14crates/jcode-base/src/memory/pending.rsL129–174
  15. 15crates/jcode-tool-core/src/lib.rsL9–65, L75–139
  16. 16crates/jcode-app-core/src/tool/mod.rsL150–255, L318–341, L627–678, L543–601, L603–638
  17. 17crates/jcode-app-core/src/tool/batch.rsL180–202, L202–281
  18. 18crates/jcode-app-core/src/tool/bash.rsL742–760, L884–930, L1095–1175, L689–704, L724–760, L1095–1134
  19. 19crates/jcode-base/src/hooks.rsL220–323, L325–352
  20. 20crates/jcode-app-core/src/tool/bash_destructive_gate.rsL1–39
  21. 21crates/jcode-base/src/mcp/manager.rsL1–59, L114–199, L318–360
  22. 22crates/jcode-base/src/mcp/client.rsL14–55
  23. 23crates/jcode-base/src/skill.rsL11–60, L98–145, L231–303
  24. 24crates/jcode-app-core/src/server/swarm.rsL1528–1613, L1615–1700, L333–448
  25. 25crates/jcode-swarm-core/src/lib.rsL377–432, L435–496, L699–740, L213–253
  26. 26crates/jcode-app-core/src/server/swarm_mutation_state_tests.rsL39–83
  27. 27crates/jcode-base/src/session/persistence.rsL307–395, L26–128, L142–172
  28. 28crates/jcode-base/src/session_tests/cases.rsL835–880, L930–1095
  29. 29crates/jcode-base/src/session_metrics.rsL1–25, L63–128
  30. 30crates/jcode-telemetry-core/src/lib.rsL312–395
  31. 31crates/jcode-app-core/src/tool/communicate_tests/end_to_end.rsL370–442
  32. 32LICENSEL1–21
下一步

从课程回到报告,做一次反向核验

教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。

查看 JCode 报告 ↗