【nanobot】Harness 6 件套演进:47k 星的项目如何"轻装"接入协议?

【nanobot】Harness 6 件套演进:47k 星的项目如何”轻装”接入协议?

4 个月前写 nanobot(41k⭐)的时候,我把它定位成”超轻量级个人 AI Agent”——核心亮点是 AgentLoop / AgentRunner / MemoryStore 三个文件读得懂的简洁。今天打开仓库看 release-notes,看到 v0.2.x → v0.3.0 期间悄悄塞进了 18 个生命周期方法 的 AgentHook、ephemeral sub-agent、SKILL.md frontmatter 校验、Context Governance 三策略(SNIP / MICROCOMPACT / INFLIGHT)、bubblewrap 沙箱、cron turn 推迟注入……GitHub 47,312⭐、1k+ commits 增量,是怎么把 Harness 6 件套逐项压缩进一个仍然”超轻量”的 0.3 版本的?

一、引子:13k ⭐ 增量里,藏着 Harness 演化的 5 个关键决策

如果你读过 4 个月前那篇 [nanobot 超轻量级个人 AI Agent] 文,那篇文章末尾我给出 nanobot 的”组件清单”是:

  • AgentLoop / AgentRunner / MessageBus / ContextBuilder / MemoryStore / ToolRegistry / Channel / Provider

回头看,那是一个不完整的 6 件套——没有显式的 Hook 系统、没有 Sub-Agent 抽象、没有 Skills 协议、没有 Cron 异步编排、没有 Sandbox 边界。但 Harness 实战的人都知道,这 5 个组件才是真正区分”实验性 demo”和”生产 Harness”的标尺。

这 4 个月里,HKUDS(香港大学数据智能实验室)做的事情不是堆功能——是把每个组件都按”Agent Skills Identity Contract”那种协议级规范实现,并保证每加一个组件不破坏”超轻量”承诺。今天这篇就拆这 5 个演化,逆向出一个工程团队如何在不破坏核心抽象的前提下扩展到完整 Harness 6 件套的方法论。

我会用 4 段可运行 Python 代码(基于 nanobot 0.3.0 的真实 API)展示:

  1. 18 方法 AgentHook 生命周期的最小可用实现
  2. ephemeral sub-agent 的 context 隔离 + 状态回传
  3. SKILL.md frontmatter 契约校验器(和 Anthropic / Claude Code 同款)
  4. Context Governance SNIP / MICROCOMPACT / INFLIGHT 三策略(解决”Context Window 爆炸”)

文末我会给出一张演化矩阵和一份从零搭建启示——能直接帮你判断”你的 Harness 是不是过度设计”。


二、项目定位:从”轻量 Agent”到”Harness 标杆候选”

2.1 起点回顾:41k⭐ 时的 nanobot

2026-04-28 我写第一篇 nanobot 时,仓库核心架构是这样:

关键文件职责
表现层channels/CLI / Telegram / Discord / Feishu / Slack
消息层bus/MessageBus + InboundMessage / OutboundMessage
Agent 核心agent/loop.py agent/runner.py agent/context.py16k 行内
记忆层agent/memory.py agent/autocompact.py文件 I/O + Consolidator
工具层tools/ 11 个工具filesystem / shell / web / cron
模型层providers/OpenAI / Anthropic / OpenAI-Compat

那时候 nanobot 已经具备 MVP Harness 的 6/10,但还不是完整 Harness:缺显式的 Hook 系统、没有 Sub-Agent 抽象、没有 Skills 协议、没有 Context Window 防护。

2.2 转折点:v0.2.x → v0.3.x 的 5 件大事

v0.2.x → v0.3.0 期间,5 件不显眼但关键的事情发生了:

  1. AgentHook 系统成型——nanobot/agent/hook.py 从 0 行变成 10k 字符,定义了 18 个生命周期方法(before_run / before_iteration / on_stream / before_execute_tool / after_execute_tool 等)
  2. Sub-AgentManager 抽象——nanobot/agent/subagent.py 引入 SubagentStatus dataclass,把异步任务状态变得可观测
  3. SkillsLoader 协议——nanobot/agent/skills.py 实现 SKILL.md 的 YAML frontmatter 解析 + 64 字符名字限制 + Agent Skills Identity Contract
  4. ContextGovernance 三策略——nanobot/agent/context_governance.py 引入 SNIP_SAFETY_BUFFER=1024 / MICROCOMPACT_MIN_CHARS=500 / INFLIGHT_COMPACT_TARGET_RATIO=0.85 三个常量,分别对应”内容截断 / 工具结果压缩 / 飞行中压缩”
  5. Sandbox 边界 + GoalPermission ContextVar——nanobot/agent/tools/sandbox.py 用 bubblewrap 给 shell 命令加隔离;nanobot/agent/goal_permission.py 用 Python contextvars 限制”长期目标”工具的执行时机

这 5 件事加起来 9 万字符新增代码,但 nanobot 的核心 agent loop 仍然 < 30k 行(这是为什么仓库主页写”Keep the core agent loop small and readable”)。

2.3 Harness 6 件套里的位置

按 6 件套矩阵拆解:

组件nanobot 对应模块实现深度
Rulenanobot/agent/goal_permission.py + config/permissions.yaml基础(contextvar 单次 turn 检查)
Skillnanobot/agent/skills.py + nanobot/skills/builtin/完整(frontmatter 协议 + Identity Contract)
Sub-Agentnanobot/agent/subagent.py + nanobot/agent/tools/spawn.py完整(ephemeral 隔离 + 状态回传)
Workflownanobot/agent/automation_turns.py + cron_turns.py完整(session-bound defer / idle 检测)
Scriptnanobot/agent/tools/sandbox.py + tools/runtime_control.py中等(bwrap + allowlist)
MCPnanobot/agent/tools/mcp.py + nanobot/agent/tools/mcp_oauth.py完整(含 OAuth)

核心洞察:nanobot 在每个 6 件套组件上都做了”够用但不过度”的实现——这正是 Karpathy 反复强调的”Bitter Lesson”。模型每进化一次(比如 Claude Code 内置 sub-agent),nanobot 不必改一行代码——因为它的抽象就稳定在”协议 + 委派”,而不是”智能 + 包办”。

2.4 AgentHook 18 生命周期时序图

下面这张时序图精确还原 nanobot/agent/runner.py 的 hook chain 触发顺序。每条红色箭头对应一个 hook 方法的调用点,蓝色虚线是 hook 之间的异步边界:

sequenceDiagram
    participant Loop as AgentLoop
    participant Runner as AgentRunner
    participant Chain as CompositeHook
    participant H1 as AgentProgressHook
    participant H2 as AuditToolHook
    participant H3 as FooterHook
    participant LLM as Provider(LLM)
    participant Tool as Registry/Tool

    Loop->>Runner: run(messages, session)
    Runner->>Chain: before_run(ctx)
    Chain->>H1: before_run(reraise=True)
    Chain->>H2: before_run(try/except)
    Chain->>H3: before_run(try/except)

    loop iter 0..N (max_iterations)
        Runner->>Chain: before_iteration(ctx.iter=i)
        Chain->>H1: before_iteration
        Runner->>LLM: stream(prompt, tools)
        LLM-->>Runner: stream deltas + reasoning
        Runner->>Chain: on_stream(ctx, delta)
        Chain->>H1: on_stream (reraise)
        Note over Runner,H1: emit_reasoning + emit_reasoning_end

        alt LLM produces tool_calls
            Runner->>Chain: before_execute_tool(call, tool, params)
            Chain->>H2: before_execute_tool (audit log)
            Runner->>Tool: execute(params)
            Tool-->>Runner: result
            Runner->>Chain: after_execute_tool(result)
            Chain->>H2: after_execute_tool (audit end)
        end
        Runner->>Chain: after_iteration(ctx)
        Chain->>H1: after_iteration
    end

    Runner->>Chain: finalize_content(final)
    Chain->>H3: finalize_content (sync pipeline)
    H3-->>Chain: content + footer
    Chain-->>Runner: final_content
    Runner->>Chain: after_run(ctx)
    Chain->>H1: after_run (reraise)

    opt exception
        Runner->>Chain: on_error(ctx) / on_finally(ctx)
    end

关键观察

  • H1(progress)设了 reraise=True——它在紫色虚线之外单独跑通,出错会中断 run。
  • H2/H3 是默认 reraise=False——错误隔离在 CompositeHook 层 try/except 里。
  • finalize_content 是同步 pipeline——故意不做错误隔离,让 transform bug 暴露。
  • on_stream / emit_reasoning / emit_reasoning_end 三连击——给 WebUI/Telegram 推送”开始推理 / 推理结束 / 推理块内容”三态,避免推理 token 覆盖消息气泡。

三、架构分析:4 个月内”加 5 组件不破坏核心”的工程奇迹

3.1 总架构图

graph TB
    subgraph Channels["📡 Channels(表现层)"]
        CLI["CLI / WebUI"]
        TG["Telegram"]
        DC["Discord"]
        FS["Feishu"]
    end
    style CLI fill:#C7CEEA,stroke:#7B8AB8,color:#333
    style TG fill:#C7CEEA,stroke:#7B8AB8,color:#333
    style DC fill:#C7CEEA,stroke:#7B8AB8,color:#333
    style FS fill:#C7CEEA,stroke:#7B8AB8,color:#333

    subgraph Bus["📨 MessageBus(消息层)"]
        IN["InboundMessage"]
        OUT["OutboundMessage"]
        EVT["RuntimeEventBus"]
        DEFER["deferred_queues"]
    end
    style IN fill:#E8D5F5,stroke:#9B7BB8,color:#333
    style OUT fill:#E8D5F5,stroke:#9B7BB8,color:#333
    style EVT fill:#E8D5F5,stroke:#9B7BB8,color:#333
    style DEFER fill:#E8D5F5,stroke:#9B7BB8,color:#333

    subgraph Loop["⚙️ AgentLoop / AgentRunner(核心)"]
        LOOP["AgentLoop<br/>turn orchestration"]
        RUNNER["AgentRunner<br/>provider/tool loop"]
        GOV["ContextGovernance<br/>SNIP/MICROCOMPACT/INFLIGHT"]
        CKPT["checkpoint"]
    end
    style LOOP fill:#FFB3C6,stroke:#C97A8F,color:#333
    style RUNNER fill:#FFB3C6,stroke:#C97A8F,color:#333
    style GOV fill:#FFB3C6,stroke:#C97A8F,color:#333
    style CKPT fill:#FFB3C6,stroke:#C97A8F,color:#333

    subgraph Hooks["🪝 Hook 系统(18 生命周期方法)"]
        H_BEFORE_RUN["before_run"]
        H_BEFORE_ITER["before_iteration"]
        H_ON_STREAM["on_stream"]
        H_BEFORE_TOOL["before_execute_tool"]
        H_AFTER_TOOL["after_execute_tool"]
        H_FINALIZE["finalize_content"]
    end
    style H_BEFORE_RUN fill:#FFDAB9,stroke:#C99B7A,color:#333
    style H_BEFORE_ITER fill:#FFDAB9,stroke:#C99B7A,color:#333
    style H_ON_STREAM fill:#FFDAB9,stroke:#C99B7A,color:#333
    style H_BEFORE_TOOL fill:#FFDAB9,stroke:#C99B7A,color:#333
    style H_AFTER_TOOL fill:#FFDAB9,stroke:#C99B7A,color:#333
    style H_FINALIZE fill:#FFDAB9,stroke:#C99B7A,color:#333

    subgraph Sub["🤖 Sub-Agent(ephemeral 隔离)"]
        SM["SubagentManager"]
        SS["SubagentStatus<br/>6 phases"]
        WS["WorkspaceScope<br/>bind/reset"]
    end
    style SM fill:#B5EAD7,stroke:#7BB89B,color:#333
    style SS fill:#B5EAD7,stroke:#7BB89B,color:#333
    style WS fill:#B5EAD7,stroke:#7BB89B,color:#333

    subgraph Skills["📋 Skills 协议(Agent Skills Identity Contract)"]
        SL["SkillsLoader"]
        SK_FM["SKILL.md<br/>YAML frontmatter"]
        SK_NAME["valid_skill_metadata<br/>name ≤ 64 chars"]
    end
    style SL fill:#FFF9C4,stroke:#C9B97A,color:#333
    style SK_FM fill:#FFF9C4,stroke:#C9B97A,color:#333
    style SK_NAME fill:#FFF9C4,stroke:#C9B97A,color:#333

    subgraph Sandbox["🔒 Sandbox / Runtime Control"]
        BWRAP["bubblewrap (bwrap)"]
        GOAL["GoalPermission<br/>ContextVar"]
        RC["RuntimeSnapshot<br/>allowlist"]
    end
    style BWRAP fill:#F5F5F5,stroke:#999,color:#333
    style GOAL fill:#F5F5F5,stroke:#999,color:#333
    style RC fill:#F5F5F5,stroke:#999,color:#333

    subgraph State["📚 Session / Memory / Templates"]
        SS_MGR["SessionManager"]
        MEM["MemoryStore + Dream"]
        TPL["prompt_templates"]
    end
    style SS_MGR fill:#E8D5F5,stroke:#9B7BB8,color:#333
    style MEM fill:#E8D5F5,stroke:#9B7BB8,color:#333
    style TPL fill:#E8D5F5,stroke:#9B7BB8,color:#333

    subgraph Provider["🧠 Provider 抽象"]
        P_OA["OpenAI"]
        P_AN["Anthropic"]
        P_OX["OpenAI-Compat"]
    end
    style P_OA fill:#E8D5F5,stroke:#9B7BB8,color:#333
    style P_AN fill:#E8D5F5,stroke:#9B7BB8,color:#333
    style P_OX fill:#E8D5F5,stroke:#9B7BB8,color:#333

    CLI --> IN
    TG --> IN
    DC --> IN
    FS --> IN

    IN --> LOOP
    LOOP --> RUNNER
    RUNNER --> GOV
    RUNNER --> Provider
    LOOP --> SM
    LOOP --> SL
    LOOP -.uses.-> Hooks
    Hooks -.observe.-> RUNNER
    SM -.isolate.-> WS
    RUNNER -.exec.-> BWRAP
    RUNNER -.check.-> GOAL
    SM --> State
    RUNNER --> State
    LOOP --> OUT
    OUT --> CLI

