tiny-agent 0.3.0

一个小而完整的 Rust LLM Agent 运行时:可中断、可恢复、可观测、可插拔的 agent loop / A small but complete LLM agent runtime in Rust — an interruptible, resumable, observable, pluggable agent loop.
Documentation
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
<h1 align="center">tiny-agent</h1>

<p align="center">
  <strong>A composable LLM agent runtime for Rust.</strong><br>
  可中断、可恢复、可观测、可插拔的 agent 运行时。
</p>

<p align="center">
  <a href="https://crates.io/crates/tiny-agent"><img src="https://img.shields.io/crates/v/tiny-agent.svg" alt="Crates.io"></a>
  <a href="https://docs.rs/tiny-agent"><img src="https://docs.rs/tiny-agent/badge.svg" alt="Docs.rs"></a>
  <a href="#许可证"><img src="https://img.shields.io/crates/l/tiny-agent.svg" alt="License"></a>
  <img src="https://img.shields.io/badge/rustc-1.85%2B-blue.svg" alt="MSRV">
</p>

---

tiny-agent 把 “调用模型 → 执行工具 → 回灌结果” 的循环建模成一个**显式状态机**,并将模型、沙箱、存储、可观测与拦截全部抽象为 trait。框架本身不绑定任何后端:用哪家模型、状态存到哪里、工具跑在何种隔离环境,皆由调用方决定。

它面向需要把 agent 嵌进真实服务的场景——进程会重启、用户会取消、工具有副作用、运行过程要审计——这些工程负担都被收敛进清晰的边界里,而不是散落在业务代码中。

```toml
[dependencies]
tiny-agent = "0.3"
```

## 工作原理

一次运行就是状态之间的转移。每个状态都携带可序列化的 `AgentState`,这是 checkpoint 与恢复能成立的基础。

```text
 submit ─▶ Reasoning ─┬─▶ ToolExecuting ─▶ Reasoning   执行工具、回灌结果,继续推理
                      ├─▶ WaitingForUser                工具请求用户输入,停车等待
                      └─▶ Success                       模型给出最终回复(终态)

         cancel ─▶ Interrupted ─▶ resume               任意安全点取消并落 checkpoint,可续跑
         error  ─▶ Fail                                 超过重试 / 迭代上限(终态)
```

## 特性

- **状态机式的 agent loop** — `Ready → Reasoning → ToolExecuting → …` 全程显式建模,每个状态都可序列化。
- **Checkpoint 与恢复** — 中断点写快照,`resume` 从断点续跑,终态自动清理。
- **协作式取消** — 传入 `CancellationToken` 即可在安全点停车;未应答的工具调用会被补齐占位结果,transcript 不留孤儿 `tool_use`。
- **后端无关** — Provider、Sandbox、两类存储、实时消息与 Middleware 边界清晰,可任意替换。
- **内置 Provider** — Anthropic 与 OpenAI 兼容端点(DeepSeek、OpenAI 及各类兼容服务),均为流式。
- **沙箱抽象** — 触达外部世界的工具统一经 `Sandbox::execute`,自带 `HostSandbox`,换隔离方案不改工具代码。
- **类型安全的工具** — 参数 struct 经 `schemars` 自动生成 JSON Schema;只读工具之间自动并发调度。
- **Skills** — 按 `SKILL.md` 约定从目录发现技能,模型按需加载,不必把说明一次性塞满上下文。
- **中间件** — 洋葱模型的生命周期钩子,可改写请求/消息/工具结果,或在工具执行前短路。
- **可观测** — 按 OpenTelemetry GenAI 语义约定产出嵌套 span(`agent.run` / `user_input` / `gen_ai.chat` / `tool.call`),含完整输入输出、工具入参结果与 token 用量;由宿主程序决定是否接入 OTLP、日志或监控平台。
- **子 Agent** — `spawn_agent` 把子任务委托给独立配置的子 agent。
- **健壮性** — 区分可重试与永久错误,指数退避 + jitter,并限制最大连续失败与迭代次数。

## 快速开始

