Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6665afc
{tool/claudecode, docs/mkdocs}: add Claude Code toolset
Flash-LHR Apr 20, 2026
91ca004
tool/claudecode: tidy module dependencies
Flash-LHR Apr 20, 2026
a120560
tool/claudecode: raise test coverage
Flash-LHR Apr 20, 2026
be2b295
tool/claudecode: stabilize and extend tests
Flash-LHR Apr 20, 2026
66744f7
tool/claudecode: cover remaining helper branches
Flash-LHR Apr 20, 2026
eeecd8d
tool/claudecode: make ripgrep path test environment independent
Flash-LHR Apr 20, 2026
332aba9
tool/claudecode: cover remaining patch branches
Flash-LHR Apr 20, 2026
d982a17
tool/claudecode: cover remaining helper paths
Flash-LHR Apr 20, 2026
b15cf0f
tool/claudecode: cover common and read helpers
Flash-LHR Apr 20, 2026
2be7fa3
tool/claudecode: cover search helpers
Flash-LHR Apr 20, 2026
ae04f5b
tool/claudecode: cover helper branches for patch checks
Flash-LHR Apr 20, 2026
d4a9d66
{tool/claudecode, docs/mkdocs}: adopt focused review fixes
Flash-LHR Apr 20, 2026
ed32bcb
tool/claudecode: tighten web search and notebook edge cases
Flash-LHR Apr 21, 2026
385abdc
tool/claudecode: align google search offset semantics
Flash-LHR Apr 21, 2026
fd3cd53
docs
Flash-LHR Apr 23, 2026
6f1556c
remove tool search
Flash-LHR Apr 23, 2026
2b4e018
del ask user
Flash-LHR Apr 23, 2026
68413e4
tool/claudecode: add coverage for web fetch and bash branches
Flash-LHR Apr 23, 2026
1a3bcb4
tool/claudecode: add coverage for additional web fetch branches
Flash-LHR Apr 23, 2026
d4cbcb6
tool/claudecode: stabilize web fetch coverage tests
Flash-LHR Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/mkdocs/en/tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,56 @@ searchTool := duckduckgo.NewTool(
)
```

### Claude Code ToolSet

`tool/claudecode` provides a code-oriented ToolSet that exposes a Claude Code-style tool surface inside the framework. It is designed for common coding workflows such as file editing, repository search, command execution, and web retrieval, and can be attached directly to `LLMAgent` or other runtimes. If your goal is to invoke the local Claude Code CLI and consume its execution trace and tool events, see the [Claude Code Agent guide](claudecode.md).

By default, `claudecode` exposes a core set of workflow tools: `Bash`, `TaskStop`, `TaskOutput`, `Read`, `Glob`, `Grep`, `WebFetch`, `WebSearch`, and `ToolSearch`. When read-only mode is disabled, it also exposes `Write`, `Edit`, and `NotebookEdit`. In practice, `Bash` is suited for builds, tests, Git operations, and project scripts, while `Read`, `Glob`, and `Grep` cover file inspection and repository search. `Edit` and `Write` are intended for targeted edits and full rewrites respectively.

#### Basic Usage

```go
import (
"log"

"trpc.group/trpc-go/trpc-agent-go/agent/llmagent"
"trpc.group/trpc-go/trpc-agent-go/tool"
"trpc.group/trpc-go/trpc-agent-go/tool/claudecode"
)

toolSet, err := claudecode.NewToolSet(
claudecode.WithBaseDir("."),
)
if err != nil {
log.Fatal(err)
}
defer toolSet.Close()