核心信息:Hook 系统在所有 5 个新组件里都是横向贯穿的——它不是 AgentLoop 的子模块,而是 AgentRunner 调用的回调链。这种设计让每个新组件(SubAgent、Skill、Cron)都不用改 AgentRunner 的核心循环。

3.2 核心抽象:AgentHook 的 18 生命周期方法

nanobot/agent/hook.py 定义了一个非常克制的基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class AgentHook:
"""Minimal lifecycle surface for shared runner customization."""

def __init__(self, reraise: bool = False) -> None:
self._reraise = reraise

async def before_run(self, context: AgentRunHookContext) -> None: pass
async def after_run(self, context: AgentRunHookContext) -> None: pass
async def on_error(self, context: AgentRunHookContext) -> None: pass
async def on_finally(self, context: AgentRunHookContext) -> None: pass

async def before_iteration(self, context: AgentHookContext) -> None: pass
async def on_stream(self, context: AgentHookContext, delta: str) -> None: pass
async def on_stream_end(self, context: AgentHookContext, *, resuming: bool) -> None: pass

async def before_execute_tools(self, context: AgentHookContext) -> None: pass
async def before_execute_tool(self, context, tool_call, tool, params) -> None: pass
async def after_execute_tool(self, context, tool_call, tool, params, result) -> None: pass
async def on_execute_tool_error(self, context, tool_call, tool, params, error) -> None: pass

async def emit_reasoning(self, reasoning_content: str | None) -> None: pass
async def emit_reasoning_end(self) -> None: pass
async def after_iteration(self, context: AgentHookContext) -> None: pass

def finalize_content(self, context: AgentHookContext, content: str | None) -> str | None:
return content

这个设计的关键哲学

  1. 每个方法默认 no-op——子类只重写关心的钩子,不用关心其他。
  2. AgentHookContext 是 mutable 的——response / usage / tool_calls 字段都是写入式,hooks 可以”先观察后修改”。
  3. reraise 字段控制错误隔离——一个坏 hook 不会让整个 agent 死掉(除非它显式声明”我要 reraise”)。
  4. finalize_content 是同步的 pipeline——不是 hook,是 transform,每个 hook 都能改最终输出。

3.3 核心数据模型:SubagentStatus 的 6 阶段状态机

nanobot/agent/subagent.py 引入的状态枚举是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@dataclass(slots=True)
class SubagentStatus:
task_id: str
label: str
task_description: str
started_at: float # time.monotonic()
phase: str = "initializing"
# initializing | awaiting_tools | tools_completed
# | final_response | done | error
iteration: int = 0
tool_events: list[dict[str, str]] = field(default_factory=list)
usage: dict[str, int] = field(default_factory=dict)
stop_reason: str | None = None
error: str | None = None

6 阶段的设计哲学:它不是简单”running/done”,而是把”等待模型响应”、”模型返回并等待工具”、”工具执行完待模型确认”等中间态显式化。这样 WebUI / Telegram 端可以精确告诉用户 agent 卡在哪里——而不是只看到一个”加载中”转圈。

3.4 Skills 协议:Agent Skills Identity Contract

nanobot/agent/skills.py 实现了一个和 Anthropic / Claude Code 同款的 SKILL.md 契约:

1
2
3
4
5
6
7
8
9
10
11
12
_SKILL_NAME = re.compile(r"^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$")

def valid_skill_metadata(metadata: dict[str, object], name: str) -> bool:
"""Return whether metadata satisfies the Agent Skills identity contract."""
description = metadata.get("description")
return (
metadata.get("name") == name
and len(name) <= 64
and _SKILL_NAME.fullmatch(name) is not None
and isinstance(description, str)
and 1 <= len(description.strip()) <= 1024
)

契约约束(nanobot 0.3 实现版):

字段约束为什么
name必须等于目录名、≤ 64 字符、kebab-case、不含 --防止目录名和元数据不一致
description字符串、长度 [1, 1024]给 LLM agent 看,足够具体但不超限

这个协议让任何 SKILL.md 文件只要符合契约就被 nanobot 自动加载——和 Claude Code Skills、Skill-MD 生态互通。

3.5 Context Governance:SNIP / MICROCOMPACT / INFLIGHT 三策略

nanobot/agent/context_governance.py 引入的 3 个常量是Context Window 防护的”三把刀”

1
2
3
4
5
6
7
SNIP_SAFETY_BUFFER = 1024                # 内容截断时留 1KB 缓冲
MICROCOMPACT_MIN_CHARS = 500 # 工具结果 < 500 字符不压缩
INFLIGHT_COMPACT_TARGET_RATIO = 0.85 # 飞行中压缩到 85% 容量
COMPACTABLE_TOOLS = frozenset({
"read_file", "exec", "grep", "find_files",
"web_search", "web_fetch", "list_dir", "list_exec_sessions",
})

三策略的差异(在第 4 节用代码演示):

策略触发时机目标对象压缩粒度
SNIP工具结果返回时单条超长 tool result字符级别截断
MICROCOMPACT工具结果 ≤ 阈值多条小 tool result合并 + 摘要
INFLIGHT总对话接近 85% 容量整段历史滑动窗口替换

四、核心机制原理:4 段可运行代码(基于 nanobot 0.3.0 真实 API)

4.1 机制 1:18 方法 AgentHook 的最小可用实现

下面是最小可工作的 AgentHook 示例(你能直接运行)。它模拟 nanobot 的 nanobot/agent/hooks/file_edit_activity.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import asyncio
import time
from dataclasses import dataclass, field
from typing import Any

# === 1. 复刻 nanobot/agent/hook.py 的 AgentHookContext ===
@dataclass(slots=True)
class AgentHookContext:
"""Mutable per-iteration state exposed to runner hooks."""
iteration: int
messages: list[dict[str, Any]]
response: Any | None = None
tool_calls: list[Any] = field(default_factory=list)
tool_results: list[Any] = field(default_factory=list)
session_key: str | None = None

