NovaCode 终端:编辑 README.md 前弹出确认,选项是允许、本次模式不再询问、拒绝

终端 AI 编码助手

NovaCode

运行在终端中的 AI 编程助手,通过自然语言读写代码、执行命令,并管理权限与长会话。

  • Python
  • Textual
  • MCP
  • OS Sandbox
05 / 05 · DOCUMENTATION

上下文与扩展

工具结果先预算,再进入稳定历史;摘要保留原文尾部,MCP 定义按预算选择加载策略。

大工具结果先落盘

长会话的压力不只来自用户消息。一次搜索可以返回数百行,一次测试可以输出大量日志,MCP 工具定义也会占用模型输入。上下文管理因此需要同时关注工具结果、历史消息和工具 Schema,而不是只在报出窗口超限时删掉几条对话。

工具输出很大时,直接追加到对话会占用后续窗口并破坏可用性。NovaCode 在结果进入历史前,按预算将大结果保存为文件,模型上下文保留预览和回读路径。

novacode/novacode/context/manager.pyL148-185 @8484bf0
def apply_tool_result_budget(
    tool_results: list[ToolResultBlock],
    session_dir: Path,
    exempt_ids: set[str] | None = None,
) -> None:
    """在一轮工具结果进入对话历史之前执行聚合预算。

    整批结果的总字符数超过 AGGREGATE_CHAR_LIMIT 时,从最大的开始逐条
    溢写到磁盘、就地替换成预览,直到总量回到限额内。消息进历史前处理完,
    历史里的内容自此不再改动,Prompt Cache 前缀天然稳定。

    exempt_ids 里的 tool_use_id 不参与溢写:溢写文件的回读结果(再溢写
    模型就永远看不到全文),以及本轮已经单条溢写过的结果。全是豁免项时
    接受超额。
    """
    exempt = exempt_ids or set()
    total = sum(len(tr.content) for tr in tool_results)
    if total <= AGGREGATE_CHAR_LIMIT:
        return

    # 按内容长度降序挑选:先溢写最大的,回到限额内需要动的条数最少。
    ranked = sorted(tool_results, key=lambda tr: len(tr.content), reverse=True)
    for tr in ranked:
        if total <= AGGREGATE_CHAR_LIMIT:
            break
        if tr.tool_use_id in exempt:
            continue
        if len(tr.content) <= PREVIEW_CHARS:
            # 比预览还短的结果,溢写换不回空间
            continue
        try:
            fp = persist_tool_result(tr.tool_use_id, tr.content, session_dir)
        except OSError:
            # 写盘失败就保留原文。消息随即定型进历史,不会再有重试
            continue
        preview = make_persisted_preview(tr.content, fp)
        total -= len(tr.content) - len(preview)
        tr.content = preview
聚合预算从最大的结果开始溢写;回读内容豁免,避免模型反复得到截断后的文件指针。

结果进入历史后保持稳定,有利于模型端的前缀缓存。落盘失败会保留原文,所有结果都豁免时也可能接受超预算;这是一种明确取舍,不是绝对硬截断。

为什么要保留回读路径

直接截断大日志会使错误位置或关键行永久消失。落盘后保留预览和路径,模型可以先判断是否需要完整内容,再通过读取工具获取相关部分。回读结果被豁免再次溢写,否则会陷入“读文件只得到另一个文件指针”的循环。

聚合预算对整批工具结果计算,而不只限制单个结果:十份各自不大的输出也可能一起挤满窗口。优先处理最大结果能用较少替换动作回收更多空间;无法写盘时保留原文,则优先保障信息可用性而接受体积超额。

这一层主要使用字符预算近似控制体积,不等价于模型 tokenizer 的精确 token 数。是否接近实际窗口,还需要结合模型窗口配置、用量反馈和下一层压缩阈值判断。

摘要与恢复不是删除全部历史

接近窗口阈值时压缩较早的前缀,保留近期逐字尾部。压缩边界要避开工具调用 / 结果配对,恢复附件补充最近读过的文件与已启用 Skill,避免只有摘要却不知道当前工作环境。

会话持久化保存压缩边界,恢复时校验工具配对。工具结果文件也需要随会话的生命周期管理,否则预览中的回读路径可能变成失效指针。