`AgentRunTime` 需要五样必填依赖:一个 **Provider**、一个 **model** 名、两类**存储**(checkpoint 与 transcript)、一个 **sandbox**。Sandbox 框架自带 `HostSandbox`;存储 trait 由你按后端实现(内存、文件、SQLite、Redis……)。

下面用内存存储跑通一轮完整的 agent loop,完整版见 [`src/main.rs`](src/main.rs):

```rust
use std::{collections::HashMap, sync::{Arc, Mutex}};
use async_trait::async_trait;
use tiny_agent::{
    Agent, AgentRunTimeBuilder, StorageError,
    checkpoint::CheckpointStorage,
    providers::openai_compatible::OpenAiCompatibleProvider,
    sandbox::HostSandbox,
    tools::{ToolRegistry, register_default_tools},
    transcript::{Transcript, TranscriptStorage},
};
use tokio_util::sync::CancellationToken;

// 最小内存存储:同时实现 checkpoint 与 transcript 两个 trait。
// Agent 没有 Clone 但可序列化,所以 checkpoint 按 JSON 字符串存。
#[derive(Default)]
struct MemoryStore {
    checkpoints: Mutex<HashMap<String, String>>,
    transcripts: Mutex<HashMap<String, Transcript>>,
}

#[async_trait]
impl CheckpointStorage for MemoryStore {
    async fn save_checkpoint(&self, sid: &str, agent: &Agent) -> Result<(), StorageError> {
        let json = serde_json::to_string(agent).map_err(|e| StorageError::serde(e.to_string()))?;
        self.checkpoints.lock().unwrap().insert(sid.into(), json);
        Ok(())
    }
    async fn get_checkpoint(&self, sid: &str) -> Result<Option<Agent>, StorageError> {
        match self.checkpoints.lock().unwrap().get(sid).cloned() {
            Some(j) => Ok(Some(serde_json::from_str(&j).map_err(|e| StorageError::serde(e.to_string()))?)),
            None => Ok(None),
        }
    }
    async fn delete_checkpoint(&self, sid: &str) -> Result<(), StorageError> {
        self.checkpoints.lock().unwrap().remove(sid);
        Ok(())
    }
}

#[async_trait]
impl TranscriptStorage for MemoryStore {
    async fn save_transcript(&self, sid: &str, t: &Transcript) -> Result<(), StorageError> {
        self.transcripts.lock().unwrap().insert(sid.into(), t.clone());
        Ok(())
    }
    async fn get_transcript(&self, sid: &str) -> Result<Option<Transcript>, StorageError> {
        Ok(self.transcripts.lock().unwrap().get(sid).cloned())
    }
    // 只需实现 remove_transcript(删单个会话);delete_transcript 由 trait 提供,
    // 默认会沿 subsession 递归级联删除整棵子会话树。
    async fn remove_transcript(&self, sid: &str) -> Result<(), StorageError> {
        self.transcripts.lock().unwrap().remove(sid);
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = Arc::new(OpenAiCompatibleProvider::new(
        std::env::var("LLM_API_KEY")?,
        "https://api.deepseek.com/v1".to_string(),
    ));
    let store = Arc::new(MemoryStore::default());

    let mut tools = ToolRegistry::new();
    register_default_tools(&mut tools); // read_file / write_file / bash / check / kill / askFollowupQuestion

    let runtime = AgentRunTimeBuilder::new()
        .provider(provider)
        .model("deepseek-chat")
        .checkpoint_storage(store.clone())
        .transcript_storage(store.clone())
        .sandbox(Arc::new(HostSandbox::new()))
        .tools(tools)
        .system_prompt("你是一个简洁高效的助手。")
        .build()?;

    let session_id = runtime.create_session().await?;
    let ready = runtime.submit(&session_id, "用 bash 执行 echo hello 并总结").await?;
    if let Agent::Success(state) = runtime.run(ready, CancellationToken::new()).await? {
        println!("{}", state.final_resp.unwrap_or_default());
    }
    Ok(())
}
```