agent := llmagent.New(
"claude-style-agent",
llmagent.WithToolSets([]tool.ToolSet{toolSet}),
)
```

In most cases, `llmagent.WithToolSets(...)` is the preferred integration point over flattening `Tools()` into `WithTools(...)`. That keeps lifecycle management at the ToolSet level and matches the framework's organization model for grouped tools.

#### Common Options

The main `tool/claudecode` options focus on working directory, read-only mode, and web behavior:

| Option | Description |
| --- | --- |
| `WithName(name)` | Overrides the ToolSet name. The default name is `claudecode`. |
| `WithBaseDir(dir)` | Sets the base directory used by file, search, and command execution tools. |
| `WithReadOnly(readOnly)` | Removes `Write`, `Edit`, and `NotebookEdit` when enabled. |
| `WithMaxFileSize(size)` | Limits the maximum readable file size. |
| `WithWebFetchOptions(opts)` | Configures domain policy, timeout, and content handling for `WebFetch`. |
| `WithWebSearchOptions(opts)` | Configures backend, paging, and request options for `WebSearch`. |

`WithBaseDir` is usually the first option to make explicit. It defines the working scope for `Read`, `Write`, `Edit`, `Glob`, and `Grep`, and it also determines the default working directory for `Bash`. If the agent is meant to operate inside a single repository, setting `baseDir` explicitly is recommended.

When integrating `tool/claudecode`, it is usually best to decide up front whether the agent should be read-only. For inspection-heavy workflows, read-only mode keeps the tool surface narrower and easier to control. When the agent is expected to modify files directly, keep `Write`, `Edit`, and `NotebookEdit` enabled. In prompts, it is also useful to make the intended tool boundaries explicit, such as using `Read` for context gathering, `Glob` and `Grep` for repository search, and `Bash` plus `TaskOutput` / `TaskStop` for long-running commands.

## MCP Tools

MCP (Model Context Protocol) is an open protocol that standardizes how applications provide context to LLMs. MCP tools are based on JSON-RPC 2.0 and provide standardized integration with external services for Agents.
Expand Down
48 changes: 48 additions & 0 deletions docs/mkdocs/zh/tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,54 @@ searchTool := duckduckgo.NewTool(
)
```

### Claude Code ToolSet

`tool/claudecode` 提供了一组面向代码工作的 ToolSet,用于在框架内部暴露与 Claude Code 接近的工具接口。它适合用于文件读写、代码检索、命令执行和网页获取这类高频场景,便于直接挂接到 `LLMAgent` 或其他运行时。如果你的目标是调用本地 Claude Code CLI,并消费 CLI 的执行轨迹与工具事件,请参考 [Claude Code Agent 使用指南](claudecode.md)。

从能力组成上看,`claudecode` 默认会提供一组代码工作流工具,包括 `Bash`、`TaskStop`、`TaskOutput`、`Read`、`Glob`、`Grep`、`WebFetch`、`WebSearch` 和 `ToolSearch`。在非只读模式下,还会额外提供 `Write`、`Edit` 和 `NotebookEdit`。其中,`Bash` 适合构建、测试、Git 操作和项目脚本,`Read`、`Glob`、`Grep` 更适合仓库内的文件读取与内容检索,`Edit` 与 `Write` 则分别面向局部修改和整体重写。

#### 基本用法

```go
import (
"trpc.group/trpc-go/trpc-agent-go/agent/llmagent"
"trpc.group/trpc-go/trpc-agent-go/tool"
"trpc.group/trpc-go/trpc-agent-go/tool/claudecode"
)

toolSet, err := claudecode.NewToolSet(
claudecode.WithBaseDir("."),
)
if err != nil {
log.Fatal(err)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
defer toolSet.Close()

agent := llmagent.New(
"claude-style-agent",
llmagent.WithToolSets([]tool.ToolSet{toolSet}),
)
```

通常建议通过 `llmagent.WithToolSets(...)` 直接挂载 ToolSet,而不是把 `Tools()` 展开后逐个传给 `WithTools(...)`。这样可以保留 ToolSet 的统一生命周期,也更符合框架对工具集的组织方式。

#### 常用配置

`tool/claudecode` 的配置重点围绕工作目录、只读模式和 Web 能力展开:

| Option | 说明 |
| --- | --- |
| `WithName(name)` | 覆盖 ToolSet 名称,默认值为 `claudecode`。 |
| `WithBaseDir(dir)` | 指定工具集的基础目录。文件、检索和命令执行都会以此为基准。 |
| `WithReadOnly(readOnly)` | 启用只读模式后,不再暴露 `Write`、`Edit`、`NotebookEdit`。 |
| `WithMaxFileSize(size)` | 限制单个文件可读取的最大尺寸。 |
| `WithWebFetchOptions(opts)` | 配置 `WebFetch` 的域名策略、超时与内容处理方式。 |
| `WithWebSearchOptions(opts)` | 配置 `WebSearch` 的后端、分页参数与请求选项。 |

其中,`WithBaseDir` 最值得优先明确。它决定了 `Read`、`Write`、`Edit`、`Glob`、`Grep` 等文件相关工具的工作范围,也决定了 `Bash` 执行命令时的默认目录。如果 Agent 只面向单个仓库工作,建议始终显式设置 `baseDir`。