flowchart TD
  T[工具结果批次] --> B[结果预算]
  B --> P[预览与磁盘回读路径]
  P --> H[稳定对话历史]
  H --> C{接近窗口阈值}
  C -->|否| M[继续模型调用]
  C -->|是| S[较早前缀摘要]
  S --> R[摘要、逐字尾部与恢复附件]
  R --> M

压缩边界不能随意切

工具调用与结果是一组协议记录。保留调用却丢失结果,或只保留结果却丢失调用,都可能让下一次模型请求无效。压缩会调整保留起点,尽量保持这些配对;恢复时还要再次检查,不能仅因 JSONL 能读出就视为会话可用。

摘要保留目标、已完成工作和关键约束,近期尾部保留精确措辞,恢复附件补充最近文件和 Skill。三者一起减少压缩后的信息断层,但摘要依然有损,重要事实应以原文件和可回读结果核对。

压缩本身也依赖模型,可能超时或失败。项目使用失败熔断,避免每轮都为同一问题付出无效摘要调用;“自动压缩”不是无成本、永远成功的后台能力。

MCP 工具加载的三种路径

novacode/novacode/mcp/loading_strategy.pyL61-81 @8484bf0
def decide_mode(
    base_url: str,
    context_window: int,
    mcp_schema_chars: int,
    threshold_percent: int = DEFAULT_EAGER_THRESHOLD_PERCENT,
) -> McpLoadingMode:
    override = os.environ.get(_ENV_OVERRIDE, "").strip().lower()
    if override in ("eager", "native", "dispatch"):
        return McpLoadingMode(override)

    if mcp_schema_chars <= 0:
        # 没有 MCP 工具,走哪条都一样,eager 最省事
        return McpLoadingMode.EAGER

    budget = context_window * threshold_percent / 100
    if estimate_schema_tokens(mcp_schema_chars) < budget:
        return McpLoadingMode.EAGER

    if is_official_anthropic_endpoint(base_url):
        return McpLoadingMode.NATIVE
    return McpLoadingMode.DISPATCH
决策依据包括环境覆盖、工具 Schema 估算预算和端点能力,不只按工具数量选择。
模式适用情况
EAGERSchema 预算足够,直接提供工具定义
NATIVE大工具集,且端点支持对应原生策略
DISPATCH大工具集通过发现和分发减少首轮定义占用

策略选择后还要保持会话工具定义稳定,避免每轮动态改变导致缓存失效。MCP 连接支持 stdio 与 Streamable HTTP,Skill 提供按需指令,Hook 提供生命周期扩展,它们不共享同一种授权语义。

为什么依据 Schema 预算

十个参数复杂、描述很长的工具,可能比几十个简单工具占用更多输入。加载策略估算 Schema 字符对应的 token 体积,并与窗口预算比较,再结合端点能力决定是否延迟加载。工具数量只能作为粗略线索,不能替代体积判断。

EAGER 把定义直接放进可用集合;NATIVE 使用端点支持的延迟工具能力;DISPATCH 通过发现与分发减少首轮暴露的定义。后两者减少固定开销,也可能增加查找步骤和调用成本,适合工具集大且首轮只需要少数工具的场景。

指令、记忆与扩展各自存什么

机制保存或提供的内容不应被混淆为
NOVACODE.md项目长期指令与约定每轮任务的全部执行记录
MEMORY.md 与召回可复用记忆索引及选择式信息当前文件内容的绝对真值
会话 JSONL对话、工具配对与压缩边界对存活进程的自动接管
Skill按需工作方法,可 inline 或 fork绕过运行时权限的授权
Hook生命周期上的检查与扩展动作独立、无限制的 Agent 执行能力

这些机制共同改善长任务连续性,但存储生命周期和权限范围需要分别管理。特别是恢复历史不意味着重新执行所有旧工具,也不意味着此前启动的系统进程已被收养。

已知限制

Hook 的 agent 动作尚未实现,会明确失败;不能列为已具备能力。本站 mock 的 /compact 只保留最近四条演示消息,不调用模型生成摘要、不保留真实工具协议历史,也不模拟磁盘结果回读。真实能力应以这些源码与项目回归记录为准。