```bash
# 默认指向 DeepSeek,可用 LLM_BASE_URL / LLM_MODEL 覆盖
LLM_API_KEY=sk-xxx cargo run -- "用 bash 执行 echo hello,然后用一句话总结"
```

> `HostSandbox` 会在**本机**直接执行 bash 命令,`register_default_tools` 也会注册文件读写与 shell 工具;这套组合适合本地试跑和受信任环境。对外提供服务时,请实现隔离型 `Sandbox`,并按业务白名单手动注册工具。

## 两种构造方式

上面的 `AgentRunTimeBuilder` 是链式写法。等价地,你也可以先组一个 `AgentRuntimeConfig`,再用 `AgentRunTime::from_config` 构造——当配置来自外部(反序列化、依赖注入、需要在多个 runtime / 子 agent 间复用同一份配置)时更顺手。

```rust
use std::sync::Arc;
use tiny_agent::{
    AgentRunTime, AgentRuntimeConfig, AgentRuntimeConfigInput, AgentRuntimeOptions,
    AgentRuntimeStorage,
    middleware::MiddlewareChain,
};

let config = AgentRuntimeConfig::new(AgentRuntimeConfigInput {
    provider,
    model: "deepseek-chat".to_string(),
    storage: AgentRuntimeStorage::shared(store.clone()),
    sandbox: sandbox.clone(),
    options: AgentRuntimeOptions {
        tools,
        system_prompt: Some("你是一个简洁高效的助手。".to_string()),
        middleware: MiddlewareChain::new().with(Arc::new(Guardrail)),
        temperature: Some(0.2),
        max_iter: 20,
        skill_dirs: vec!["./skills".into()],
        ..Default::default()
    },
});

let runtime = AgentRunTime::from_config(config)?;
```

`AgentRunTimeBuilder` 内部也只是收集这些字段后调 `from_config`,两者完全等价,按喜好选用。

## 可选 feature

tiny-agent 默认不带文件存储和 exporter,应用可按需打开:

| Feature | 提供内容 |
| --- | --- |
| `file-storage` | `storage::FileStorage`,把 checkpoint / transcript 以 JSON 落盘 |
| `otel-tempo` | `telemetry::init_tempo_from_env` / `init_tempo`,通过 OTLP/gRPC 导出 tracing span |

```toml
tiny-agent = { version = "0.3", features = ["file-storage", "otel-tempo"] }
```

```rust
use std::sync::Arc;
use tiny_agent::{AgentRuntimeStorage, storage::FileStorage};

let store = Arc::new(FileStorage::new("./.agent_storage").await?);
let storage = AgentRuntimeStorage::shared(store);
```

## 核心概念

### Agent 状态机

| 状态 | 含义 |
| --- | --- |
| `Ready` | 准备进入下一轮推理 |
| `Reasoning` | 正在调用模型 |
| `ToolExecuting` | 正在执行模型请求的工具 |
| `WaitingForUser` | 工具要求用户交互(如追问),已停车 |
| `Success` / `Fail` | 终态,运行结束清理 checkpoint |
| `Interrupted` | 被取消,已写快照,可 `resume` |

驱动入口:

| 方法 | 作用 |
| --- | --- |
| `create_session()` | 新建会话 |
| `submit(session_id, text)` | 追加一条用户消息,返回 `Ready` |
| `run(agent, token)` | 把状态机跑到停车点 |
| `run_turn(session_id, text, token)` | 可选会话 id + 用户输入,一站式追加并跑完一轮 |
| `resume(session_id, token)` | 从中断的 checkpoint 续跑 |
| `run_session(session_id, token)` | 一站式:有断点就恢复,否则从头跑 |

```rust
// 传 None 自动新建会话;传 Some(id) 复用已有会话(缺失则按该 id 初始化)。
let turn = runtime
    .run_turn(Some(existing_session_id), "继续分析这个问题", CancellationToken::new())
    .await?;
println!("session id: {}", turn.session_id);
```

### 可插拔的 trait