在接入 `tool/claudecode` 时,建议先根据 Agent 的任务边界决定是否启用只读模式:如果主要用于代码阅读与分析,可以开启只读模式,只暴露 `Read`、`Glob`、`Grep`、`Bash`、`WebFetch`、`WebSearch` 等非修改类工具;如果需要在仓库内直接修改文件,再保留 `Write`、`Edit` 与 `NotebookEdit`。在提示词设计上,通常也应明确这组工具的分工,例如优先通过 `Read` 获取上下文,优先使用 `Glob` 和 `Grep` 做路径与内容检索;当需要执行耗时命令时,再通过 `Bash` 发起后台任务,并结合 `TaskOutput` 与 `TaskStop` 管理执行过程。

## MCP Tools 协议工具

MCP(Model Context Protocol)是一个开放协议,标准化了应用程序向 LLM 提供上下文的方式。MCP 工具基于 JSON-RPC 2.0 协议,为 Agent 提供了与外部服务的标准化集成能力。
Expand Down
84 changes: 84 additions & 0 deletions tool/claudecode/ask_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// Tencent is pleased to support the open source community by making
// trpc-agent-go available.
//
// Copyright (C) 2025 Tencent. All rights reserved.
//
// trpc-agent-go is licensed under the Apache License Version 2.0.
//

package claudecode

import (
"context"
"fmt"
"strings"

"trpc.group/trpc-go/trpc-agent-go/tool"
"trpc.group/trpc-go/trpc-agent-go/tool/function"
)

func newAskUserQuestionTool() (tool.Tool, error) {
return function.NewFunctionTool(
func(_ context.Context, in askUserQuestionInput) (askUserQuestionOutput, error) {
if len(in.Questions) == 0 {
return askUserQuestionOutput{}, fmt.Errorf("at least one question is required")
}
if len(in.Questions) > 4 {
return askUserQuestionOutput{}, fmt.Errorf("at most 4 questions are allowed")
}
if err := validateAskUserQuestions(in.Questions); err != nil {
return askUserQuestionOutput{}, err
}
answers := in.Answers
if answers == nil {
answers = map[string]string{}
}
return askUserQuestionOutput{
Questions: in.Questions,
Answers: answers,
Annotations: in.Annotations,
}, nil
},
function.WithName(toolAskUser),
function.WithDescription(askUserDescription()),
), nil
}

func validateAskUserQuestions(questions []askUserQuestion) error {
seenQuestions := map[string]struct{}{}
for _, question := range questions {
text := strings.TrimSpace(question.Question)
if text == "" {
return fmt.Errorf("question text is required")
}
if _, ok := seenQuestions[text]; ok {
return fmt.Errorf("question texts must be unique")
}
seenQuestions[text] = struct{}{}
if len(question.Options) < 2 || len(question.Options) > 4 {
return fmt.Errorf("question %q must have 2-4 options", text)
}
seenOptions := map[string]struct{}{}
for _, option := range question.Options {
label := strings.TrimSpace(option.Label)
if label == "" {
return fmt.Errorf("question %q has an option with empty label", text)
}
if _, ok := seenOptions[label]; ok {
return fmt.Errorf("option labels must be unique within question %q", text)
}
seenOptions[label] = struct{}{}
}
}
return nil
}

func askUserDescription() string {
return `Ask the user a bounded multiple-choice clarification question set.

Usage:
- Use this tool only when the next step is blocked on user preference or clarification.
- Each question must be unique and include 2-4 unique options.
- This tool is for collecting structured user choices rather than free-form conversation.`
}
167 changes: 167 additions & 0 deletions tool/claudecode/bash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//
// Tencent is pleased to support the open source community by making
// trpc-agent-go available.
//
// Copyright (C) 2025 Tencent. All rights reserved.
//
// trpc-agent-go is licensed under the Apache License Version 2.0.
//

package claudecode

import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"time"

"github.com/google/uuid"
"trpc.group/trpc-go/trpc-agent-go/tool"
"trpc.group/trpc-go/trpc-agent-go/tool/function"
)

func newBashTool(runtime *runtime) (tool.Tool, error) {
return function.NewFunctionTool(
func(ctx context.Context, in bashInput) (bashOutput, error) {
if in.RunInBackground {
return runBackgroundCommand(runtime, in.Command)
}
return runForegroundCommand(ctx, runtime, in)
},
function.WithName(toolBash),
function.WithDescription(bashDescription()),
), nil
}