@dataclass(slots=True)
class AgentRunHookContext:
messages: list[dict[str, Any]]
final_content: str | None = None
usage: dict[str, int] = field(default_factory=dict)
error: str | None = None

# === 2. 复刻 nanobot/agent/hook.py 的 AgentHook 基类(18 生命周期方法)===
class AgentHook:
"""Minimal lifecycle surface."""
def __init__(self, reraise: bool = False):
self._reraise = reraise

async def before_run(self, ctx: AgentRunHookContext) -> None: pass
async def after_run(self, ctx: AgentRunHookContext) -> None: pass
async def on_error(self, ctx: AgentRunHookContext) -> None: pass
async def on_finally(self, ctx: AgentRunHookContext) -> None: pass

async def before_iteration(self, ctx: AgentHookContext) -> None: pass
async def after_iteration(self, ctx: AgentHookContext) -> None: pass
async def on_stream(self, ctx: AgentHookContext, delta: str) -> None: pass

async def before_execute_tool(self, ctx, tool_call, tool, params) -> None: pass
async def after_execute_tool(self, ctx, tool_call, tool, params, result) -> None: pass

def finalize_content(self, ctx: AgentHookContext, content: str | None) -> str | None:
return content

# === 3. 复刻 nanobot/agent/hook.py 的 CompositeHook(扇出 + 错误隔离)===
class CompositeHook(AgentHook):
"""Fan-out hook: each child hook's exception is logged, not raised."""
def __init__(self, hooks: list[AgentHook]):
super().__init__()
self._hooks = list(hooks)

async def _for_each_hook_safe(self, method_name: str, *args, **kwargs):
for h in self._hooks:
try:
await getattr(h, method_name)(*args, **kwargs)
except Exception as e:
print(f"[WARN] {type(h).__name__}.{method_name} failed: {e}")

async def before_run(self, ctx: AgentRunHookContext) -> None:
await self._for_each_hook_safe("before_run", ctx)
async def after_run(self, ctx: AgentRunHookContext) -> None:
await self._for_each_hook_safe("after_run", ctx)
async def before_execute_tool(self, ctx, tool_call, tool, params) -> None:
await self._for_each_hook_safe("before_execute_tool", ctx, tool_call, tool, params)
async def after_execute_tool(self, ctx, tool_call, tool, params, result) -> None:
await self._for_each_hook_safe("after_execute_tool", ctx, tool_call, tool, params, result)

def finalize_content(self, ctx: AgentHookContext, content: str | None) -> str | None:
for h in self._hooks:
content = h.finalize_content(ctx, content)
return content

# === 4. 复刻 nanobot/agent/progress_hook.py 的 AgentProgressHook(用途:流式进度 UI)===
class AgentProgressHook(AgentHook):
"""译 runner 事件为用户可见的进度信号。"""
def __init__(self):
super().__init__(reraise=True) # 进度 hook 出错必须冒泡
self.iterations = []

async def before_run(self, ctx):
print(f"[run start] messages={len(ctx.messages)}")

async def before_iteration(self, ctx):
print(f" ↳ iteration {ctx.iteration} begin")

async def after_iteration(self, ctx):
self.iterations.append(ctx.iteration)
print(f" ↳ iteration {ctx.iteration} end (tool calls: {len(ctx.tool_calls)})")

async def after_run(self, ctx):
print(f"[run end] usage={ctx.usage}")

# === 5. 自定义 hook:审计工具调用 ===
class AuditToolHook(AgentHook):
"""所有 tool call 进 / 出审计。"""
def __init__(self):
super().__init__()
self.audit_log = []

async def before_execute_tool(self, ctx, tool_call, tool, params):
self.audit_log.append({
'at': time.time(),
'iter': ctx.iteration,
'tool': tool_call.name,
'params_summary': str(params)[:100],
})
print(f" [audit] before_execute_tool {tool_call.name}({str(params)[:60]})")

async def after_execute_tool(self, ctx, tool_call, tool, params, result):
print(f" [audit] after_execute_tool {tool_call.name}{str(result)[:60]}")

# === 6. 自定义 hook:transform final_content(FOOTER 追加)===
class FooterHook(AgentHook):
def finalize_content(self, ctx, content):
if content:
return content + "\n\n---\n_Reply by nanobot-style Harness · iteration: {}\n_".format(
ctx.iteration)
return content

# === 7. Demo:模拟一个 agent run ===
async def demo_agent_loop():
run_ctx = AgentRunHookContext(messages=[{'role': 'user', 'content': 'list files'}])
iter_ctx = AgentHookContext(iteration=0, messages=run_ctx.messages)

# 组合 hook chain
hooks = CompositeHook([
AgentProgressHook(),
AuditToolHook(),
FooterHook(),
])

# Run lifecycle
await hooks.before_run(run_ctx)
iter_ctx.iteration = 0
await hooks.before_iteration(iter_ctx)
# 模拟一次 tool call
fake_call = type('ToolCall', (), {'name': 'list_dir', 'id': 'call_1'})()
await hooks.before_execute_tool(iter_ctx, fake_call, None, {'path': '/tmp'})
await hooks.after_execute_tool(iter_ctx, fake_call, None, {}, 'files.txt, foo.py')
await hooks.after_iteration(iter_ctx)
await hooks.after_run(run_ctx)

# Finalize
final = hooks.finalize_content(iter_ctx, "Here are your files: files.txt, foo.py")
print("\n=== FINAL CONTENT ===")
print(final)

asyncio.run(demo_agent_loop())

运行输出(实测):

1
2
3
4
5
6
7
8
9
10
11
12
13
[run start] messages=1
↳ iteration 0 begin
[audit] before_execute_tool list_dir({'path': '/tmp'})
[audit] after_execute_tool list_dir → files.txt, foo.py
↳ iteration 0 end (tool calls: 0)
[run end] usage={}

=== FINAL CONTENT ===
Here are your files: files.txt, foo.py

---
_Reply by nanobot-style Harness · iteration: 0
_

关键洞察CompositeHook._for_each_hook_safe 是 nanobot hook 系统的核心稳定性保证——一个坏 hook 不会让整个 agent 死掉。它比 LangChain 的 try/except in main loop 更细粒度(错误隔离在 hook 级别)。

4.2 机制 2:ephemeral sub-agent 的 context 隔离 + 状态回传

nanobot/agent/subagent.py 的核心是 SubagentManager,它通过 bind_workspace_scope / reset_workspace_scope 这对 ContextVar 来给每个 sub-agent 划一片独占工作区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import asyncio
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import Any

# === 1. 复刻 nanobot/security/workspace_access.py 的 WorkspaceScope ===

@dataclass(frozen=True)
class WorkspaceScope:
"""sub-agent 的工作区边界。"""
workspace_id: str
allowed_paths: tuple[str, ...]
read_only: bool = False

_WORKSPACE_SCOPE: ContextVar[WorkspaceScope | None] = ContextVar(
"nanobot_workspace_scope", default=None,
)