| Trait | 职责 | 自带实现 |
| --- | --- | --- |
| `LlmProvider` | 流式 chat completion | `AnthropicProvider`、`OpenAiCompatibleProvider` |
| `Sandbox` | 工具执行环境(按会话 open / execute / save / fork) | `HostSandbox` |
| `CheckpointStorage` | 可恢复状态快照的存取 | `FileStorage`(需 `file-storage`)或自行实现 |
| `TranscriptStorage` | 喂给模型的消息历史 | `FileStorage`(需 `file-storage`)或自行实现 |
| `Middleware` | 生命周期钩子(拦截 / 改写 / 短路) | 自行实现 |

### 工具

用参数 struct(实现 `Deserialize + JsonSchema`)注册,Schema 自动生成:

```rust
#[derive(serde::Deserialize, schemars::JsonSchema)]
struct EchoArgs { text: String }

tools.register(
    "echo",
    "原样返回文本",
    true, // read_only:只读工具之间会被并发调度;有副作用的工具必须传 false
    |_sandbox, args: EchoArgs| async move {
        Ok(serde_json::json!({ "echo": args.text }).into())
    },
);
```

内置工具:`read_file`、`write_file`(write / replace / insert / append)、`bash`、`check`、`kill`、`askFollowupQuestion`。
此外 `register_spawn_agent` 提供子 agent,`register_read_skill` 提供 skill 加载(配置 skill 目录时自动注册)。

`register_default_tools` 是便捷入口,会一次性注册文件读写、shell、后台任务查询与追问工具。它不会替你做权限控制;生产环境通常应从 `ToolRegistry::new()` 开始,只注册当前场景允许的工具,或用 `Middleware::before_tool` 做二次拦截。也可以先注册一套全集,再用 `registry.subset(&["read_file", "bash"])` 按能力边界裁剪出某个(子)agent 可见的子集(保留各工具的 callable / schema / 异步策略);`contains` / `names` 可用于检视已注册的工具。

长耗时工具可以用结构化注册声明为 `asynchronous`:工具在 `yield_after` 窗口内完成时直接返回完整结果;超过窗口则登记为后台工具任务,立即返回 `{id, status:"running", output}`,之后由 `check` 轮询、`kill` 终止。工具执行期间可通过 `ctx.progress` 发增量状态,这些状态会实时推给 UI,也会被 `check(id)` 按增量读回。
`ToolCtx` 对外只暴露只读的 `session_id()` 与 `progress`;工具不要直接操作 runtime 的消息通道或后台任务表。

```rust
use std::time::Duration;
use tiny_agent::tools::{AsyncToolConfig, ToolSpec};

tools.register_with_ctx_tool(
    ToolSpec::new("slow_report", "生成一份较慢的报告", false)
        .asynchronous(AsyncToolConfig::new(Duration::from_secs(2))),
    |_sandbox, args: ReportArgs, ctx| async move {
        ctx.progress.emit_text("started\n");
        let report = build_report(args).await?;
        ctx.progress.emit_text("finished\n");
        Ok(serde_json::json!({ "report": report }).into())
    },
);
```

`bash` 只是内置的一个 asynchronous 工具,默认让步窗口是 2 秒。后台工具任务表存在于当前 runtime 进程内:进程重启后 checkpoint 和 transcript 可以恢复,但已经在后台运行的宿主机进程不会被 tiny-agent 重新挂接,旧的任务 id 也会失效。

### Skills

