一个构建在 smolagents 之上的轻量运行时,为 agent 添加持久化工作区和线程,使其能够跨会话恢复长期任务。
A thin runtime built on smolagents, adding persistent workspaces and threads for long-lived agent tasks.
smolagents 提供了完善的 agent 循环:代码执行、工具调度、LLM 调用——但它是无状态的。每次 agent.run() 结束,所有运行时上下文随之消失。如果你需要一个 agent 在同一个目录里连续工作数十次,记住之前做过什么,并且不会因为上下文窗口爆满而崩溃,smolagents 本身不提供这些能力。
smolagents provides a solid agent loop — code execution, tool dispatch, LLM calls — but it's stateless. When agent.run() finishes, everything is gone. If you need an agent to work in the same directory across dozens of sessions, remember what it did before, and not crash from context overflow, smolagents doesn't cover that.
Runweave 补上了这个缺口。它不重新实现 smolagents 的任何功能——agent 循环、代码解析、工具调度全部由 smolagents 处理。Runweave 只负责 smolagents 不做的事:线程、持久化、上下文压缩和摘要。
Runweave fills that gap. It doesn't reimplement anything smolagents already does — agent loop, code parsing, tool dispatch all stay in smolagents. Runweave only handles what smolagents doesn't: threads, persistence, context compression, and summaries.
# 从源码安装(尚未发布到 PyPI)
# From source (package not yet on PyPI)
git clone https://github.com/AlexLiu190625/Runweave.git
cd Runweave
pip install -e .需要 Python 3.12+。
Requires Python 3.12+.
Runweave 通过 smolagents 调用 LLM。项目根目录有一个 .env.example 文件,复制并填入你的配置:
Runweave calls LLMs through smolagents. Copy the .env.example file in the project root and fill in your credentials:
cp .env.example .env.env 文件内容 / .env file contents:
# OpenAI(大部分示例使用)/ OpenAI (used by most examples)
OPENAI_API_KEY=your_api_key_here
OPENAI_BASE_URL=https://api.openai.com/v1
# 如果使用第三方代理,改为代理地址
# If using a third-party proxy, change to your proxy URL
# OPENAI_BASE_URL=https://api.your-proxy.com/v1
# Anthropic(参见 examples/08)/ Anthropic (see examples/08)
# ANTHROPIC_API_KEY=your_anthropic_key_here所有示例都会通过 python-dotenv 自动加载 .env,无需手动 export。也可以在代码中直接传参:
All examples auto-load .env via python-dotenv, no manual export needed. You can also pass credentials directly in code:
from smolagents import OpenAIServerModel
model = OpenAIServerModel(
model_id="gpt-5.3",
api_key="sk-...",
api_base="https://api.your-proxy.com/v1",
)一般不需要配置。Runtime 会从模型名自动查表确定 context window 大小,并按默认比例分配 token 预算。只有在需要微调时才手动传入 ContextBudget:
Usually no configuration needed. Runtime automatically looks up the context window size from the model name and allocates token budgets with sensible defaults. Only pass a custom ContextBudget if you need to tune:
from smolagents import OpenAIServerModel
from runweave import Runtime
from runweave.context import ContextBudget
model = OpenAIServerModel(model_id="gpt-5.3")
budget = ContextBudget(
model_id=model.model_id, # 保持和 model 一致 / keep in sync with model
buffer_tokens=8192, # 预留给输出的安全余量,默认 4096 / output margin, default 4096
instruction_ratio=0.40, # 指令占可用 token 的比例,默认 0.25 / instruction share, default 0.25
)
rt = Runtime(model=model, context_budget=budget)三个参数的含义 / What each parameter does:
buffer_tokens— 从 context window 中扣除的安全余量,预留给模型输出和系统开销。默认 4096。/ Tokens reserved from the context window for model output and overhead. Default 4096.instruction_ratio— 剩余 token 中分给跨 run 指令(历史 + 摘要 + 技能目录)的比例,其余留给 run 内步骤历史。默认 0.25。比例越大,注入的历史越详细,但留给 agent 思考的空间越小。/ Share of remaining tokens for cross-run instructions (history + summary + skill catalog); the rest goes to intra-run step history. Default 0.25. Higher ratio = more history detail, less room for agent reasoning.
详见 examples/05_context_budget.py。
See examples/05_context_budget.py for a working example.
from smolagents import OpenAIServerModel
from runweave import Runtime
model = OpenAIServerModel(model_id="gpt-5.3")
rt = Runtime(model=model)
# Run 1: create a script
result = rt.run("Create a Python script that generates Fibonacci numbers.")
print(result.thread_id) # a3f7c2b1
print(result.output) # "Created fibonacci.py..."
# Run 2 (days later): continue the work
result = rt.run(
"Add error handling and tests.",
thread_id="a3f7c2b1",
)
# The agent already knows what was done in Run 1,
# without receiving the full memory — just a summary.在 Run 1 和 Run 2 之间,Runweave 做了这些事:
Between Run 1 and Run 2, Runweave:
-
把 Run 1 的完整记忆保存到
~/.runweave/threads/a3f7c2b1/memory.json -
通过 LLM 生成了一段约 200 字的摘要:"Created fibonacci.py with a recursive implementation..."
-
把摘要写入
summary.txt -
在 Run 2 启动时,把这段摘要注入 agent 的指令中
-
Agent 看到摘要后,在工作区里找到了
fibonacci.py,读取它,添加了错误处理和测试——整个过程中它没有看到 Run 1 的原始记忆 -
Saved Run 1's full memory to
~/.runweave/threads/a3f7c2b1/memory.json -
Generated a ~200-word summary via LLM: "Created fibonacci.py with a recursive implementation..."
-
Persisted the summary to
summary.txt -
In Run 2, injected that summary into the agent's instructions
-
The agent saw the summary, found
fibonacci.pyin the workspace, read it, and added tests — without ever seeing Run 1's raw memory
线程是 Runweave 的基本工作单元。一个线程拥有自己的工作区目录、内存归档和运行历史。线程之间相互隔离。
A thread is Runweave's unit of work. Each thread owns its own workspace directory, memory archive, and run history. Threads are isolated from each other.
一次 runtime.run(task, thread_id) 调用就是一次 run。同一个线程可以有很多次 run。每次 run 结束后,Runweave 会归档 agent 的记忆、记录执行步骤、生成运行摘要。
One call to runtime.run(task, thread_id) is a run. A thread can have many runs. After each run, Runweave archives the agent's memory, records execution steps, and generates a summary.
当你在已有线程上开始新 run 时,agent 不会收到之前所有 run 的完整记忆——那会撑爆上下文。它收到的是一段 200-300 字的压缩摘要,由 LLM 在上一次 run 结束时生成。
When you start a new run on an existing thread, the agent doesn't receive full memory of all prior runs — that would blow up the context window. Instead it receives a compressed summary of everything done so far, a 200-300 word narrative generated by the LLM after the previous run.
这是一个有意的取舍:agent 不能完美回忆过去说过的每一个字,但线程可以跑上百次而不会溢出。
This is a deliberate trade-off: the agent can't perfectly recall every word from past runs, but threads can survive hundreds of runs without overflow.
Runweave 有三层,依赖方向严格向下。
Runweave has three layers with strict downward dependency.
Layer 3: Runweave Runtime Shell
Runtime, Thread, ThreadStore, MemoryIO,
SummaryGenerator, HistoryWriter, SkillLoader, ToolLoader,
ContextBudget, InstructionCompressor, StepCompressor
|
v
Layer 2: Runweave Executor Extension
WorkspaceExecutor (subclasses LocalPythonExecutor, ~30 lines)
|
v
Layer 1: smolagents (imported, never modified)
CodeAgent, Tool, LocalPythonExecutor, AgentMemory, ...
第一层是 smolagents,原封不动地导入使用。第二层只有一个类 WorkspaceExecutor,在执行代码前切换到线程的工作目录。第三层是 Runweave 自身的代码,全是 smolagents 没有对应功能的东西。
Layer 1 is smolagents, imported as-is. Layer 2 is a single class WorkspaceExecutor that chdirs into the thread's workspace before executing code. Layer 3 is Runweave's own code — everything that smolagents doesn't provide.
task, thread_id
|
v
[1] Load or create thread from ThreadStore
[2] Build WorkspaceExecutor for thread's workspace
[3] Collect instructions: user prompt + skill catalog + run history + thread summary
[4] Compress instructions within token budget (InstructionCompressor)
[5] Merge tools: user tools + custom tools (ToolLoader) + skill tools (SkillLoader)
[6] Build smolagents CodeAgent with step compression callback
[7] agent.run(task) — smolagents handles the loop
[8] Extract which skills were used during this run
[9] Save agent memory to disk (for inspection, not replay)
[10] Write run record (run-NNN.json/md), regenerate HISTORY.md
[11] Generate/update thread summary via LLM
[12] Return RunResult
其中第 7 步以外的所有步骤都是 Runweave 的工作。第 7 步完全由 smolagents 处理——agent 循环、代码执行、工具调度、LLM 调用、最终答案检测,Runweave 不碰这些。
Everything except step 7 is Runweave's job. Step 7 is entirely smolagents — agent loop, code execution, tool dispatch, LLM calls, final answer detection. Runweave doesn't touch any of that.
Runweave 有三个组件处理上下文管理,各管一层:
Runweave has three components for context management, each handling a different layer:
- ContextBudget — 配置 token 预算和分配比例,其他两个组件都读它 / Configures token budget and allocation ratios; the other two components read from it.
- InstructionCompressor — 压缩跨 run 的指令(历史 + 摘要 + 技能目录)/ Compresses cross-run instructions (history + summary + skill catalog).
- StepCompressor — 压缩单次 run 内的步骤历史(通过
step_callbacks)/ Compresses intra-run step history (viastep_callbacks).
长时间运行的 agent 有一个现实问题:上下文窗口会满。smolagents 没有处理这个问题——它每步都把完整记忆发给 LLM,直到 API 报错。
Long-running agents have a practical problem: the context window fills up. smolagents doesn't handle this — it sends the full memory to the LLM every step until the API errors out.
Runweave 在两个层面解决这个问题:
Runweave addresses this at two levels:
跨 run 压缩 (InstructionCompressor):注入给 agent 的指令(用户提示 + 技能目录 + key_facts + 线程摘要 + 运行历史)会被压缩到 token 预算内。历史记录超长时按 U 形衰减 逐级压缩:头部和尾部 run 始终保留 FULL,中段按距尾部远近渐进降级(TAKEAWAY → TITLE → LOG_LINE)。预算紧张时只压中段,头尾恒定。用户指令、key_facts、摘要永不裁剪。
Cross-run compression (InstructionCompressor): Instructions injected into the agent (user prompt + skill catalog + key_facts + thread summary + run history) are compressed within a token budget. When history gets too long, it's compressed via U-shaped decay: head runs and tail runs are always rendered FULL; middle runs are progressively bucketed by distance from tail (TAKEAWAY → TITLE → LOG_LINE). Budget pressure only compresses the middle; head and tail are fixed. User instructions, key_facts, and the summary are never trimmed.
N=10 history decay (head_count=2, tail_count=3):
Run: 1 2 3 4 5 6 7 8 9 10
Level: FULL FULL LOG TITLE TITLE TAKEAWAY TAKEAWAY FULL FULL FULL
└──head──┘ └────────── middle ──────────────────┘ └─────tail──────┘
ContextBudget(head_count=N, tail_count=M) 可调节锚定窗口大小。长 thread (N > 20) 配合 key_facts + summary 时,可设 head_count=0 关闭头部固定。
ContextBudget(head_count=N, tail_count=M) tunes the anchor window. For long threads (N > 20) using key_facts + summary, head_count=0 is reasonable — the early direction signal is already captured by those tracks.
单次 run 内压缩 (StepCompressor):通过 smolagents 的 step_callbacks 扩展点,每步结束后检查实际 token 使用量。超过阈值时,对旧步骤逐级压缩——截断输出、清除推理过程、最后完全清除代码和输出。最近的 3 步始终保持完整。
Intra-run compression (StepCompressor): Via smolagents' step_callbacks extension point, actual token usage is checked after each step. When above threshold, old steps are progressively compressed — truncate output, clear reasoning, finally clear code and output entirely. The most recent 3 steps always remain intact.
技能是可复用的指令文档,带有可选的脚本和参考文件。agent 在运行时按需加载技能。
Skills are reusable instruction documents with optional scripts and reference files. The agent loads skills on demand during a run.
skills/
deploy/
SKILL.md # frontmatter + instructions
scripts/
check_status.sh
references/
runbook.md
SKILL.md 格式 / SKILL.md format:
---
name: "deploy"
description: "Production deployment procedures"
---
# Deploy
Step-by-step instructions for deployment...Runweave 自动为 agent 注册三个工具:load_skill(加载指令)、read_skill_resource(读取参考文件)、run_skill_script(执行脚本)。agent 看到技能目录后自行决定何时加载哪个技能。
Runweave automatically registers three tools for the agent: load_skill (load instructions), read_skill_resource (read reference files), run_skill_script (execute scripts). The agent sees the skill catalog and decides when to load which skill.
在指定目录下放置 .py 文件,Runweave 的 ToolLoader 会自动发现并加载 smolagents Tool 实例。
Place .py files in a specified directory and Runweave's ToolLoader will automatically discover and load smolagents Tool instances.
# tools/search.py
from smolagents import Tool
class SearchTool(Tool):
name = "search"
description = "Search the web"
inputs = {"query": {"type": "string", "description": "Search query"}}
output_type = "string"
def forward(self, query: str) -> str:
...
search = SearchTool() # must be instantiated at module levelrt = Runtime(
model=model,
tools_dir=Path("./tools"),
)~/.runweave/threads/<thread-id>/
workspace/ # agent 的工作目录 / agent's working directory
memory.json # 归档记忆(仅供检查)/ archived memory (inspection only)
summary.txt # 线程摘要(叙事)/ thread summary (narrative)
key_facts.md # 关键事实(锚点)/ curated anchor facts
HISTORY.md # 运行历史索引 / run history index
runs/
run-001.json # 运行记录 / run record
run-001.md # 可读报告 / readable report
...
meta.json # {id, created_at}
memory.json 保存了完整的 agent 记忆,但仅供检查——不会在下一次 run 时注入回 LLM 上下文。
memory.json stores the full agent memory, but only for inspection — it is not injected back into the LLM context on the next run.
每次 run 结束后,Runweave 会并行运行两次独立的 LLM 调用,产出两个互补的制品:
After each run, Runweave fires two independent LLM calls in parallel, producing two complementary artifacts:
-
summary.txt— 叙事摘要 / narrative summary:概括本轮做了什么、当前状态如何。随 run 次数累积并在超过阈值时压缩重写。 -
summary.txt— narrative summary: describes what happened this run and the current state. Grows across runs, then condenses when it passes a word-count threshold. -
key_facts.md— 关键事实 / curated anchor facts:受 MemReader 启发,走独立 LLM triage,只保留目标、硬约束、决策、产物等稳定事实。每条带[run N]前缀;新事实 supersede 旧事实时替换而非追加。它是 thread 的锚点,不随近期活动稀释。 -
key_facts.md— curated anchor facts: MemReader-inspired selective distillation. Retains only goals, hard constraints, decisions, and produced artifacts, each tagged with[run N]. A new fact that supersedes an existing one replaces it rather than appending. This is the thread's anchor — it resists dilution by recent activity.
在下一轮 run 启动时,两者都会注入到 agent 的指令中(key_facts 在前),优先级高于 history,预算紧张时 history 先被砍。
On the next run, both are injected into the agent's instructions (key_facts first) and ranked above history. When the instruction budget is tight, history is cut before either.
-
已有 thread 在 v0.2 之前没有
key_facts.md。首次 resume 后,distiller 会基于该 run 的 task/output 生成一个初始key_facts.md,随后每轮持续演化。 -
不需要手动迁移,也不需要删除已有 thread。历史 run 不会追溯补录到 key_facts(distiller 只看当前 run + 已有的 key_facts 文件)。
-
如果某个 thread 的 key_facts 需要手动编辑或清空,直接修改/删除
~/.runweave/threads/<id>/key_facts.md即可——下一次 run 会从文件的当前状态继续。 -
Threads created before v0.2 have no
key_facts.md. On the first resume after upgrade, the distiller produces an initial file from that run's task/output; subsequent runs evolve it normally. -
No manual migration is required. Historical runs are not retroactively back-filled (the distiller only sees the current run + the existing
key_facts.md). -
To manually curate or reset a thread's key facts, edit or delete
~/.runweave/threads/<id>/key_facts.md— the next run continues from whatever the file currently contains.
Runtime(
model: Model, # smolagents model instance
tools: list[Tool] | None = None, # tools passed directly
instructions: str | None = None, # additional instructions appended to smolagents' built-in system prompt
base_dir: Path | None = None, # data dir, default ~/.runweave
additional_authorized_imports: list[str] = None, # extra imports for executor
skills_dir: Path | None = None, # skills directory
tools_dir: Path | None = None, # tools directory
context_budget: ContextBudget | None = None, # token budget config
)result = rt.run(
task: str, # task description
thread_id: str | None = None, # thread ID, None to auto-create
tool_names: list[str] | None = None, # select specific tools from tools_dir
)@dataclass
class RunResult:
output: Any # agent's final output
thread_id: str # thread ID
state: str # "success" | "error" | ...
step_count: int # number of steps executed
token_usage: dict | None # token usage stats
timing: dict | None # timing stats
summary: str # thread summary after this run
skills_used: list[str] # skills loaded during this runRuntime 用一个 model 跑到底;当任务跨越多个 step 且 step 难度差异大时,更好的选择是 PlanningRuntime。它由 planner LLM 一次性出计划 + Router 按 step metadata 路由 model + 逐 step 用 smolagents.CodeAgent 执行 三段组成。
Runtime runs a single model end-to-end. For multi-step tasks where steps differ widely in difficulty, PlanningRuntime is the better fit: a planner LLM produces a plan once, the Router picks the best-fit model per step from its metadata, and each step is executed by smolagents.CodeAgent.
from runweave import ModelProfile, PlanningRuntime, Router
from smolagents import OpenAIServerModel
haiku = OpenAIServerModel(model_id="claude-haiku-4-5-20251001")
sonnet = OpenAIServerModel(model_id="claude-sonnet-4-6")
opus = OpenAIServerModel(model_id="claude-opus-4-7")
models = [
ModelProfile(model=haiku, context_window=200_000, supports_tools=True,
supports_structured_output=True, coding_score=0.7,
long_context_score=0.65, latency="low", cost_tier="low"),
ModelProfile(model=sonnet, context_window=1_000_000, supports_tools=True,
supports_structured_output=True, coding_score=0.9,
long_context_score=0.85, latency="medium", cost_tier="medium"),
ModelProfile(model=opus, context_window=1_000_000, supports_tools=True,
supports_structured_output=True, coding_score=0.95,
long_context_score=0.9, latency="high", cost_tier="high"),
]
rt = PlanningRuntime(planner_model=opus, models=models, router=Router())
result = rt.run("Build a dataclass + tests + README")PlanningRuntime 是 orchestrator 而非 agent:它不调 LLM 决定"下一步做什么"。下一步由 plan.json 拓扑确定;重规划只在 step 失败 / 超时 / 产物缺失时被确定性触发,且总次数封顶(默认 max_replans=3)。
PlanningRuntime is an orchestrator, not an agent: it never calls an LLM to decide "what next." The next step comes from plan.json topologically; replans are triggered only on deterministic failure conditions (step failed / timed out / expected output missing), capped at max_replans=3 by default.
每次 PlanningRuntime.run() 在 thread 下写一个活动 plan.json,结束时归档到 plans/plan-NNN.json。归档里能看到每个 step 的 selected_model_id、状态、输出、失败原因——一个完整的可追溯轨迹。
Each PlanningRuntime.run() writes an active plan.json under the thread, archived to plans/plan-NNN.json on completion. The archive records selected_model_id, status, output, and failure reason for every step — a fully traceable record.
完整示例:examples/11_planning_runtime.py。
Full example: examples/11_planning_runtime.py.
PlanningRuntime(
planner_model=opus, # 出计划 + 默认跑 summary/key_facts
models=[...],
summary_model=haiku, # 可选:用便宜模型跑 summary/key_facts 省钱
router=Router(),
step_timeout_seconds=600, # 单 step 超时
max_step_iterations=30, # step 执行总次数上限
max_replans=3, # 跨-step 重规划次数上限
)summary_model 默认 fallback 到 planner_model。设置成便宜模型可显著降本——典型场景里 summary + key_facts 占总 token 的 5-15%,用 Haiku 替代 Opus 可省 70%+ 这部分开销。
summary_model defaults to planner_model. Pointing it at a cheaper model can meaningfully reduce cost — summary + key_facts typically account for 5-15% of total tokens, and using Haiku instead of Opus saves 70%+ on that slice.
RunResult.token_usage 完整聚合了 5 类 LLM 调用:planner、replan、每个 step 的 CodeAgent、summary、key_facts。审计可信。
RunResult.token_usage aggregates all five LLM call categories: planner, replan, per-step CodeAgent, summary, and key_facts. Fully auditable.
expected_outputs只检查文件存在,不检查修改。如果上游 step 已经创建了该文件而当前 step 声称它作为产出但实际什么都没做,检查仍然通过。Workaround:planner 不要让多个 step 共用expected_outputs。v0.4 会加 mtime 追踪。expected_outputschecks file existence only, not modification. If an upstream step already created the file and the current step declared it as an output but did nothing, the check still passes. Workaround: planner should not duplicateexpected_outputsacross steps. v0.4 will add mtime tracking.- 当前所有 step 顺序执行,即使彼此无依赖。并发执行 defer 到 v0.4。
- All steps currently execute sequentially even when independent. Parallel execution is deferred to v0.4.
- 没有
PlanningRuntime.run_stream()。多 step 流式语义 defer 到 v0.4。 - No
PlanningRuntime.run_stream()yet. Multi-step streaming semantics are deferred to v0.4.
运行时依赖只有一个:smolagents[openai]==1.24.0。开发依赖:pytest、python-dotenv。
The only runtime dependency is smolagents[openai]==1.24.0. Dev dependencies: pytest, python-dotenv.
Apache License 2.0. See LICENSE.