Atelia.Completion.Tools
0.1.0-preview.3
dotnet add package Atelia.Completion.Tools --version 0.1.0-preview.3
NuGet\Install-Package Atelia.Completion.Tools -Version 0.1.0-preview.3
<PackageReference Include="Atelia.Completion.Tools" Version="0.1.0-preview.3" />
<PackageVersion Include="Atelia.Completion.Tools" Version="0.1.0-preview.3" />
<PackageReference Include="Atelia.Completion.Tools" />
paket add Atelia.Completion.Tools --version 0.1.0-preview.3
#r "nuget: Atelia.Completion.Tools, 0.1.0-preview.3"
#:package Atelia.Completion.Tools@0.1.0-preview.3
#addin nuget:?package=Atelia.Completion.Tools&version=0.1.0-preview.3&prerelease
#tool nuget:?package=Atelia.Completion.Tools&version=0.1.0-preview.3&prerelease
Atelia.Completion.Tools - 快速上手(面向使用者)
net10.0;依赖 Abstractions 与 Diagnostics,不依赖 Completion 的 provider 实现。本包使用 MIT 许可证。
dotnet add package Atelia.Completion.Tools --version 0.1.0-preview.3
ToolSession 面向顺序使用、非线程安全。执行序号不会自动提供持久化、事务或 exactly-once;副作用提交、恢复与世界仲裁仍归宿主。Release 库内部 Debug 级别调用已被 [Conditional("DEBUG")] 编译裁掉,无法由环境变量恢复,需要源码 Debug 联调。
读者:要把宿主能力或结构化产物暴露给 LLM tool calling 的上层应用作者。 不读这份:要修改 schema 反射、raw JSON 绑定或执行器内部实现的人。那类工作请直接看
Declaration/ReflectedToolDefinitionBuilder.cs、ObjectInputToolRuntime.cs和对应测试。 配套阅读:Atelia.Completion的 client /CompletionRequest用法见 对应版本快速上手。本 README 只覆盖 tool 的定义、注册、执行和回灌。
1. 这层库解决什么问题
如果你直接把宿主能力暴露给 LLM,通常会很快遇到三类重复劳动:
- 手写
ToolDefinition/ JSON schema。 - 手写
RawArgumentsJson的解析、反序列化、DataAnnotations 校验。 - 手写“本轮对模型可见哪些工具、执行后怎样桥接回
ToolResultsMessage”的样板代码。
Atelia.Completion.Tools 把这三件事收口成一条统一主链:
DTO / Artifact class
↓ 反射
MethodToolWrapper / ArtifactToolWrapper<T>
↓ 注册
ToolRegistry
↓ CreateSession 工厂
ToolSession(持有可逐轮替换的 Access 快照,内化执行)
↓ VisibleDefinitions + ExecuteAsync(RawToolCall)
CompletionPromptPrefix.OutputContract.Tools / ToolResultsMessage
它提供三种核心能力:
MethodToolWrapper:把一个正常的业务方法包装成ITool。适合“读文件”“查索引”“调用宿主服务”这类能力型工具。ArtifactToolWrapper<T>:把“让模型交一个结构化产物T”包装成工具。适合 outline、patch plan、抽取结果、排序结果这类产物型工具。ToolSession:由registry.CreateSession(...)工厂产出,是使用方唯一持有的有状态对象。它按Access快照投影VisibleDefinitions,并内化执行把RawToolCall执行成ToolResult。
2. 心智模型
先分清三层状态:
ToolRegistry:共享注册表(静态、不可变)。通常跟宿主或 app 生命周期一致;通过CreateSession(...)工厂产出会话。ToolSession:单个 LLM session 的运行态,使用方唯一需要持有的有状态主概念。携带可逐轮替换的Access快照、IServiceProvider、任意Items,并内化了工具执行(ExecuteAsync)。
ToolExecutionContext 是工具真正看到的运行时入口,里面有:
Session:当前ToolSessionRawToolCall:模型原始发出的工具调用ExecutionSequence:本 session 内单调递增的执行序号(跨轮、跨工具集变化都不重置)Services/Items:直接透出自ToolSession
这意味着:
- 工具 schema 是注册时确定的。
- 工具可见性是 session 级决定的。
- 工具执行时的上下文数据也应该从 session 注入,而不是散落在全局静态变量里。
3. 先跑通:MethodToolWrapper
MethodToolWrapper 适合“宿主能力型工具”。主路径只有四步:
- 写一个输入 DTO。
- 写一个标了
[Tool]的方法。 - 用
MethodToolWrapper.FromMethod(...)或FromDelegate(...)包装。 - 注册到
ToolRegistry,由registry.CreateSession(...)创建ToolSession,再调用session.ExecuteAsync(...)。
3.1 最小示例
using System.ComponentModel;
using System.Text.Json.Serialization;
using Atelia.Completion.Abstractions;
using Atelia.Completion.Tools;
var host = new EchoTools();
var echoTool = MethodToolWrapper.FromMethod(
host,
typeof(EchoTools).GetMethod(nameof(EchoTools.EchoAsync))!
);
var registry = new ToolRegistry([echoTool]);
var session = registry.CreateSession(
items: new Dictionary<string, object?> { ["scope"] = "quick-start" }
);
var execution = await session.ExecuteAsync(
new RawToolCall("workspace.echo", "call-1", """{"text":"hello"}"""),
CancellationToken.None
);
Console.WriteLine(execution.ExecuteResult.GetFlattenedText());
// hello|quick-start|1
public sealed class EchoTools {
[Tool("workspace.echo", "Echo text and expose session scope.")]
public ValueTask<ToolExecuteResult> EchoAsync(
EchoInput input,
ToolExecutionContext context,
CancellationToken cancellationToken
) {
_ = cancellationToken;
var scope = context.Items is not null && context.Items.TryGetValue("scope", out var value)
? value as string
: null;
return ValueTask.FromResult(
ToolExecuteResult.FromText(
ToolExecutionStatus.Success,
$"{input.Text}|{scope}|{context.ExecutionSequence}"
)
);
}
}
[Description("Input for workspace.echo.")]
public sealed record class EchoInput(
[property: Description("Text to echo back.")]
[property: JsonPropertyName("text")]
string Text
);
3.2 这个例子里真正发生了什么
[Tool(...)]提供 tool-level description。它不是从 DTO 的[Description]推出来的。- 输入 DTO 的字段说明来自 DTO 属性上的
[Description]/[JsonPropertyName]/ DataAnnotations。 - 模型只会看到第一个业务输入参数,也就是
EchoInput的 schema。 ToolExecutionContext和CancellationToken是运行时基础设施参数,不会暴露给模型。
MethodToolWrapper 目前强制要求方法签名是:
ValueTask<ToolExecuteResult> Method(
TInput input,
ToolExecutionContext context,
CancellationToken cancellationToken
)
并且该方法必须带 [Tool(name, description)]。
3.3 适合什么场景
优先用 MethodToolWrapper 的典型场景:
- 读写宿主状态
- 查询索引 / 检索文档 / 调系统服务
- 需要访问
context.Services/context.Items - 需要返回不仅仅是“接受/拒绝”而是完整业务结果文本
如果你已经拿着一个已标 [Tool] 的方法委托,也可以直接用:
var echoTool = MethodToolWrapper.FromDelegate<EchoInput>(host.EchoAsync);
4. 结构化产物:ArtifactToolWrapper<T>
ArtifactToolWrapper<T> 适合“模型提交一个结构化产物,然后宿主决定接不接受”。
它不是把 T 当普通工具参数来处理,而是把“产物本身”作为主角:
- schema 从
T反射生成 - raw JSON 先过 schema 解析、反序列化、DataAnnotations 校验
- 全都成功后才把
T交给你的 handler
4.1 最小示例
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Atelia.Completion.Abstractions;
using Atelia.Completion.Tools;
var acceptedDrafts = new List<OutlineDraft>();
var submitDraft = ArtifactToolWrapper<OutlineDraft>.Create(
"draft.submit",
(draft, context) => {
acceptedDrafts.Add(draft);
return new ValidateResult(true, $"saved:{draft.Title}|{context.ExecutionSequence}");
}
);
var session = new ToolRegistry([submitDraft]).CreateSession();
var execution = await session.ExecuteAsync(
new RawToolCall(
"draft.submit",
"call-2",
"""{"title":"Atelia Tools","sections":["Why","How"]}"""
),
CancellationToken.None
);
Console.WriteLine(execution.ExecuteResult.GetFlattenedText());
// saved:Atelia Tools|1
[Description("Draft outline submitted by the model.")]
public sealed class OutlineDraft {
[Description("Document title.")]
[MinLength(3)]
[JsonPropertyName("title")]
public string Title { get; init; } = string.Empty;
[Description("Top-level sections.")]
[JsonPropertyName("sections")]
public IReadOnlyList<string> Sections { get; init; } = [];
}
4.2 什么时候优先用它
优先用 ArtifactToolWrapper<T> 的场景:
- 让模型交一份 outline / patch plan / analysis report
- 让模型做结构化抽取
- 让模型给出排序、打标签、分类结果
- 你真正想保留的是一个
T,不是一串自由文本
和 MethodToolWrapper 的关键区别是:
MethodToolWrapper更像“调用宿主能力”。ArtifactToolWrapper<T>更像“向宿主提交结构化作品”。
4.3 handler 只看见“已经过关”的 T
ArtifactToolWrapper<T> 的 handler 只有在下面三关都通过后才会被调用:
- raw JSON 能按 schema 解析
- JSON 能反序列化成
T T通过 DataAnnotations 校验
因此如果模型多传未知字段、漏必填字段、或产物对象不满足 [MinLength] / [Range] 等约束,handler 根本不会执行,工具会直接返回 Failed。
5. ToolSession 怎么接进 Completion 主循环
ToolSession 有两个直接用途:
- 把当前 session 可见的工具投影成
CompletionPromptPrefix.OutputContract.Tools - 把模型返回的
RawToolCall执行成ToolResult,再回灌成ToolResultsMessage
最小接法是:
var session = registry.CreateSession();
var history = new List<IHistoryMessage> {
new ObservationMessage("Read README and propose an outline."),
};
var request = new CompletionRequest(
"Qwen3.5-27b-GPTQ-Int4",
new CompletionPromptPrefix(
"You are a helpful coding agent.",
CompletionOutputContract.ProviderDefault(session.VisibleDefinitions),
history
),
tailMessages: []
);
var completion = await client.StreamCompletionAsync(request, null, cancellationToken);
if (completion.Termination.Kind != CompletionTerminationKind.Completed) {
throw new InvalidOperationException($"Completion ended: {completion.Termination.Kind}");
}
history.Add(completion.Message);
if (completion.Message.ToolCalls.Count > 0) {
var toolResults = new List<ToolResult>();
foreach (var call in completion.Message.ToolCalls) {
var execution = await session.ExecuteAsync(call, cancellationToken);
toolResults.Add(execution.ToToolResult());
}
history.Add(new ToolResultsMessage(content: null, results: toolResults));
}
这里的桥接关系非常重要:
completion.Message.ToolCalls给你RawToolCallToolSession.ExecuteAsync(...)给你ToolCallExecutionResultToolCallExecutionResult.ToToolResult()变成可回灌的ToolResultnew ToolResultsMessage(null, results)再喂回下一轮请求的 shared context 或 tail
如果你已经在用 Atelia.Completion,这一段就是 tool loop 的主拼接点。
6. session 可见性与运行时注入
ToolSession 负责当前 session 的三件事:
Access(ToolAccessSnapshot):哪些工具对当前模型可见 / 可执行,可逐轮替换Services:工具运行时依赖的服务容器Items:轻量 session 数据,比如 scope、repo root、当前用户上下文
示例:隐藏某个工具
var session = registry.CreateSession(
access: new ToolAccessSnapshot(hiddenToolNames: ["internal.only"]),
items: new Dictionary<string, object?> { ["scope"] = "review" }
);
此时:
session.VisibleDefinitions不会把internal.only暴露给模型- 即使模型伪造了
RawToolCall("internal.only", ...),ExecuteAsync(...)也会返回Failed,而不是执行成功
这是一层简单但有用的 defense in depth。
7. DTO / Artifact 声明约束
这套反射式声明器故意不是“任意 CLR 类型都能自动映射”。当前主线建议把输入对象控制在它自然擅长的范围内。
7.1 已支持的主路径
- 根类型必须是 concrete
class/record class - 标量支持:
string、bool、int、long、double、非[Flags]enum - 集合支持:数组、
List<T>、IReadOnlyList<T> - 对象支持:嵌套 object
- 元数据支持:
[Description]、[JsonPropertyName]、[Required]、[Range]、[StringLength]、[MinLength]、[RegularExpression]
7.2 当前不适合的类型
- 循环对象图
- nullable object / nullable array / nullable collection element
[Flags]enum- 条件式
JsonIgnore(Condition = ...) - 需要自定义反序列化协议的复杂 union
如果你的输入类型明显超出上面范围,直接手写 ITool 往往更清晰。
8. 常见坑
MethodToolWrapper的方法签名不对:必须正好是“一个业务输入对象 +ToolExecutionContext+CancellationToken”,返回ValueTask<ToolExecuteResult>。- 把 DTO 的
[Description]当成 tool description:MethodToolWrapper的 tool description 来自[Tool],不是 DTO 根类型的[Description]。 - 模型多传未知字段:schema 默认
additionalProperties: false,会直接触发解析失败。 - 以为 validation 失败后 handler 还会跑:不会。
MethodToolWrapper和ArtifactToolWrapper<T>都会在调用业务逻辑前拦住。 - 用大小写不同的重复 JSON 名:反射 schema 时会直接抛错。
- 想把
ToolExecutionContext里的运行时状态偷偷放到 schema 里:不应该。它本来就是 runtime-only 参数。
9. 何时不该用 wrapper
下面这些情况,通常应该直接手写 ITool:
- 你需要完全自定义的参数协议
- 你要处理超出反射声明器能力边界的类型
- 你要返回非文本
ToolResultBlock(未来若扩展)或更特别的执行语义 - 你根本不需要反射 schema,只想手写
ToolDefinition
如果你只需要“从一个类型生成 ToolDefinition”,但不需要可执行工具,可以直接用:
var definition = ReflectedToolDefinitionBuilder.BuildDefinitionUsingTypeDescription<MyInput>("tool.name");
10. 可运行样例
本 README 中的主路径样例已经落实成可执行测试,方便以后改 API 时及时发现文档漂移:
- tests/Completion.Tests/Tools/CompletionToolsQuickStartSamplesTests.cs
- tests/Completion.Tests/Tools/MethodToolWrapperTests.cs
- tests/Completion.Tests/Tools/ArtifactToolWrapperTests.cs
- tests/Completion.Tests/Tools/ToolSessionTests.cs
只跑这批样例可用:
dotnet test tests/Completion.Tests/Completion.Tests.csproj --filter "FullyQualifiedName~Completion.Tools"
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Atelia.Completion.Abstractions (>= 0.1.0-preview.3)
- Atelia.Diagnostics (>= 0.1.0-preview.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0-preview.3 | 49 | 9/23/2026 |
| 0.1.0-preview.2 | 45 | 9/20/2026 |
| 0.1.0-preview.1 | 60 | 9/14/2026 |