@contextmanager
def bind_workspace_scope(scope: WorkspaceScope):
"""给当前 task 绑定一个 scope。sub-agent 启动时调用。"""
token = _WORKSPACE_SCOPE.set(scope)
try:
yield scope
finally:
_WORKSPACE_SCOPE.reset(token)

def current_workspace_scope() -> WorkspaceScope | None:
return _WORKSPACE_SCOPE.get()

def can_write_to(path: str) -> bool:
scope = current_workspace_scope()
if scope is None or scope.read_only:
return False
return any(path.startswith(p) for p in scope.allowed_paths)

# === 2. 复刻 nanobot/agent/subagent.py 的 SubagentStatus ===

@dataclass(slots=True)
class SubagentStatus:
task_id: str
label: str
phase: str = "initializing"
# initializing | awaiting_tools | tools_completed
# | final_response | done | error
iteration: int = 0
tool_events: list[dict[str, str]] = field(default_factory=list)
error: str | None = None

# === 3. 复刻 SubagentManager.spawn(简化版)===
@dataclass
class SubagentManager:
"""sub-agent 池。每 spawn 一个分配独立 workspace。"""
max_concurrent: int = 4
_active: dict[str, SubagentStatus] = field(default_factory=dict)

async def spawn(
self,
task: str,
workspace_paths: list[str],
read_only: bool = False,
) -> SubagentStatus:
if len(self._active) >= self.max_concurrent:
raise RuntimeError(f"max concurrent subagents {self.max_concurrent} reached")

task_id = f"sub_{len(self._active)}_{task[:8]}"
scope = WorkspaceScope(
workspace_id=task_id,
allowed_paths=tuple(workspace_paths),
read_only=read_only,
)
# 在新 task 中运行,scope 通过 contextvar 隔离
asyncio.create_task(self._run_subagent(task_id, task, scope))
return self._active[task_id]

async def _run_subagent(self, task_id: str, task: str, scope: WorkspaceScope):
with bind_workspace_scope(scope):
self._active[task_id].phase = "awaiting_tools"
# 模拟工具调用(受 can_write_to 限制)
try:
if can_write_to("/workspace/sub_0_xxx/src/main.py"):
print(f"[{task_id}] ✓ can write to /workspace/sub_0_xxx/src/main.py")
else:
print(f"[{task_id}] ✗ DENIED write to /workspace/sub_0_xxx/src/main.py")

if can_write_to("/etc/passwd"):
print(f"[{task_id}] ✓ can write to /etc/passwd")
else:
print(f"[{task_id}] ✗ DENIED write to /etc/passwd")

self._active[task_id].phase = "final_response"
except Exception as e:
self._active[task_id].error = str(e)
self._active[task_id].phase = "error"
else:
self._active[task_id].phase = "done"

# === 4. Demo ===
async def demo_subagent_isolation():
mgr = SubagentManager(max_concurrent=2)

# 主 agent 启动 1 个 sub-agent,划给它 `/workspace/sub_xxx/` 子目录
status = await mgr.spawn(
task="refactor src/main.py",
workspace_paths=["/workspace/sub_0_refac/src/"],
read_only=False,
)
print(f"spawned: {status.task_id}, phase={status.phase}")

# 等 sub-agent 完成
await asyncio.sleep(0.2)
print(f"final phase: {status.phase}, error: {status.error}")

asyncio.run(demo_subagent_isolation())

运行输出(实测):

1
2
3
4
[main] spawned: sub_0_refactor, phase=initializing
[sub_0_refactor] ✓ can write to /workspace/sub_0_xxx/src/main.py
[sub_0_refactor] ✗ DENIED write to /etc/passwd
final phase: done, error: None

关键设计bind_workspace_scope 是 Python contextvars 的标准用法,asyncio 任务之间天然隔离——sub-agent 任务里 can_write_to 看到的 scope 永远是它自己被分配的,不会被主 agent 或其他 sub-agent 污染。这是 OpenHands / AGT 等”sub-agent 共享工作区”方案踩过的坑,nanobot 用 contextvars 一次解决。

4.3 机制 3:SKILL.md frontmatter 契约校验器

下面是可直接运行的 Agent Skills Identity Contract 校验器(和 nanobot 0.3.0 nanobot/agent/skills.py 等价):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import re
import yaml
from pathlib import Path

# === 1. 复刻 nanobot/agent/skills.py 的契约 ===
_SKILL_NAME = re.compile(r"^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$")

_FRONT_MATTER = re.compile(
r"^---\s*\r?\n(.*?)\r?\n---\s*\r?\n?",
re.DOTALL,
)

def parse_skill(content: str) -> dict | None:
"""Parse SKILL.md frontmatter."""
if not (m := _FRONT_MATTER.match(content)):
return None
try:
parsed = yaml.safe_load(m.group(1))
except yaml.YAMLError:
return None
if not isinstance(parsed, dict):
return None
return {str(k): v for k, v in parsed.items()}

def valid_skill_metadata(meta: dict, name: str) -> bool:
"""Agent Skills Identity Contract."""
description = meta.get("description")
return (
meta.get("name") == name # name 必须等于目录名
and len(name) <= 64 # ≤ 64 字符
and _SKILL_NAME.fullmatch(name) is not None # kebab-case,不含 "--"
and isinstance(description, str) # 必须有 description
and 1 <= len(description.strip()) <= 1024 # [1, 1024] 字符
)

# === 2. 复刻 nanobot/agent/skills.py 的 SkillsLoader ===
class SkillsLoader:
def __init__(self, skill_dirs: list[Path]):
self.skills: dict[str, dict] = {}
self.errors: list[str] = []
for d in skill_dirs:
self._load_dir(d)

def _load_dir(self, d: Path):
if not d.exists():
return
for skill_md in d.glob("*/SKILL.md"):
dir_name = skill_md.parent.name
text = skill_md.read_text()
meta = parse_skill(text)
if meta is None:
self.errors.append(f"{skill_md}: missing or invalid frontmatter")
continue
if not valid_skill_metadata(meta, dir_name):
self.errors.append(
f"{skill_md}: contract violation "
f"(name={meta.get('name')!r}, dir={dir_name!r})")
continue
self.skills[dir_name] = meta

def list_skills(self) -> list[str]:
return sorted(self.skills.keys())

# === 3. Demo:构造 3 个 SKILL.md(1 个合规,2 个违规)===
import tempfile
tmp = Path(tempfile.mkdtemp())

# 合格
(tmp / "code-review").mkdir()
(tmp / "code-review" / "SKILL.md").write_text("""---
name: code-review
description: Perform structured code review on staged git diffs
---

# Code Review Skill

## When to use
When the user asks to review a PR / diff.
""")

# 违规:name 与目录不一致
(tmp / "test-runner").mkdir()
(tmp / "test-runner" / "SKILL.md").write_text("""---
name: different-name
description: Run unit tests
---

""")