Skill 让模型按需加载领域知识或操作手册,约定沿用 [Agent Skills](https://www.anthropic.com/news/skills):一个目录配一个 `SKILL.md`,YAML frontmatter 写 `name` 与 `description`,正文是给模型看的说明,同目录其余文件作为可选资源。

构建时用 `skill_dir` / `skill_dirs` 指定发现目录,发现规则:

- `dir/SKILL.md` 存在 → `dir` 本身是一个 skill;
- `root/*/SKILL.md` 存在 → 每个子目录各是一个 skill;
- skill id 取目录名,`name` / `description` 取自 frontmatter。

```rust
let runtime = AgentRunTimeBuilder::new()
    // ...必填项
    .skill_dir("./skills")
    .build()?;
```

配置了 skill 目录后,框架自动注册 `readSkill` 工具:模型先看到所有 skill 的 `name` + `description` 索引,需要时再调用 `readSkill` 拉取某个 skill 的正文或某个资源文件——按需加载,避免把全部说明一次性塞进上下文。

底层的 `SkillManager` 也可独立使用(`discover_dirs` / `list` / `read`),用于在运行时之外检视或预加载技能。

### 子 Agent(spawn_agent)

`spawn_agent` 是一个工具:让父 agent 把一段独立子任务委托给一个**单独配置**的子 agent,子 agent 跑完后把结果回传给父 agent。

子 agent 的形态由你传入的一份 `AgentRuntimeConfig` 决定——它可以用**不同的 model、system prompt、工具集、中间件**,与父 agent 互不影响。注册时把这份子配置交给 `register_spawn_agent`:

```rust
use tiny_agent::{
    AgentRunTimeBuilder, AgentRuntimeConfig, AgentRuntimeConfigInput, AgentRuntimeOptions,
    AgentRuntimeStorage,
    tools::{ToolRegistry, register_default_tools, register_spawn_agent},
};

// 1. 子 agent 的配置:可与父 agent 完全不同的一套
let mut child_tools = ToolRegistry::new();
register_default_tools(&mut child_tools);
let child_config = AgentRuntimeConfig::new(AgentRuntimeConfigInput {
    provider: provider.clone(),
    model: "deepseek-chat".to_string(), // 子 agent 可用更便宜/更快的模型
    storage: AgentRuntimeStorage::shared(store.clone()),
    sandbox: sandbox.clone(),
    options: AgentRuntimeOptions {
        tools: child_tools,
        system_prompt: Some("你是子任务执行者,直接完成被委托的任务并返回简洁结论。".to_string()),
        ..Default::default()
    },
});

// 2. 父 agent 的工具集:默认工具 + spawn_agent
let mut parent_tools = ToolRegistry::new();
register_default_tools(&mut parent_tools);
register_spawn_agent(&mut parent_tools, child_config);

// 3. 父 runtime 使用 parent_tools,模型即可调用 spawn_agent { task: "..." } 派生子 agent
let parent_runtime = AgentRunTimeBuilder::new()
    .provider(provider)
    .model("deepseek-chat")
    .checkpoint_storage(store.clone())
    .transcript_storage(store)
    .sandbox(sandbox)
    .tools(parent_tools)
    .system_prompt("你是主 agent,可以直接处理任务,也可以把独立子任务委托给 spawn_agent。")
    .build()?;
```

几个关键点:

- 子 agent 拥有**独立的会话与 transcript**(互不污染上下文),但**共享父 agent 当前的 sandbox**——子任务里 `write_file`、`bash` 的副作用落在同一个工作环境,父 agent 后续可见。注册时配置里的 `sandbox` 会在调用时被父 sandbox 覆盖,所以那一项填什么都行。
- 子会话会**登记为父会话的 subsession**(父 transcript 的 `subsessions` 里记下子会话 id),便于按一次 turn 下钻到各子 agent 会话;级联删除父会话时子会话树会被一并清掉。
- **取消会向下传播**:子 agent 用父 run 取消令牌派生的 child token 跑,父任务被取消时子 agent 也会干净地一并取消(走 Interrupted / 存档),而非被硬掐断。
- 工具入参只有一个 `task` 字段;返回 `child_session_id` 与子 agent 的最终结果(含失败 / 等待用户 / 中断等状态)。
- 子配置同样可挂自己的 skills、中间件、工具集,实现「专用子 agent」。

### 中间件

钩子按洋葱模型组合:`before_*` 正序、`after_*` 逆序。可改请求、改消息、给工具结果脱敏,或在工具执行前直接短路。

```rust
#[async_trait::async_trait]
impl Middleware for Guardrail {
    fn name(&self) -> &str { "guardrail" }

    async fn before_tool(&self, _ctx: &MiddlewareCtx<'_>, call: &ToolCall)
        -> Result<ToolDecision, String>
    {
        if is_dangerous(call) {
            return Ok(ToolDecision::ShortCircuit {
                output: vec![ContentBlock::Text { text: "blocked".into() }],
                is_error: true,
            });
        }
        Ok(ToolDecision::Proceed)
    }
}
```

### 可观测:tracing / OpenTelemetry

tiny-agent 不内置观测存储,也不替宿主程序初始化全局 exporter。运行时把一轮对话建模成一棵**嵌套 span 树**,按 OpenTelemetry GenAI 语义约定产出,可直接被 Tempo / Jaeger / Langfuse 等 OTLP 后端渲染:

```text
agent.run                      一轮运行(root)
├─ user_input                  用户提交本轮输入
├─ gen_ai.chat                 一次 LLM 调用
│   gen_ai.request.system / .model / .temperature / .max_tokens
│   gen_ai.input.messages      发给模型的完整输入(system + 历史 + 本轮,全量 JSON)
│   gen_ai.output.messages     模型输出(thinking / 回答 / 工具调用,全量)
│   gen_ai.response.finish_reasons
│   gen_ai.usage.input_tokens / .output_tokens / .total_tokens
├─ tool.call                   一次工具调用
│   gen_ai.tool.name / .call.id
│   gen_ai.tool.call.arguments 工具入参(全量 JSON)
│   gen_ai.tool.call.result    工具结果(全量 JSON)/ error
└─ gen_ai.chat …
```

`tracing` event 只作为轻量阶段标记挂在对应 span 上(`user_message`、`reasoning_started`、`assistant_message`、`tool_started`、`tool_completed`、`succeeded` 等),只携带 id、模型、工具名、数量、字节数、错误标记等元数据;完整 prompt / 消息 / 工具入参 / 工具结果只记录在 span attribute 上。框架自身的补充统计放在 `tiny_agent.*` 属性下。是否记录、采样、脱敏、落盘或导出,全部由你的应用侧 `tracing_subscriber` / OpenTelemetry exporter 决定——库不会和宿主已有的 tracing 配置争抢全局 subscriber。

打开 `otel-tempo` feature 后,可用便捷函数按环境变量接入本地 Collector / Tempo:

```rust
let _telemetry = tiny_agent::telemetry::init_tempo_from_env("my-agent")?;
```

设置 `TINY_AGENT_OTEL=1` 或 `OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317` 后会启用导出;未设置时 `init_tempo_from_env` 返回 `Ok(None)`。如果宿主已有 subscriber,可用 `build_provider` / `tempo_layer` 自行组合。

> **⚠️ 隐私(自 0.2 起的行为变化)**:span attribute 现在会记录**完整原文**——系统提示、对话历史、模型输出、工具入参与结果都会进入 tracing 属性。这便于审计与排查,但意味着 trace 里会含敏感数据。如不需要全量内容,请在应用侧 subscriber 过滤 `tiny_agent` target 或相应字段、调低采样、或在 Collector 侧脱敏;只要不接 exporter,则什么都不会离开进程。

## 配置

`AgentRunTimeBuilder` 上的可调参数:

| 方法 | 默认 | 说明 |
| --- | --- | --- |
| `max_iter` | 10 | 单次运行的最大推理轮数,超限以 `MaxIter` 失败 |
| `max_consecutive_fail_count` | 3 | 最大连续失败次数 |
| `retry_backoff` | 200ms | 可重试失败的退避基数(指数 + jitter),`ZERO` 关闭 |
| `tool_timeout` | 120s | 单个工具调用超时,`ZERO` 关闭 |
| `temperature` / `max_tokens` | 无 | 透传给 provider |
| `system_prompt` | 无 | 系统提示 |
| `skill_dir` / `skill_dirs` | 无 | skill 发现目录 |
| `middleware` | 空链 | 可多次调用追加 |

## 兼容性

需要 Rust 1.85 及以上(edition 2024)。运行时基于 [Tokio](https://tokio.rs)。

## 许可证

采用 [MIT](LICENSE-MIT) 或 [Apache-2.0](LICENSE-APACHE) 双许可,任选其一。

除非你明确声明,你有意提交并被本仓库收录的贡献,均按上述双许可授权,不附加额外条款。