Skip to content

Commit 13259a0

Browse files
Feat: tRPC-Agent支持Goal能力(对齐Go版本)
1 parent 73655ab commit 13259a0

23 files changed

Lines changed: 2730 additions & 0 deletions

File tree

docs/mkdocs/en/tool.md

Lines changed: 358 additions & 0 deletions
Large diffs are not rendered by default.

docs/mkdocs/zh/tool.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Agent 通过以下步骤动态使用工具:
3535
| [WebSearchTool](#websearchtool) | 公网搜索引擎检索 | 实例化 WebSearchTool 并加入 tools | 实时资讯、版本发布、事实/定义查询 |
3636
| [TodoWriteTool](#todowritetool-任务清单工具) | 多步任务规划与进度跟踪(整表替换) | 挂载 `TodoWriteTool` | 短清单、无依赖编排、token 不敏感 |
3737
| [Task 工具族](#task-工具族结构化任务看板) | 结构化任务看板(按 id 增量更新 + 依赖) | 挂载 `TaskToolSet` | 长任务板、跨轮跟踪、blockedBy 依赖 |
38+
| [Goal 工具族](#goal-工具族持久会话目标) | 单会话持久目标 + 强制收尾 | `setup_goal(agent)` | 跨轮大目标、宿主设目标、未完成不许收尾 |
3839
| [Agent Code Executor](./code_executor.md) | 自动生成并执行代码场景、数据处理场景 | 配置 CodeExecutor | API 自动调用、表格数据处理 |
3940
---
4041

@@ -3040,3 +3041,163 @@ print(render_task_list(store))
30403041
| --- | --- |
30413042
| [examples/task_tools](../../../examples/task_tools/) | 多轮对话:依赖编排、逐项完成、跨轮 `get_task_store` 读回看板 |
30423043
| [examples/task_tools_parallel](../../../examples/task_tools_parallel/) | 验证 `parallel_tool_calls``task_store_lock`(Phase 1–2 无需 API Key) |
3044+
3045+
---
3046+
3047+
## Goal 工具族(持久会话目标)
3048+
3049+
`GoalToolSet` 暴露三个工具——`create_goal``get_goal``update_goal`——对齐 Claude Code 的 **Session Goal** 能力。与 `TodoWriteTool`(多行待办)和 `TaskToolSet`(多任务看板)不同,Goal 在每个 session branch 上**至多只有一个**持久目标:在目标为 `active` 期间,模型给出「看起来像最终答案」的文本**不算完成**——必须继续执行,或显式调用 `update_goal('complete' | 'blocked')` 收尾。
3050+
3051+
目标序列化为**单个 JSON blob**`GoalRecord`)写入 `tool_context.state["goal[:<branch>]"]`,跨 `Runner.run_async` 调用存活。除三个模型工具外,完整能力还需通过 `setup_goal()` 挂载一对 **enforcement callbacks**`before_model` / `after_model`),在**同一次 invocation 内**拦截过早的最终回复并自动重试。
3052+
3053+
### 功能特性
3054+
3055+
- **单目标契约**:每个 branch 一个 `GoalRecord``objective` + 三态 `active` / `complete` / `blocked`);`complete` / `blocked`**不可逆**终态
3056+
- **跨轮持久化**:随 function-response 的 state delta 落库;**勿用** `temp:` 前缀
3057+
- **子 Agent 隔离**:state key 追加 `:<branch>`,父 / 子 Agent 各自维护独立目标
3058+
- **强制收尾(enforcement)**:目标 `active` 时,`after_model` 检测「无 tool call、有可见文本、非 partial」的过早 final,抑制该回复并在同 invocation 内 re-run;`before_model` 注入 user-role nudge
3059+
- **fail-open 预算**`max_retries`(默认 3)次拦截后放行最终回复,避免无限循环;计数器存在 invocation 级 `agent_context.metadata`,不持久化
3060+
- **双入口创建**
3061+
- **模型侧**`create_goal(objective=...)` —— LLM 判断多步任务后自主创建
3062+
- **宿主侧**`start_goal(session_service, ...)` —— 应用层在首轮前写入 session,模型无需调用 `create_goal`
3063+
- **Prompt 引导分层**`DEFAULT_GUIDANCE` 在目标 active 时经 `before_model` 注入 system instruction(`inject_guidance=True`);硬约束由 store 校验 + callback 共同保证
3064+
- **并发安全**`_GoalToolBase` 在 load → mutate → save 外包 `goal_store_lock`(按 session + branch),兼容 `parallel_tool_calls=True`
3065+
3066+
### 与 Todo / Task 的关系
3067+
3068+
| 维度 | `TodoWriteTool` | `TaskToolSet` | Goal 工具族 |
3069+
| --- | --- | --- | --- |
3070+
| 粒度 | 多行待办清单 | 多任务看板 + 依赖 | **单个**会话目标 |
3071+
| 更新方式 | 整表替换 |`taskId` 增量 | `create_goal` / `update_goal` |
3072+
| 未完成能否收尾 | Prompt 引导 | Prompt 引导 | **callback 强制拦截** |
3073+
| state key | `todos[:branch]` | `tasks[:branch]` | `goal[:branch]` |
3074+
| 典型用途 | 步骤可视、短清单 | 长看板、依赖编排 | 整件事是否算做完 |
3075+
3076+
> Todo / Task 管「步骤分解」,Goal 管「整体完成契约」。可组合使用,但避免让模型同时混用过多规划工具。
3077+
3078+
### GoalOptions 构造参数
3079+
3080+
通过 `setup_goal(agent, GoalOptions(...))` 配置:
3081+
3082+
| 参数 | 类型 | 默认值 | 说明 |
3083+
|------|------|--------|------|
3084+
| `state_key_prefix` | `str` | `"goal"` | state key 前缀;勿使用 `temp:` |
3085+
| `inject_guidance` | `bool` | `True` | 是否在 `before_model` 向 system instruction 注入 `DEFAULT_GUIDANCE` |
3086+
| `guidance` | `str` | `DEFAULT_GUIDANCE` | 注入的长文案(含串行调用 goal 工具等约定) |
3087+
| `max_retries` | `int` | `3` | 同 invocation 内拦截过早 final 的预算;耗尽后 fail-open |
3088+
| `nudge_template` | `str` | `DEFAULT_NUDGE` | 拦截后以 user-role 追加的提醒模板(支持 `{attempt}` / `{max_retries}` / `{objective}`|
3089+
| `on_retry` | `Callable[[RetryEvent], None]` | `None` | 每次拦截或预算耗尽时的可观测回调 |
3090+
3091+
仅挂载模型工具、不要 enforcement 时,可直接 `tools=[GoalToolSet()]`,但不具备「未完成不许收尾」能力。
3092+
3093+
### 三个工具的 LLM 参数概要
3094+
3095+
**`create_goal`**
3096+
3097+
| 参数 | 必填 | 说明 |
3098+
|------|------|------|
3099+
| `objective` || 完成标准——「done」具体指什么 |
3100+
3101+
成功返回 `{message, goal}`;若已有 `active` 目标返回 `{error: "INVALID_STATE: ..."}`
3102+
3103+
**`get_goal`**
3104+
3105+
无参数。有目标时返回 `{message, goal}`;无目标时返回 `{message: "No session goal is set."}`
3106+
3107+
**`update_goal`**
3108+
3109+
| 参数 | 必填 | 说明 |
3110+
|------|------|------|
3111+
| `status` || `complete`(目标已达成)或 `blocked`(同一阻塞条件反复出现、无用户输入无法继续) |
3112+
3113+
成功返回 `{message, goal}`;无 active 目标或已是终态时返回 `{error: "INVALID_STATE: ..."}`
3114+
3115+
**`GoalRecord` 字段**(JSON 使用 camelCase 别名持久化):
3116+
3117+
| 字段 | 说明 |
3118+
|------|------|
3119+
| `id` | 服务端分配的 uuid |
3120+
| `objective` | 完成标准文本 |
3121+
| `status` | `active` / `complete` / `blocked` |
3122+
| `createdAtUnix` / `updatedAtUnix` | 创建 / 最后更新时间(unix 秒) |
3123+
| `terminalAtUnix` | 进入终态的时间(可选) |
3124+
3125+
### enforcement 工作流程
3126+
3127+
```text
3128+
模型输出 final 文本(无 tool call,goal 仍 active)
3129+
3130+
after_model 判定为 premature final
3131+
3132+
抑制该 final(不提交为答案),retry_count += 1
3133+
before_model 注入 nudge,同 invocation 继续 agent loop
3134+
3135+
retry_count >= max_retries → fail-open,on_retry(reason="exhausted")
3136+
```
3137+
3138+
拦截条件(`_is_premature_final`):非 partial、无 error、content 含可见文本,且**不含** `function_call` / `function_response`
3139+
3140+
### 使用方式
3141+
3142+
推荐一行挂载工具 + callbacks:
3143+
3144+
```python
3145+
from trpc_agent_sdk.agents import LlmAgent
3146+
from trpc_agent_sdk.models import OpenAIModel
3147+
from trpc_agent_sdk.tools.goal_tools import GoalOptions, RetryEvent, setup_goal
3148+
3149+
def on_retry(event: RetryEvent) -> None:
3150+
if event.reason == "blocked":
3151+
print(f"拦截过早收尾 (attempt {event.attempt_number}/{event.max_retries})")
3152+
3153+
agent = LlmAgent(
3154+
name="goal_agent",
3155+
model=OpenAIModel(model_name="...", api_key="...", base_url="..."),
3156+
instruction="多步工程任务请用 goal 工具跟踪完成状态。",
3157+
tools=[...], # 业务工具,如 BashTool / WriteTool
3158+
)
3159+
setup_goal(agent, GoalOptions(max_retries=3, on_retry=on_retry))
3160+
```
3161+
3162+
宿主在首轮前预注入目标(模型不调用 `create_goal`):
3163+
3164+
```python
3165+
from trpc_agent_sdk.tools.goal_tools import start_goal
3166+
3167+
goal = await start_goal(
3168+
session_service,
3169+
app_name="my_app",
3170+
user_id="user_1",
3171+
session_id=session_id,
3172+
objective="在当前目录创建 notes/ 并写入 summary.txt 与 example.py",
3173+
agent_name=agent.name, # 与 LlmAgent.name 一致,用于 branch 隔离
3174+
)
3175+
```
3176+
3177+
读回持久化目标(REST / 审计 / demo 收尾):
3178+
3179+
```python
3180+
from trpc_agent_sdk.tools.goal_tools import get_goal_record, render_goal
3181+
3182+
goal = get_goal_record(session, branch=agent.name)
3183+
print(render_goal(goal))
3184+
# ✅ Goal [complete]
3185+
# objective: ...
3186+
# created: 1782893110
3187+
# terminal: 1782893116
3188+
```
3189+
3190+
### Goal 工具族最佳实践
3191+
3192+
- **`setup_goal` 而非只挂 `GoalToolSet`**:只有 callbacks 才能实现「active 期间不许 final」
3193+
- **模型侧 vs 宿主侧**:Slash command、`/goal`、配置驱动任务用 `start_goal()`;让模型自主判断多步任务时用 `create_goal`
3194+
- **一次响应只调一个 goal 工具**`DEFAULT_GUIDANCE` 要求串行语义;不要同轮 `create_goal` + `update_goal`
3195+
- **`blocked` 慎用**:仅当同一阻塞条件跨多次尝试仍无法推进、且需要用户输入或外部状态变化时使用;不要因为任务难、慢或不完整就标记 blocked
3196+
- **可观测性**:通过 `on_retry` 记录 `⚡ Premature final intercepted` 与预算耗尽,便于调优 prompt 或 `max_retries`
3197+
- **与 Todo / Task 分工**:Todo / Task 展示步骤与依赖;Goal 约束「整件事是否已真正完成」
3198+
3199+
### Goal 工具族完整示例
3200+
3201+
| 示例 | 说明 |
3202+
| --- | --- |
3203+
| [examples/goal_tools](../../../examples/goal_tools/) | Case 1:模型 `create_goal`;Case 2:宿主 `start_goal` 预注入;演示 enforcement 拦截与 `update_goal(complete)` |

examples/goal_tools/.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME
2+
TRPC_AGENT_API_KEY=your-api-key
3+
TRPC_AGENT_BASE_URL=your-base-url
4+
TRPC_AGENT_MODEL_NAME=your-model-name

examples/goal_tools/README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Goal 工具示例
2+
3+
演示 **Goal 工具族**`create_goal` / `get_goal` / `update_goal`):为会话设置一个持久目标,目标未完成前 Agent 应继续执行,而不是过早给出最终回复。示例同时挂载 `Bash` / `Write` / `Read` 完成真实的多步文件任务。
4+
5+
## 快速开始
6+
7+
```bash
8+
# 在项目根目录安装
9+
cd trpc-agent-python
10+
python3 -m venv .venv && source .venv/bin/activate
11+
pip3 install -e .
12+
13+
# 配置模型(examples/goal_tools/.env)
14+
TRPC_AGENT_API_KEY=your-api-key
15+
TRPC_AGENT_BASE_URL=your-base-url
16+
TRPC_AGENT_MODEL_NAME=your-model-name
17+
18+
# 运行
19+
cd examples/goal_tools
20+
python3 run_agent.py
21+
```
22+
23+
## 两个演示场景
24+
25+
脚本会**依次**跑两个独立 session:
26+
27+
| | Case 1 | Case 2 |
28+
| --- | --- | --- |
29+
| 谁设目标 | 模型调用 `create_goal` | 宿主调用 `start_goal()` 预注入 |
30+
| 用户消息 | 普通多步任务(可提及 goal 能力) | 只描述要做的事,不提 goal 工具 |
31+
| 典型流程 | `create_goal` → 写文件 → 验证 → `update_goal(complete)` | 写文件 → (可能被拦截)→ 补全 → `update_goal(complete)` |
32+
33+
只跑其中一个时,在 `run_agent.py``main()` 里注释掉对应 case。
34+
35+
## 日志说明
36+
37+
| 输出 | 含义 |
38+
| --- | --- |
39+
| `🔧 [Tool call]` | 模型发起的工具调用 |
40+
| `📊 [Tool result]` | 工具返回摘要 |
41+
| `💬 ...` | 工具返回里的 `message`(给模型看的提示) |
42+
| `⚡ [Goal retry]` | 目标仍 active 时模型想提前收尾,被拦截并继续执行 |
43+
| `🤖 Assistant:` | 本轮最终回复(正常应在 `update_goal(complete)` 之后) |
44+
| `🎯 Persisted goal` | 从 session 读回的目标状态 |
45+
46+
终端较窄时,长参数的工具调用行可能折行,看起来像重复打印,以 `📊` 条数为准。
47+
48+
## 运行结果(实测摘录)
49+
50+
### Case 1:模型自己设 Goal
51+
52+
```text
53+
🔧 [Tool call] create_goal({...})
54+
📊 [Tool result] created id=... status=active objective='...'
55+
💬 Goal created and is now active. Keep working until it is genuinely met, then call update_goal('complete').
56+
57+
🔧 [Tool call] Write({'path': 'mypkg/__init__.py', ...})
58+
🔧 [Tool call] Write({'path': 'mypkg/utils.py', ...})
59+
🔧 [Tool call] Write({'path': 'mypkg/README.md', ...})
60+
...
61+
🔧 [Tool call] Bash({'command': 'ls -la mypkg/ && ... python -c "import mypkg; ..."'})
62+
🔧 [Tool call] update_goal({'status': 'complete'})
63+
64+
🤖 Assistant: 工具包 `mypkg/` 已搭建完成并验证通过 ✅
65+
66+
🎯 Persisted goal (read from session):
67+
✅ Goal [complete]
68+
```
69+
70+
本例未触发 `⚡ [Goal retry]`,说明模型在标记完成前没有过早收尾。
71+
72+
### Case 2:宿主预注入 Goal
73+
74+
```text
75+
🎯 Goal pre-injected by host:
76+
objective: '在当前目录创建 notes/ 目录,其中包含两个文件:...'
77+
status: active
78+
79+
🔧 [Tool call] Write({'path': 'notes/summary.txt', ...})
80+
⚡ [Goal retry] Premature final intercepted (attempt 1/3). Objective: '...'
81+
82+
🔧 [Tool call] Write({'path': 'notes/example.py', ...})
83+
⚡ [Goal retry] Premature final intercepted (attempt 2/3). Objective: '...'
84+
85+
🔧 [Tool call] get_goal({})
86+
🔧 [Tool call] Bash({'command': 'cd notes && python example.py', ...})
87+
🔧 [Tool call] update_goal({'status': 'complete'})
88+
89+
🤖 Assistant: 已完成。创建了 `notes/` 目录,其中包含两个文件:...
90+
91+
🎯 Persisted goal (read from session):
92+
✅ Goal [complete]
93+
```
94+
95+
Case 2 里写完第一个文件后模型就想总结,**Goal enforcement 拦截了 2 次**,随后补写 `example.py`、运行验证,再 `update_goal(complete)`——这正是 Goal 能力的预期表现。
96+
97+
## 关键文件
98+
99+
- [`run_agent.py`](./run_agent.py) — 入口,驱动两个 case 并打印事件
100+
- [`agent/agent.py`](./agent/agent.py) — 组装 Agent,调用 `setup_goal()` 挂载 Goal 能力
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.

examples/goal_tools/agent/agent.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Agent module for the Goal tools example."""
7+
8+
import os
9+
10+
from trpc_agent_sdk.agents import LlmAgent
11+
from trpc_agent_sdk.models import LLMModel
12+
from trpc_agent_sdk.models import OpenAIModel
13+
from trpc_agent_sdk.tools.file_tools import BashTool
14+
from trpc_agent_sdk.tools.file_tools import ReadTool
15+
from trpc_agent_sdk.tools.file_tools import WriteTool
16+
from trpc_agent_sdk.tools.goal_tools import GoalOptions
17+
from trpc_agent_sdk.tools.goal_tools import RetryEvent
18+
from trpc_agent_sdk.tools.goal_tools import setup_goal
19+
20+
from .config import get_model_config
21+
from .prompts import INSTRUCTION
22+
23+
24+
def _create_model() -> LLMModel:
25+
api_key, url, model_name = get_model_config()
26+
return OpenAIModel(model_name=model_name, api_key=api_key, base_url=url)
27+
28+
29+
def on_retry(event: RetryEvent) -> None:
30+
"""Observability callback: called every time the retry intercepts a premature final."""
31+
if event.reason == "blocked":
32+
print(
33+
f" ⚡ [Goal retry] Premature final intercepted "
34+
f"(attempt {event.attempt_number}/{event.max_retries}). "
35+
f"Objective: {event.goal.objective!r}"
36+
)
37+
else:
38+
print(
39+
f" ⚠️ [Goal retry] Budget exhausted ({event.max_retries} retries). "
40+
f"Letting final response through."
41+
)
42+
43+
44+
def create_goal_agent(work_dir: str | None = None) -> LlmAgent:
45+
"""Build an agent with the Goal capability mounted via ``setup_goal``.
46+
47+
The agent exposes ``create_goal`` / ``get_goal`` / ``update_goal`` tools
48+
plus file-system tools for actually executing multi-step work. The
49+
retry callbacks (guidance injection + premature-final interception)
50+
are installed automatically by :func:`setup_goal`.
51+
52+
Args:
53+
work_dir: Working directory for Bash / Write / Read tools.
54+
"""
55+
cwd = work_dir or os.getcwd()
56+
agent = LlmAgent(
57+
name="goal_agent",
58+
description="Engineering assistant that pursues a persistent session goal step by step.",
59+
model=_create_model(),
60+
instruction=INSTRUCTION,
61+
tools=[
62+
BashTool(cwd=cwd),
63+
WriteTool(cwd=cwd),
64+
ReadTool(cwd=cwd),
65+
],
66+
)
67+
opts = GoalOptions(
68+
max_retries=3,
69+
on_retry=on_retry,
70+
)
71+
setup_goal(agent, opts)
72+
return agent
73+
74+
75+
goal_agent = create_goal_agent()
76+
root_agent = goal_agent
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Agent config module."""
7+
8+
import os
9+
10+
11+
def get_model_config() -> tuple[str, str, str]:
12+
"""Get LLM model config from environment variables."""
13+
api_key = os.getenv("TRPC_AGENT_API_KEY", "")
14+
url = os.getenv("TRPC_AGENT_BASE_URL", "")
15+
model_name = os.getenv("TRPC_AGENT_MODEL_NAME", "")
16+
if not api_key or not url or not model_name:
17+
raise ValueError(
18+
"TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL, and TRPC_AGENT_MODEL_NAME "
19+
"must be set in environment variables"
20+
)
21+
return api_key, url, model_name
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Prompts for the Goal tools demo agent."""
7+
8+
INSTRUCTION = """You are a rigorous engineering assistant that can work toward session goals. """
9+

0 commit comments

Comments
 (0)