func runForegroundCommand(ctx context.Context, runtime *runtime, in bashInput) (bashOutput, error) {
timeoutMs := bashTimeout(in.Timeout)
start := time.Now()
runCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutMs)*time.Millisecond)
defer cancel()
result, err := runCapturedProcess(runCtx, runtime.currentBaseDir(), nil, "bash", "-lc", in.Command)
durationMs := time.Since(start).Milliseconds()
timedOut := errorsIsDeadlineExceeded(runCtx.Err())
exitCode := result.ExitCode
if err != nil && exitCode == 0 {
if timedOut {
exitCode = 124
} else {
exitCode = 1
}
}
stdout := string(result.Stdout)
stderr := string(result.Stderr)
return bashOutput{
Command: in.Command,
ExitCode: exitCode,
Stdout: stdout,
Stderr: stderr,
Output: joinOutput(stdout, stderr),
DurationMs: durationMs,
TimedOut: timedOut,
}, nil
}

func bashTimeout(timeout *int) int {
timeoutMs := defaultBashTimeoutMs
if raw := os.Getenv("BASH_DEFAULT_TIMEOUT_MS"); raw != "" {
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
timeoutMs = parsed
}
}
if timeout != nil {
timeoutMs = *timeout
}
if timeoutMs <= 0 {
timeoutMs = defaultBashTimeoutMs
}
if timeoutMs > maxBashTimeoutMs {
timeoutMs = maxBashTimeoutMs
}
return timeoutMs
}

func runBackgroundCommand(runtime *runtime, command string) (bashOutput, error) {
taskID := uuid.NewString()
outputDir := filepath.Join(os.TempDir(), "trpc-agent-go-claudecode")
if err := os.MkdirAll(outputDir, 0o755); err != nil {
return bashOutput{}, err
}
outputPath := filepath.Join(outputDir, taskID+".log")
outputFile, err := os.Create(outputPath)
if err != nil {
return bashOutput{}, err
}
process, err := startProcess(runtime.currentBaseDir(), nil, outputFile, outputFile, "bash", "-lc", command)
if err != nil {
_ = outputFile.Close()
return bashOutput{}, err
}
runtime.taskState.mu.Lock()
runtime.taskState.tasks[taskID] = &backgroundTask{
ID: taskID,
Command: command,
Type: toolBash,
OutputPath: outputPath,
Process: process,
Status: "running",
}
runtime.taskState.mu.Unlock()
go func() {
state, waitErr := process.Wait()
_ = outputFile.Close()
runtime.taskState.mu.Lock()
task := runtime.taskState.tasks[taskID]
if task != nil && task.Status == "running" {
task.Status = backgroundTaskStatus(waitErr, state)
exitCode := backgroundTaskExitCode(waitErr, state)
task.ExitCode = &exitCode
}
runtime.taskState.mu.Unlock()
}()
return bashOutput{
Command: command,
ExitCode: 0,
Output: fmt.Sprintf("Command is running in the background. Read %s later to inspect the output.", outputPath),
BackgroundTaskID: taskID,
OutputPath: outputPath,
}, nil
}
Comment thread
Flash-LHR marked this conversation as resolved.

func backgroundTaskStatus(waitErr error, state *os.ProcessState) string {
if waitErr != nil {
return "exited"
}
if state == nil || !state.Success() {
return "exited"
}
return "completed"
}

func backgroundTaskExitCode(waitErr error, state *os.ProcessState) int {
if state != nil {
return state.ExitCode()
}
if waitErr != nil {
return 1
}
return 0
}

func errorsIsDeadlineExceeded(err error) bool {
return err == context.DeadlineExceeded
}

func bashDescription() string {
return fmt.Sprintf(`Execute a local shell command.

Usage:
- Use %s for shell-native tasks such as git, build, test, lint, package managers, and project scripts.
- Prefer dedicated tools when they fit better: use %s to read files, %s to create or overwrite files, %s for targeted text replacements, %s for notebook cell edits, %s for filename search, %s for repository content search, %s to fetch a specific URL, and %s for broad web discovery.
- NEVER use bash grep or rg for repository search when %s can answer the question.
- Commands run from the current workspace base directory.
- Use run_in_background for long-running commands that do not need an immediate result. Inspect them later with %s or stop them with %s.
- timeout is measured in milliseconds and is capped at %d ms.`, toolBash, toolRead, toolWrite, toolEdit, toolNotebookEdit, toolGlob, toolGrep, toolWebFetch, toolWebSearch, toolGrep, toolTaskOutput, toolTaskStop, maxBashTimeoutMs)
}
Loading
Loading