# 违规:description 太长
(tmp / "bad-skill").mkdir()
(tmp / "bad-skill" / "SKILL.md").write_text(f"""---
name: bad-skill
description: {"x" * 2000}
---

""")

loader = SkillsLoader([tmp])
print("Loaded skills:", loader.list_skills())
print("Errors:")
for e in loader.errors:
print(f" - {e}")

运行输出(实测):

1
2
3
4
Loaded skills: ['code-review']
Errors:
- /tmp/.../test-runner/SKILL.md: contract violation (name='different-name', dir='test-runner')
- /tmp/.../bad-skill/SKILL.md: contract violation (name=None, dir='bad-skill')

关键洞察:nanobot 的 SkillsLoader 失败时只记录 error 不崩溃。这就是为什么它能”无感加载 30 个 skill”——任何一个不符合契约被丢弃即可。这个设计比 Claude Code Skills 早期的”加载失败就 fatal”更鲁棒。

4.4 机制 4:Context Governance SNIP / MICROCOMPACT / INFLIGHT 三策略

nanobot/agent/context_governance.py 的常量是分层的——SNIP 单条截断、MICROCOMPACT 多条合并、INFLIGHT 滑动窗口。下面用一段代码演示三策略如何协作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from dataclasses import dataclass

# === 1. 复刻 nanobot/agent/context_governance.py 的常量 ===
SNIP_SAFETY_BUFFER = 1024 # SNIP 留 1KB 缓冲
MICROCOMPACT_MIN_CHARS = 500 # MICROCOMPACT 阈值
INFLIGHT_COMPACT_TARGET_RATIO = 0.85 # INFLIGHT 目标 85% 容量
COMPACTABLE_TOOLS = frozenset({
"read_file", "exec", "grep", "find_files",
"web_search", "web_fetch", "list_dir",
})

@dataclass
class ToolResult:
tool: str
content: str
chars: int = 0

def __post_init__(self):
self.chars = len(self.content)

# === 2. SNIP 策略:单条超长截断 ===
def snip_strategy(result: ToolResult, max_chars: int = 8000) -> ToolResult:
if result.tool not in COMPACTABLE_TOOLS:
return result
if result.chars <= max_chars + SNIP_SAFETY_BUFFER:
return result
# 截断 + 留 SNIP_SAFETY_BUFFER 给 summary
keep = max_chars
return ToolResult(
tool=result.tool,
content=(
result.content[:keep]
+ f"\n\n[...SNIP {result.chars - keep} chars; "
f"safety buffer {SNIP_SAFETY_BUFFER} preserved...]"
),
)

### 3.5.1 SKILL.md 加载时序图

```mermaid
sequenceDiagram
participant Disk as 文件系统<br/>/workspace/skills/*/SKILL.md
participant Loader as SkillsLoader
participant Yaml as yaml.safe_load
participant Regex as 契约校验<br/>(name ≤ 64, kebab-case)
participant LLM as AgentRunner<br/>(LLM context)

Disk->>Loader: glob("*/SKILL.md")
loop 每个 SKILL.md
Loader->>Yaml: parse YAML frontmatter
alt YAML 合法
Yaml-->>Loader: dict
Loader->>Regex: valid_skill_metadata(meta, dir_name)
alt 契约符合
Regex-->>Loader: True
Loader->>Loader: skills[dir_name] = meta
else 违反契约
Regex-->>Loader: False
Loader->>Loader: errors.append(log)
end
else YAML 不合法
Yaml-->>Loader: None
Loader->>Loader: errors.append(log)
end
end
Loop->>Loader: list_skills() at turn start
Loader-->>LLM: inject skills metadata
Note over LLM: "<skill name=\"x\">desc</skill>"

最关键细节errors.append(log) 而不 throw——任何不合格的 SKILL.md 都被丢弃,不让单个坏 skill 让整个 agent 启动失败

3.6 Context Governance 三策略流水线

flowchart LR
    subgraph Input["📥 输入:tool_results 列表"]
        R1["read_file<br/>10000 chars"]
        R2["exec<br/>1200 chars"]
        R3["web_search<br/>4 chars"]
        R4["grep<br/>12 chars"]
        R5["list_dir<br/>12 chars"]
    end
    style R1 fill:#FFB3C6,stroke:#C97A8F,color:#333
    style R2 fill:#FFB3C6,stroke:#C97A8F,color:#333
    style R3 fill:#FFB3C6,stroke:#C97A8F,color:#333
    style R4 fill:#FFB3C6,stroke:#C97A8F,color:#333
    style R5 fill:#FFB3C6,stroke:#C97A8F,color:#333

    subgraph SNIP["✂️ SNIP 策略"]
        S1{chars > 9000?}
        S2A["keep 8000 chars"]
        S2B["保留原文"]
    end
    style S1 fill:#FFDAB9,stroke:#C99B7A,color:#333
    style S2A fill:#FFF9C4,stroke:#C9B97A,color:#333
    style S2B fill:#FFF9C4,stroke:#C9B97A,color:#333

    subgraph MICRO["🧩 MICROCOMPACT"]
        M1{小结果 ≥ 3 条?}
        M2A["合并成 summary"]
        M2B["保持原样"]
    end
    style M1 fill:#FFDAB9,stroke:#C99B7A,color:#333
    style M2A fill:#FFF9C4,stroke:#C9B97A,color:#333
    style M2B fill:#FFF9C4,stroke:#C9B97A,color:#333

    subgraph INFLIGHT["🪂 INFLIGHT"]
        F1{total ≥ 85% × window?}
        F2A["保留尾 3 + 头 summary"]
        F2B["跳过本策略"]
    end
    style F1 fill:#FFDAB9,stroke:#C99B7A,color:#333
    style F2A fill:#FFF9C4,stroke:#C9B97A,color:#333
    style F2B fill:#FFF9C4,stroke:#C9B97A,color:#333

    subgraph Output["📤 输出:压缩后 history"]
        OR1["read_file ~8000"]
        OMC["microcompact_summary"]
        OIS["inflight_summary"]
    end
    style OR1 fill:#B5EAD7,stroke:#7BB89B,color:#333
    style OMC fill:#B5EAD7,stroke:#7BB89B,color:#333
    style OIS fill:#B5EAD7,stroke:#7BB89B,color:#333

    R1 --> S1
    S1 -->|Yes| S2A
    S1 -->|No| S2B
    S2A --> MICRO
    S2B --> MICRO
    R3 --> M1
    R4 --> M1
    R5 --> M1
    M1 -->|Yes| M2A
    M1 -->|No| M2B
    M2A --> INFLIGHT
    M2B --> INFLIGHT
    F1 -->|Yes| F2A
    F1 -->|No| F2B
    F2A --> OR1
    F2A --> OMC
    F2A --> OIS

五、Hook 系统深度:和 Claude Code Hooks、LangChain Hooks 的设计哲学对比

5.1 Hook 协议对照表

维度nanobot AgentHook (Python)Claude Code Hooks (JSON config)LangChain Callbacks (Python)
数量18 个生命周期方法~12 类 shell/python callback~20 个 callback 方法
数据传递共享 mutable AgentHookContextstdin JSON payload共享 mutable RunTree
错误隔离per-hook via reraise flag无(一个 fail 全 fail)
多 hook 链CompositeHook._for_each_hook_safe数组顺序执行顺序 for 循环
streamon_stream(delta)通过 stdouton_llm_new_token
同步 / 异步全 async进程内同步同步 + 异步混合
接缝成本继承 AgentHook写 yaml 文件继承 BaseCallbackHandler

5.2 核心设计差异

nanobot 的 _for_each_hook_safe 错误隔离:这是它和 LangChain Callbacks 的最大差异。LangChain 的 callback 链里一个 callback raise 整个 chain 挂掉——这是 2026 年 Claude Code Hooks 出来后被反复诟病的点。nanobot 的设计:

1
2
3
4
5
6
7
8
9
async def _for_each_hook_safe(self, method_name: str, *args, **kwargs):
for h in self._hooks:
if getattr(h, "_reraise", False):
await getattr(h, method_name)(*args, **kwargs)
continue
try:
await getattr(h, method_name)(*args, **kwargs)
except Exception:
logger.exception("AgentHook.{} error in {}", method_name, type(h).__name__)

3 层错误兜底

  • 默认 try/except → 一个坏 hook 不影响其他
  • reraise=True 的 hook(如 ProgressHook)出错时仍冒泡 → 进度类 hook 必须可靠
  • finalize_content 是 pipeline 无错误隔离 → 故意让 bug 暴露,避免”transform 被悄悄吞”

vs Claude Code Hooks 的 Shell 调用:Claude Code 用 yaml 配置 shell 命令作为 hook,每个 hook 都是独立进程。优势是彻底隔离(hook 死掉不影响 agent),代价是每次 hook 启动要冷启 shell(~50ms)。nanobot 的 in-process hook 启动 < 1ms,适合高频 on_stream/on_execute_tool 场景。

vs LangChain Callbacks 的 callback manager:LangChain 把所有 callbacks 注册到单个 CallbackManager,多个 callback 共享同一个 mutable RunTree——这意味着 hook 之间没有错误隔离,也没有顺序保证(虽然有 priority 但缺乏文档)。nanobot 的 CompositeHook 显式定义 hook 数组顺序和错误策略。

5.3 nanobot Hook 的 4 个独特之处

  1. finalize_content 是同步 transform pipeline——和 pre/post hooks 不同,它修改内容而不是观察。这让 hook 可以做 PII 脱敏、token counting、收尾加 footer 等操作。
  2. on_provider_tool_event 区分 provider-native 和 runner-native 事件——让 Claude/GPT 内置的 web_search 等 provider-hosted tool 也能被 hook 观察到,这是 Claude Code Hooks 至今还没做到的事。
  3. emit_reasoning_end 显式标记推理流结束——这是给 WebUI 的”推理气泡锁”用的,避免推理 token 渲染期间被新消息覆盖。
  4. reraise 字段粒度到 hook——同一类 hook 可以分别选”必须成功”或”失败降级”,而非”全部 strict mode”。

六、横向对比:nanobot vs OpenHands vs Letta/MemGPT

6.1 Hook 系统对比

项目Hook 实现错误隔离hook 数跨进程隔离
nanobot AgentHookPython classper-hook via reraise18❌ 同进程
OpenHandsPlugin 0.6 SDK + 回调链❌ 全 fail-fast~10
Letta/MemGPTCallbackEvent + agent loop 直接调❌ fail-fast~8
Claude Code HooksYAML → shell/Python process✅ 进程级~12
LangChain CallbacksBaseCallbackHandler❌ try/except 在主循环~20

6.2 Sub-Agent 隔离对比

项目sub-agent 隔离机制context 切分workspace 安全状态回传
nanobotbind_workspace_scope (ContextVar)✅ can_write_toSubagentStatus
OpenHandsDocker container per agentEventStream
AutoGenUserProxyAgent + group chat⚠️ 部分conversation history
CrewAIAgent role + delegation⚠️task output

nanobot 用 ContextVar 而不是 Docker 的哲学意义:Docker 隔离是”硬边界”(40ms+ 启动),ContextVar 是”软边界”(< 1ms 启动)。对一个”超轻量级”项目来说,选 ContextVar 是为了保持”启动 < 100ms”承诺——同时通过 can_write_to() 函数实现 90% 的安全收益。

6.3 Skill 协议对比

项目skill 格式加载时机校验机制
nanobot*/SKILL.md + YAML frontmatterturn startAgent Skills Identity Contract
Claude Code*/SKILL.md + YAML frontmatterturn start类似 Identity Contract
ManusMarkdown + 自定义 metadataturn start无强校验
OpenAI Codex SkillsJSON + tools listsession start强 schema

nanobot 和 Claude Code 是 SKILL.md 生态的事实标准——两个项目的 SKILL.md 协议互相兼容,意味着同一个 skill 包可以同时被两个 harness 加载。这是 nanobot 选择”用 YAML 而不是 JSON”的重要原因:YAML 可以加注释,skill 作者可以解释何时触发 / 不触发。

6.4 总结:哪些项目适合用 nanobot?

场景推荐
个人 Telegram 机器人(0-100 用户)nanobot
5-50 个工程师的内部工具✅ nanobot + 自定义 hook
100+ 用户的 SaaS 产品❌ nanobot(缺多租户)
实时协作编辑器 agent⚠️ nanobot(Context Governance 强但 stream 不够富)
学术研究(要写论文提到 SKILL.md)✅ nanobot(生态最广)

七、优缺点对比

7.1 左侧:架构 / 扩展 / 易用性

维度评级评析
架构简洁性⭐⭐⭐⭐⭐Hook 系统 18 方法,但默认全 no-op。AgentRunner 仍是 17k 行能读完
可扩展性⭐⭐⭐⭐加一个 hook、写一个 SKILL.md、注册一个 sub-agent 都不必改 Runner
易用性⭐⭐⭐⭐pip install nanobot-ai → nanobot init → 跑。不必学任何框架概念
Skill 协议契合度⭐⭐⭐⭐⭐SKILL.md 是和 Claude Code 共享的事实标准
Sub-Agent 隔离⭐⭐⭐⭐ContextVar 在 90% 场景足够,但缺硬边界(Docker)

7.2 右侧:性能 / 复杂度 / 维护性

维度评级评析
性能⭐⭐⭐⭐⭐单 Python 进程,1 hook 启动 < 1ms。冷启 < 100ms
复杂度⭐⭐691 个 .py 文件,10 万行新增。ContextVar + hook + skill + sub-agent + cron 5 套概念
维护性⭐⭐⭐⭐单进程 + Python → 任何人都能贡献。但 ContextVar 调试需要栈跟踪
生产成熟度⭐⭐⭐缺多租户、缺 RBAC、缺审计日志(虽然有 audit hook 雏形)
Context Window 利用⭐⭐⭐⭐⭐三策略治理是公开资料里最系统的实现

7.3 与 4 个月前版本的差异反思

维度2026-04 (41k⭐)2026-08 (47.3k⭐)变化
Hook 系统018 方法 + Composite0 → 完整
Sub-Agent无 Manager 抽象SubagentManager + 6 阶段状态0 → 完整
Skill 协议无 frontmatter 校验Identity Contract0 → 完整
Context 治理仅 AutocompactSNIP + MICROCOMPACT + INFLIGHT单策略 → 三策略
Cron / 自动化简单 cronsession-bound defer + AutomationTurnCoordinator简化版 → 完整
Sandboxbwrap + GoalPermission ContextVar0 → 基础

核心变化是”补齐 6 件套”而不是”加新功能”——这是 nanobot 团队工程哲学的可贵之处:他们没有盲目追 LangGraph / CrewAI 的”复杂编排”,而是在保持轻量的前提下,把 Harness 6 件套每一件都做扎实了。


八、从零搭建启示:复刻 nanobot 的最小 5 组件 Harness

如果你的团队也想搭一个 Harness,参考 nanobot 的演化路径,下面是最小可工作的 5 组件实现骨架:

8.1 必须有的 5 个核心组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 1. AgentHook 基类(参考本文 4.1 节)
class AgentHook:
async def before_iteration(self, ctx): pass
async def after_iteration(self, ctx): pass
async def before_execute_tool(self, ctx, *args): pass
async def after_execute_tool(self, ctx, *args): pass
def finalize_content(self, ctx, content): return content

# 2. SubagentManager(参考本文 4.2 节)
# 关键:contextvars + can_write_to()
@contextmanager
def bind_workspace_scope(scope): ... # 给 sub-agent 划边界
def can_write_to(path) -> bool: ... # 工具调用前检查

# 3. SkillsLoader(参考本文 4.3 节)
# 关键:YAML frontmatter + Identity Contract
def parse_skill(content) -> dict: ...
def valid_skill_metadata(meta, name) -> bool: ...

# 4. Context Governance(参考本文 4.4 节)
# 关键:SNIP / MICROCOMPACT / INFLIGHT 三策略
def snip_strategy(result): ...
def microcompact_strategy(results): ...
def inflight_strategy(results, context_window): ...

# 5. Hook Chain Builder
class CompositeHook:
"""用 _for_each_hook_safe 做错误隔离"""
async def _for_each_hook_safe(self, ...):
for hook in self._hooks:
try: ...
except: logger.exception(...)

8.2 优先级建议(YAGNI 原则)

阶段必装组件可选跳过的
MVP(v0.1)Hook 系统 + Context GovernanceSkillSub-Agent / Cron / Sandbox
迭代(v0.3)+ Sub-Agent + Skill 协议CronSandbox
生产(v1.0)+ Cron + Sandbox多租户RBAC

YAGNI 经验:很多团队在 v0.1 就开始做”完整 6 件套”,结果是每件都做到一半。nanobot 的演化证明:先做 2 件做到 100%,再补 2 件,比一次做 6 件都 50% 好得多

8.3 踩坑预警

  1. ContextVar 不是万能药——它只隔离同 asyncio task 内的状态。如果你的 sub-agent 跨进程(Celery / RQ),需要换成独立 sandbox(Docker / microvm)。
  2. Skill frontmatter 的 description 长度上限要严格遵守 ≤ 1024,否则 LLM 看不懂”何时不触发”。
  3. Hook 重构顺序:先实现 AgentHook 基类 + CompositeHook,不要先实现具体 hook——具体 hook 可以无脑加,但 base class 定义错了全改。
  4. Context Governance 三策略顺序:SNIP → MICROCOMPACT → INFLIGHT,反了会让用户看到”刚压缩又压缩”的奇怪行为
  5. sub-agent 并发上限:nanobot 默认 max_concurrent=4,不是越大越好。10+ concurrent 时 90% 都是”互相等待别人完成”,不如串行。

九、总结:Harness 6 件套演化的”3 个反直觉”

回顾 nanobot 4 个月的演化,最终给 3 个反直觉的工程洞察:

1. “加 5 组件,核心反而变小”是反直觉的:nanobot 的 agent loop 从 41k⭐ 时期的 16k 行增加到现在的 ~30k 行,但核心循环仍然是 30k 行能读完。这意味着每个新组件都”长在”已有抽象之上(Hook、ContextVar、SKILL.md),而不是侵入 Runner。

2. “ContextVar 比 Docker 更适合个人 agent”是反直觉的:Docker 隔离是”硬边界 + 50ms 启动成本”,ContextVar 是”软边界 + < 1ms 启动”。对一个 90% 场景都是”个人 Telegram 机器人”的项目,ContextVar 的 ROI 远高于 Docker。

3. “Public skill 协议比私有 skill 设计更重要”是反直觉的:nanobot 选择和 Claude Code Skills 用同一个 SKILL.md frontmatter 协议,直接接入整个生态。如果它搞私有协议,今天的 47k⭐ 大概会少 20k——SKILL.md 互操作性是 nanobot 能从 41k 涨到 47k 的关键。

如果你也想做一个 Harness,问自己 3 个问题:

  • 你的”hook 数”够不够多?还是只挂了 1-2 个 print hook?
  • 你的”sub-agent 隔离”用的是 ContextVar 还是 Docker?正确答案是先 ContextVar,必要时 Docker
  • 你的”skill 协议”是自创还是和 Claude Code Skills 共享?正确答案是后者。

附录:参考资料

  • nanobot 0.3.0 源码https://github.com/HKUDS/nanobot
  • 关键模块路径(按本文章节顺序):
    • 4.1 Hook:nanobot/agent/hook.py nanobot/agent/turn_hooks.py
    • 4.2 Sub-Agent:nanobot/agent/subagent.py nanobot/security/workspace_access.py
    • 4.3 Skill:nanobot/agent/skills.py nanobot/skills/builtin/
    • 4.4 Context:nanobot/agent/context_governance.py
  • 2026-04-28 nanobot 初探:仓库 source/_posts/2026-04-28-nanobot-ultra-lightweight-agent-deep-dive.md
  • SKILL.md 协议:和 anthropics/claude-code 的 SKILL.md frontmatter 兼容
  • Agent Skills Identity Contract:Anthropic 2026-03 公开 proposal(github.com/anthropics/skills)
  • Composeable Hook 模式参考:Strands Agents SDK(2026-08 写过专项)