diff --git a/docs/architecture.md b/docs/architecture.md index 10b7920..637f3aa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -49,7 +49,7 @@ Kernel 以 context、skills、memory、模型、会话和工具等能力域定 `AgentObserver` 接收持久 `session.event`、模型 usage 及模型/工具的开始、完成、失败观测事件。Runtime 注入的 Clock 使这些事件包含稳定关联 ID 与耗时;传入执行上下文时,所有模型和工具事件还直接携带冻结后的 `executionId`。投递是最佳努力,可靠 trace 与审计由 Host 或 Platform adapter 的 outbox 实现。 -工具领域只位于 `kernel/tools/protocol.ts` 与 `kernel/tools/runtime.ts`。`PolicyControlledTools` 在副作用前调用策略;Sandbox、Platform 许可与 MCP 通过 adapters 接入。Kernel 和 Core 不依赖外部网络协议、账户模型、SDK、文件系统或进程。 +工具领域只位于 `kernel/tools/protocol.ts` 与 `kernel/tools/runtime.ts`。`ToolContract` 定义工具调用契约,`ExecutableTool` 附带直接执行器,`ToolSet` 提供一次运行固定的模型目录与调用路由;模型只接收 `ModelToolDefinition` 投影。`PolicyControlledToolSet` 在副作用前调用策略;Sandbox、Platform 许可与 MCP 通过 adapters 接入。Kernel 和 Core 不依赖外部网络协议、账户模型、SDK、文件系统或进程。 ## 生命周期 diff --git a/docs/decisions/0054-tool-contract-and-set.md b/docs/decisions/0054-tool-contract-and-set.md new file mode 100644 index 0000000..624f592 --- /dev/null +++ b/docs/decisions/0054-tool-contract-and-set.md @@ -0,0 +1,35 @@ +# ADR 0054:工具契约、工具集与模型投影 + +状态:已接受 + +## 背景 + +工具领域此前以 `ModelToolDefinition` 作为 `ToolSpecification` 和 `ToolDefinition` 的父接口,容易让模型传输投影看起来像工具领域的原始定义。`ToolDefinition` 实际表示带直接执行器的工具,但名称未说明这一点。`ToolCapability` 同时拥有目录和执行路由,而其父接口 `ToolCatalog` 没有独立消费者。 + +这会模糊工具自身契约、模型可见投影和一次 Agent 运行中工具集三种不同概念,也扩大了公共类型层次。 + +## 决策 + +工具领域使用以下术语: + +- `ToolContract` 是工具的原始调用契约,拥有名称、描述、输入 schema 和输入规范化器;`parseInput` 是不可信模型输入进入执行器前的权威校验。 +- `ExecutableTool` 扩展 `ToolContract` 并提供直接执行器。它不表示操作必须在本地完成;执行器可使用组合方显式注入的外部依赖。 +- `ModelToolDefinition` 是 `ToolContract` 的只读模型投影,由 `toModelToolDefinition` 构造,不再作为工具契约的父接口。 +- `ToolSet` 表示一次 Agent 运行中固定的模型目录与调用路由,直接定义 `list` 和 `execute`。删除无独立消费者的 `ToolCatalog`。 + +`CompositeToolSet`、`PolicyControlledToolSet`、`SandboxToolSet` 和 MCP adapter 均实现 `ToolSet`。Sandbox 仍是执行环境:`SandboxToolSet` 仅将已校验的 `ToolContract` 调用委派给 `SandboxDispatcher`,不把沙箱定义为工具。 + +本决定不新增 `ToolId`、版本或通用执行后端抽象;当前运行内唯一的工具名称与持久化的 `ToolCallId` 已满足路由和审计需要。 + +## 后果 + +Core 继续只依赖 Kernel 的 `ToolSet`,对本地、MCP、策略或沙箱执行方式无感知。模型 adapter 继续只接收 `ModelToolDefinition` 并负责供应商 function-calling 协议编解码。具体工具与外部 I/O 仍留在 adapters。 + +这是公开 TypeScript API 的破坏性命名调整;所有消费者必须迁移到新的领域术语。工具调用、工具结果、会话事件、恢复和 Core 的执行顺序不变。 + +## 放弃的方案 + +- 保留旧接口并提供别名:会长期暴露两套词汇,不能真正消除领域歧义。 +- 将 `ToolSet` 替换为裸 `ExecutableTool[]`:会迫使 Core 获得名称路由、远程适配和组合职责。 +- 新建 Local/MCP/Sandbox 的继承树:会将传输协议和执行环境细节混入工具的核心身份。 +- 为尚未出现的跨名称治理需求新增 `ToolId`、版本或执行后端框架:会增加无当前消费者的抽象。 diff --git a/packages/adapters/local/src/shell-tool.ts b/packages/adapters/local/src/shell-tool.ts index c76bad7..6f86453 100644 --- a/packages/adapters/local/src/shell-tool.ts +++ b/packages/adapters/local/src/shell-tool.ts @@ -1,6 +1,6 @@ import { type ChildProcess, spawn } from 'node:child_process'; import { isAbsolute } from 'node:path'; -import type { JsonValue, ToolDefinition } from '@agent-harness/kernel'; +import type { ExecutableTool, JsonValue } from '@agent-harness/kernel'; /** kill 后等待 close 的宽限;超时则强制结束等待。 */ const DEFAULT_KILL_GRACE_MS = 1000; @@ -29,7 +29,7 @@ export interface ShellToolOptions { } /** 创建在固定 cwd 下执行 shell 命令的 Host 侧工具。 */ -export function createShellTool(options: ShellToolOptions): ToolDefinition { +export function createShellTool(options: ShellToolOptions): ExecutableTool { if (!isAbsolute(options.cwd)) { throw new TypeError('shell cwd 必须是绝对路径'); } diff --git a/packages/adapters/local/src/workspace-tools.ts b/packages/adapters/local/src/workspace-tools.ts index a29c5eb..4e22b41 100644 --- a/packages/adapters/local/src/workspace-tools.ts +++ b/packages/adapters/local/src/workspace-tools.ts @@ -1,13 +1,13 @@ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'; import { dirname } from 'node:path'; -import type { JsonObject, JsonValue, ToolDefinition } from '@agent-harness/kernel'; +import type { ExecutableTool, JsonObject, JsonValue } from '@agent-harness/kernel'; import { resolveInsideWorkspace } from './workspace.js'; const MAX_FILE_BYTES = 256 * 1024; const MAX_LIST_ENTRIES = 200; /** 创建只能在固定工作目录内列目录和读写文件的工具。 */ -export function createWorkspaceTools(workspace: string): readonly ToolDefinition[] { +export function createWorkspaceTools(workspace: string): readonly ExecutableTool[] { return [ { name: 'list_dir', diff --git a/packages/adapters/mcp/src/adapter.ts b/packages/adapters/mcp/src/adapter.ts index b9086f0..1232cda 100644 --- a/packages/adapters/mcp/src/adapter.ts +++ b/packages/adapters/mcp/src/adapter.ts @@ -1,10 +1,6 @@ /** 将 MCP client 映射为 Agent 的工具 capability。 */ -import type { - ModelToolDefinition, - ToolCapability, - ToolExecutionContext, -} from '@agent-harness/kernel'; +import type { ModelToolDefinition, ToolSet, ToolExecutionContext } from '@agent-harness/kernel'; import { assertJsonValue, isAbortError, @@ -34,7 +30,7 @@ export class McpToolsNotStartedError extends Error { } /** 已启动的 MCP 工具表;同一实例由组合方显式拥有 start 与 close 生命周期。 */ -export class McpToolAdapter implements ToolCapability { +export class McpToolAdapter implements ToolSet { readonly #client: McpClient; #tools: ReadonlyMap | undefined; #startPromise: Promise | undefined; diff --git a/packages/adapters/memory-gateway/src/writer-tool.ts b/packages/adapters/memory-gateway/src/writer-tool.ts index 3ceb597..d5ca2f5 100644 --- a/packages/adapters/memory-gateway/src/writer-tool.ts +++ b/packages/adapters/memory-gateway/src/writer-tool.ts @@ -1,4 +1,4 @@ -import { type JsonValue, MEMORY_CONTEXT_LIMITS, type ToolDefinition } from '@agent-harness/kernel'; +import { type ExecutableTool, type JsonValue, MEMORY_CONTEXT_LIMITS } from '@agent-harness/kernel'; import { boundedText } from './validation.js'; const MAX_MEMORY_STATEMENT_BYTES = 4_096; @@ -20,7 +20,7 @@ export interface MemoryGatewayWriterToolOptions { } /** 创建只能保存用户显式长期陈述的 Platform Memory function tool。 */ -export function createMemoryWriterTool(options: MemoryGatewayWriterToolOptions): ToolDefinition { +export function createMemoryWriterTool(options: MemoryGatewayWriterToolOptions): ExecutableTool { const memorySpaceId = boundedText( options.memorySpaceId, MEMORY_CONTEXT_LIMITS.maxFieldBytes, diff --git a/packages/adapters/skill-tool/src/skill-loader.ts b/packages/adapters/skill-tool/src/skill-loader.ts index 0da984e..80e8aa1 100644 --- a/packages/adapters/skill-tool/src/skill-loader.ts +++ b/packages/adapters/skill-tool/src/skill-loader.ts @@ -1,4 +1,4 @@ -import type { JsonValue, SkillCatalog, ToolDefinition } from '@agent-harness/kernel'; +import type { ExecutableTool, JsonValue, SkillCatalog } from '@agent-harness/kernel'; const DEFAULT_MAX_CONTENT_BYTES = 256 * 1024; const MAX_SKILL_ID_BYTES = 2_048; @@ -11,7 +11,7 @@ export interface SkillLoaderToolOptions { export function createSkillLoaderTool( skills: SkillCatalog, options: SkillLoaderToolOptions = {}, -): ToolDefinition { +): ExecutableTool { const maxContentBytes = positiveInteger( options.maxContentBytes ?? DEFAULT_MAX_CONTENT_BYTES, 'maxContentBytes', diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index 7b21b78..eeae44a 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -21,7 +21,7 @@ import { type SessionLog, SettlementPersistenceError, type SettlementSignalFactory, - type ToolCapability, + type ToolSet, type ToolResult, type TurnId, type UserMessage, @@ -52,7 +52,7 @@ const DEFAULT_MAX_QUEUED_RUNS = 32; export class Agent { readonly #model: ModelProvider; readonly #session: SessionLog; - readonly #tools: ToolCapability; + readonly #tools: ToolSet; readonly #ids: AgentIdGenerator; readonly #maxSteps: number; readonly #settlementSignals: SettlementSignalFactory; diff --git a/packages/hosts/cli/src/composition.ts b/packages/hosts/cli/src/composition.ts index 2d35e31..d61247f 100644 --- a/packages/hosts/cli/src/composition.ts +++ b/packages/hosts/cli/src/composition.ts @@ -14,9 +14,9 @@ import { PlatformHttpClient } from '@agent-harness/adapters/platform-http'; import type { FilesystemSkillCatalog } from '@agent-harness/adapters/skill-filesystem'; import { createSkillLoaderTool } from '@agent-harness/adapters/skill-tool'; import { - CompositeToolCapability, + CompositeToolSet, DuplicateToolNameError, - type ToolDefinition, + type ExecutableTool, ToolRegistry, } from '@agent-harness/kernel'; import { type CliConfig, type CliModelConfig, CliUsageError } from './config.js'; @@ -48,10 +48,10 @@ export function composeTools( config: CliConfig, mcp: McpToolAdapter | undefined, skills: FilesystemSkillCatalog | undefined, - memoryWriter: ToolDefinition | undefined, -): CompositeToolCapability { + memoryWriter: ExecutableTool | undefined, +): CompositeToolSet { try { - return new CompositeToolCapability([ + return new CompositeToolSet([ new ToolRegistry([ ...buildTools(config), ...(skills === undefined || config.skills === undefined @@ -104,7 +104,7 @@ export function createMemoryCapabilities( readonly importer: MemoryGatewayDocumentImporter; readonly documentDeleter: MemoryGatewayDocumentDeleter; readonly documentStatuses: MemoryGatewayDocumentStatusReader; - readonly writerTool?: ToolDefinition; + readonly writerTool?: ExecutableTool; } { const authorization = requiredEnvironment(dependencies.env, config.authorizationEnv); const client = new PlatformHttpClient({ @@ -150,7 +150,7 @@ function requiredEnvironment( return value; } -function buildTools(config: CliConfig): readonly ToolDefinition[] { +function buildTools(config: CliConfig): readonly ExecutableTool[] { if (!config.shell.enabled) { return []; } diff --git a/packages/kernel/src/agent.ts b/packages/kernel/src/agent.ts index f28faba..348f9c5 100644 --- a/packages/kernel/src/agent.ts +++ b/packages/kernel/src/agent.ts @@ -9,7 +9,7 @@ import type { } from './identifiers.js'; import type { ModelProvider, ModelStreamEvent } from './model.js'; import type { EventEnvelope, SessionLog, TurnOutcome } from './session/protocol.js'; -import type { ToolCallId, ToolCapability } from './tools/protocol.js'; +import type { ToolCallId, ToolSet } from './tools/protocol.js'; /** 当前 Agent 状态。 */ export type AgentPhase = @@ -120,7 +120,7 @@ export interface AgentIdReservationSeed { export interface AgentOptions { readonly model: ModelProvider; readonly session: SessionLog; - readonly tools: ToolCapability; + readonly tools: ToolSet; readonly ids: AgentIdGenerator; readonly maxSteps: number; readonly settlementSignals: SettlementSignalFactory; diff --git a/packages/kernel/src/index.ts b/packages/kernel/src/index.ts index 5c36d6d..a891898 100644 --- a/packages/kernel/src/index.ts +++ b/packages/kernel/src/index.ts @@ -13,9 +13,9 @@ export * from './session/submission.js'; export * from './skills/protocol.js'; export * from './tools/protocol.js'; export { - CompositeToolCapability, + CompositeToolSet, DuplicateToolNameError, - PolicyControlledTools, - SandboxToolRegistry, + PolicyControlledToolSet, + SandboxToolSet, ToolRegistry, } from './tools/runtime.js'; diff --git a/packages/kernel/src/tools/protocol.ts b/packages/kernel/src/tools/protocol.ts index 44881bb..9202c5c 100644 --- a/packages/kernel/src/tools/protocol.ts +++ b/packages/kernel/src/tools/protocol.ts @@ -12,7 +12,7 @@ export interface ToolCall { readonly input: JsonValue; } -/** 暴露给模型的工具定义。 */ +/** 暴露给模型的工具定义。它是工具契约的只读投影,不包含执行或输入校验行为。 */ export interface ModelToolDefinition { readonly name: string; readonly description: string; @@ -77,24 +77,37 @@ export interface ToolExecutionContext { readonly execution?: ExecutionMetadata; } -/** 工具规格 —— 模型可见描述与 Host 侧输入规范化。 */ -export interface ToolSpecification extends ModelToolDefinition { +/** + * 工具的调用契约。 + * + * 它是工具领域的原始定义:名称用于本次运行内的模型调用与路由,schema 用于模型请求, + * parseInput 则是执行前处理不可信模型输入的权威边界。 + */ +export interface ToolContract { + readonly name: string; + readonly description: string; + readonly inputSchema: Readonly>; parseInput(input: JsonValue): JsonValue; } -/** 本地工具定义 —— 在工具规格上附加进程内执行函数。 */ -export interface ToolDefinition extends ToolSpecification { +/** 直接附带执行器的工具。执行器可以访问任何由组合方显式注入的外部依赖。 */ +export interface ExecutableTool extends ToolContract { execute(input: JsonValue, context: ToolExecutionContext): Promise; } -/** 工具目录。 */ -export interface ToolCatalog { +/** 一次 Agent 运行中固定的工具目录与执行路由。 */ +export interface ToolSet { list(): readonly ModelToolDefinition[]; + execute(call: ToolCall, context: ToolExecutionContext): Promise; } -/** 工具能力 —— 工具的执行能力和目录。 */ -export interface ToolCapability extends ToolCatalog { - execute(call: ToolCall, context: ToolExecutionContext): Promise; +/** 从工具调用契约构造模型可见的只读定义。 */ +export function toModelToolDefinition(tool: ToolContract): ModelToolDefinition { + return { + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + }; } /** 工具授权决策。 */ @@ -132,7 +145,7 @@ export interface SandboxDispatcher { } /** 沙盒工具注册选项。 */ -export interface SandboxToolRegistryOptions { +export interface SandboxToolSetOptions { readonly workspace?: string; readonly limits?: SandboxResourceLimits; } diff --git a/packages/kernel/src/tools/runtime.ts b/packages/kernel/src/tools/runtime.ts index c26afe5..ca196ce 100644 --- a/packages/kernel/src/tools/runtime.ts +++ b/packages/kernel/src/tools/runtime.ts @@ -1,26 +1,27 @@ import type { ExecutionMetadata } from '../agent.js'; import type { JsonValue } from '../serialization.js'; import { assertJsonValue, isAbortError, serializeError } from '../serialization.js'; +import { toModelToolDefinition } from './protocol.js'; import type { ModelToolDefinition, SandboxDispatcher, SandboxResourceLimits, - SandboxToolRegistryOptions, + SandboxToolSetOptions, ToolCall, - ToolCapability, - ToolDefinition, + ExecutableTool, ToolExecutionContext, ToolExecutionPolicy, ToolResult, - ToolSpecification, + ToolContract, + ToolSet, } from './protocol.js'; -/** 不可变的本地工具注册表,也是顺序工具执行器。 */ -export class ToolRegistry implements ToolCapability { - readonly #definitions: ReadonlyMap; +/** 不可变的直接执行工具注册表,也是顺序工具执行器。 */ +export class ToolRegistry implements ToolSet { + readonly #definitions: ReadonlyMap; - constructor(definitions: readonly ToolDefinition[]) { - this.#definitions = indexToolDefinitions(definitions); + constructor(definitions: readonly ExecutableTool[]) { + this.#definitions = indexTools(definitions); } list(): readonly ModelToolDefinition[] { @@ -53,19 +54,19 @@ export class ToolRegistry implements ToolCapability { } } -/** 将已定义工具的执行委派给 Sandbox,并在 Host 侧先校验模型输入。 */ -export class SandboxToolRegistry implements ToolCapability { - readonly #definitions: ReadonlyMap; +/** 将工具契约的执行委派给 Sandbox,并在 Host 侧先校验模型输入。 */ +export class SandboxToolSet implements ToolSet { + readonly #definitions: ReadonlyMap; readonly #dispatcher: SandboxDispatcher; readonly #workspace: string | undefined; readonly #limits: SandboxResourceLimits | undefined; constructor( - definitions: readonly ToolSpecification[], + definitions: readonly ToolContract[], dispatcher: SandboxDispatcher, - options: SandboxToolRegistryOptions = {}, + options: SandboxToolSetOptions = {}, ) { - this.#definitions = indexToolDefinitions(definitions); + this.#definitions = indexTools(definitions); const normalized = normalizeSandboxOptions(options); this.#dispatcher = dispatcher; this.#workspace = normalized.workspace; @@ -111,11 +112,11 @@ export class SandboxToolRegistry implements ToolCapability { } /** 在真实工具前强制执行策略,并把拒绝作为普通工具失败持久化。 */ -export class PolicyControlledTools implements ToolCapability { - readonly #delegate: ToolCapability; +export class PolicyControlledToolSet implements ToolSet { + readonly #delegate: ToolSet; readonly #policy: ToolExecutionPolicy; - constructor(delegate: ToolCapability, policy: ToolExecutionPolicy) { + constructor(delegate: ToolSet, policy: ToolExecutionPolicy) { this.#delegate = delegate; this.#policy = policy; } @@ -163,20 +164,20 @@ export class DuplicateToolNameError extends Error { } /** 将多个已就绪的工具来源合并为单一目录,并按工具名路由执行。 */ -export class CompositeToolCapability implements ToolCapability { +export class CompositeToolSet implements ToolSet { readonly #definitions: readonly ModelToolDefinition[]; - readonly #routes: ReadonlyMap; + readonly #routes: ReadonlyMap; - constructor(capabilities: readonly ToolCapability[]) { + constructor(toolSets: readonly ToolSet[]) { const definitions: ModelToolDefinition[] = []; - const routes = new Map(); - for (const capability of capabilities) { - for (const definition of capability.list()) { + const routes = new Map(); + for (const toolSet of toolSets) { + for (const definition of toolSet.list()) { if (routes.has(definition.name)) { throw new DuplicateToolNameError(definition.name); } assertJsonValue(definition.inputSchema); - routes.set(definition.name, capability); + routes.set(definition.name, toolSet); definitions.push( Object.freeze({ name: definition.name, @@ -208,9 +209,7 @@ export class CompositeToolCapability implements ToolCapability { } } -function indexToolDefinitions( - definitions: readonly T[], -): ReadonlyMap { +function indexTools(definitions: readonly T[]): ReadonlyMap { const entries = new Map(); for (const definition of definitions) { if (entries.has(definition.name)) { @@ -222,13 +221,9 @@ function indexToolDefinitions( } function toModelToolDefinitions( - definitions: ReadonlyMap, + definitions: ReadonlyMap, ): readonly ModelToolDefinition[] { - return [...definitions.values()].map((definition) => ({ - name: definition.name, - description: definition.description, - inputSchema: definition.inputSchema, - })); + return [...definitions.values()].map(toModelToolDefinition); } function authorizationFailure(call: ToolCall, message: string): ToolResult { @@ -317,7 +312,7 @@ function denialReason(decision: unknown): string { return '执行策略返回无效授权决定'; } -function normalizeSandboxOptions(options: SandboxToolRegistryOptions): { +function normalizeSandboxOptions(options: SandboxToolSetOptions): { readonly workspace: string | undefined; readonly limits: SandboxResourceLimits | undefined; } { diff --git a/packages/kernel/test/tools.test.ts b/packages/kernel/test/tools.test.ts index cc7b584..a2f92e1 100644 --- a/packages/kernel/test/tools.test.ts +++ b/packages/kernel/test/tools.test.ts @@ -1,9 +1,15 @@ -import { brandId, type ExecutionId, type ToolCallId } from '@agent-harness/kernel'; +import { + brandId, + type ExecutionId, + toModelToolDefinition, + type ToolCallId, + type ToolContract, +} from '@agent-harness/kernel'; import { describe, expect, it } from 'vitest'; import { - CompositeToolCapability, - PolicyControlledTools, - SandboxToolRegistry, + CompositeToolSet, + PolicyControlledToolSet, + SandboxToolSet, ToolRegistry, } from '../src/index.ts'; @@ -15,6 +21,23 @@ function call(name: string, input: unknown = {}) { }; } +describe('toModelToolDefinition', () => { + it('仅投影工具契约中模型可见的字段', () => { + const tool: ToolContract = { + name: 'lookup', + description: '查询', + inputSchema: { type: 'object' }, + parseInput: (input) => input, + }; + + expect(toModelToolDefinition(tool)).toEqual({ + name: 'lookup', + description: '查询', + inputSchema: { type: 'object' }, + }); + }); +}); + describe('ToolRegistry', () => { it('拒绝重复工具注册', () => { const definition = { @@ -171,7 +194,7 @@ describe('ToolRegistry', () => { it('策略拒绝时不执行工具,并把拒绝持久化为失败结果', async () => { let executed = false; - const tools = new PolicyControlledTools( + const tools = new PolicyControlledToolSet( new ToolRegistry([ { name: 'write', @@ -208,7 +231,7 @@ describe('ToolRegistry', () => { it('策略收到冻结上下文后才委派工具', async () => { const observed: string[] = []; - const tools = new PolicyControlledTools( + const tools = new PolicyControlledToolSet( new ToolRegistry([ { name: 'write', @@ -247,7 +270,7 @@ describe('ToolRegistry', () => { release = resolve; }); const observed: unknown[] = []; - const tools = new PolicyControlledTools( + const tools = new PolicyControlledToolSet( new ToolRegistry([ { name: 'write', @@ -308,7 +331,7 @@ describe('ToolRegistry', () => { release = resolve; }); const controller = new AbortController(); - const tools = new PolicyControlledTools(new ToolRegistry([]), { + const tools = new PolicyControlledToolSet(new ToolRegistry([]), { authorize: async () => { await authorized; if (outcome === 'error') { @@ -346,7 +369,7 @@ describe('ToolRegistry', () => { it('冻结 JSON 时保留 __proto__ 自有字段且不修改对象原型', async () => { const input = JSON.parse('{"__proto__":{"admin":true},"value":1}') as Record; let observedInput: unknown; - const tools = new PolicyControlledTools( + const tools = new PolicyControlledToolSet( new ToolRegistry([ { name: 'inspect', @@ -386,7 +409,7 @@ describe('ToolRegistry', () => { it('无效策略结果 fail closed,且不执行工具', async () => { let executed = false; - const tools = new PolicyControlledTools( + const tools = new PolicyControlledToolSet( new ToolRegistry([ { name: 'write', @@ -423,7 +446,7 @@ describe('ToolRegistry', () => { it('Sandbox 在派发前校验输入,并传递任务边界', async () => { const tasks: unknown[] = []; const executions: unknown[] = []; - const tools = new SandboxToolRegistry( + const tools = new SandboxToolSet( [ { name: 'write', @@ -478,7 +501,7 @@ describe('ToolRegistry', () => { it('Sandbox 输入校验失败时不派发任务', async () => { let dispatched = false; - const tools = new SandboxToolRegistry( + const tools = new SandboxToolSet( [ { name: 'write', @@ -524,14 +547,14 @@ describe('ToolRegistry', () => { expect( () => - new SandboxToolRegistry([definition, definition], { + new SandboxToolSet([definition, definition], { dispatch: async () => ({ ok: true }), }), ).toThrow('duplicate tool registration: write'); }); it('Sandbox list 只暴露模型可见 schema', () => { - const tools = new SandboxToolRegistry( + const tools = new SandboxToolSet( [ { name: 'write', @@ -554,7 +577,7 @@ describe('ToolRegistry', () => { it('Sandbox 缺少冻结执行上下文时拒绝派发', async () => { let dispatched = false; - const tools = new SandboxToolRegistry( + const tools = new SandboxToolSet( [ { name: 'write', @@ -583,7 +606,7 @@ describe('ToolRegistry', () => { it('Sandbox 复制冻结资源限制,并拒绝不完整限制', async () => { const limits = { timeoutMs: 1_000, maxOutputBytes: 4_096 }; const observed: unknown[] = []; - const tools = new SandboxToolRegistry( + const tools = new SandboxToolSet( [ { name: 'write', @@ -613,7 +636,7 @@ describe('ToolRegistry', () => { expect(observed).toEqual([{ timeoutMs: 1_000, maxOutputBytes: 4_096 }]); expect( () => - new SandboxToolRegistry( + new SandboxToolSet( [], { dispatch: async () => ({}) }, { @@ -625,7 +648,7 @@ describe('ToolRegistry', () => { it('Sandbox 在跨边界前拒绝 parser 返回的非 JSON', async () => { let dispatched = false; - const tools = new SandboxToolRegistry( + const tools = new SandboxToolSet( [ { name: 'write', @@ -656,7 +679,7 @@ describe('ToolRegistry', () => { }); }); -describe('CompositeToolCapability', () => { +describe('CompositeToolSet', () => { const registry = (name: string) => new ToolRegistry([ { @@ -669,7 +692,7 @@ describe('CompositeToolCapability', () => { ]); it('合并目录并按名称路由执行', async () => { - const tools = new CompositeToolCapability([registry('local'), registry('remote')]); + const tools = new CompositeToolSet([registry('local'), registry('remote')]); expect(tools.list().map((tool) => tool.name)).toEqual(['local', 'remote']); await expect( tools.execute(call('remote', { q: 'x' }), { signal: new AbortController().signal }), @@ -680,7 +703,7 @@ describe('CompositeToolCapability', () => { }); it('在组合时拒绝跨来源重复工具名', () => { - expect(() => new CompositeToolCapability([registry('same'), registry('same')])).toThrow( + expect(() => new CompositeToolSet([registry('same'), registry('same')])).toThrow( 'duplicate tool registration: same', ); }); @@ -692,7 +715,7 @@ describe('CompositeToolCapability', () => { list: () => [{ name: 'stable', description: 'stable', inputSchema }], execute: source.execute.bind(source), }; - const tools = new CompositeToolCapability([mutableSource]); + const tools = new CompositeToolSet([mutableSource]); inputSchema.type = 'array'; expect(tools.list()).toEqual([ { @@ -712,7 +735,7 @@ describe('CompositeToolCapability', () => { return { status: 'success', callId: toolCall.id, output: { ok: true } }; }, }; - const tools = new CompositeToolCapability([aware]); + const tools = new CompositeToolSet([aware]); await expect( tools.execute(call('write'), { signal: new AbortController().signal, @@ -727,7 +750,7 @@ describe('CompositeToolCapability', () => { }); it('对未知工具和已取消调用返回稳定结果', async () => { - const tools = new CompositeToolCapability([]); + const tools = new CompositeToolSet([]); await expect( tools.execute(call('missing'), { signal: new AbortController().signal }), ).resolves.toMatchObject({ diff --git a/packages/runtime/src/runtime.ts b/packages/runtime/src/runtime.ts index 93abc70..c1085ab 100644 --- a/packages/runtime/src/runtime.ts +++ b/packages/runtime/src/runtime.ts @@ -11,7 +11,7 @@ import { type Clock, type MessageId, type ModelProvider, - PolicyControlledTools, + PolicyControlledToolSet, type PromptAssembler, type SessionId, type SessionLifecycleIdNamespaces, @@ -19,7 +19,7 @@ import { type SessionRepository, type SettlementSignalFactory, type StepId, - type ToolCapability, + type ToolSet, type ToolExecutionPolicy, type TurnId, } from '@agent-harness/kernel'; @@ -61,7 +61,7 @@ export class UuidAgentIds implements AgentIdGenerator { export interface AgentRuntimeOptions { readonly sessions: SessionRepository; readonly model: ModelProvider; - readonly tools: ToolCapability; + readonly tools: ToolSet; /** 可选的工具执行策略;由 Runtime 在组合点包裹真实工具。 */ readonly toolPolicy?: ToolExecutionPolicy; readonly maxSteps: number; @@ -198,7 +198,7 @@ export class AgentRuntime { tools: this.#options.toolPolicy === undefined ? this.#options.tools - : new PolicyControlledTools(this.#options.tools, this.#options.toolPolicy), + : new PolicyControlledToolSet(this.#options.tools, this.#options.toolPolicy), ids: generatedIds, maxSteps: this.#options.maxSteps, settlementSignals: this.#settlementSignals, diff --git a/packages/test-support/src/index.ts b/packages/test-support/src/index.ts index 43f0504..45bccf4 100644 --- a/packages/test-support/src/index.ts +++ b/packages/test-support/src/index.ts @@ -8,4 +8,4 @@ export { type ControlledSessionOperation, TestSettlementSignals, } from './session.js'; -export { fakeTool, ScriptedToolCapability } from './tools.js'; +export { fakeTool, ScriptedToolSet } from './tools.js'; diff --git a/packages/test-support/src/tools.ts b/packages/test-support/src/tools.ts index 1ba72a9..2a815c5 100644 --- a/packages/test-support/src/tools.ts +++ b/packages/test-support/src/tools.ts @@ -1,23 +1,23 @@ import type { JsonValue, ToolCall, - ToolCapability, - ToolDefinition, + ToolSet, + ExecutableTool, ToolExecutionContext, ToolResult, } from '@agent-harness/kernel'; -/** 提供独立于 ToolRegistry 的可编程工具 capability。 */ -export class ScriptedToolCapability implements ToolCapability { - readonly #definitions: ReturnType; - readonly #execute: ToolCapability['execute']; +/** 提供独立于 ToolRegistry 的可编程工具集。 */ +export class ScriptedToolSet implements ToolSet { + readonly #definitions: ReturnType; + readonly #execute: ToolSet['execute']; - constructor(definitions: ReturnType, execute: ToolCapability['execute']) { + constructor(definitions: ReturnType, execute: ToolSet['execute']) { this.#definitions = definitions; this.#execute = execute; } - list(): ReturnType { + list(): ReturnType { return this.#definitions; } @@ -30,7 +30,7 @@ export class ScriptedToolCapability implements ToolCapability { export function fakeTool( name: string, execute: (input: JsonValue, context: ToolExecutionContext) => Promise, -): ToolDefinition & { readonly calls: JsonValue[] } { +): ExecutableTool & { readonly calls: JsonValue[] } { const calls: JsonValue[] = []; return { name, diff --git a/packages/test-support/test/agent.test.ts b/packages/test-support/test/agent.test.ts index 8cf4fa1..c7e8ec3 100644 --- a/packages/test-support/test/agent.test.ts +++ b/packages/test-support/test/agent.test.ts @@ -24,7 +24,7 @@ import { ControlledSessionLog, fakeTool, ScriptedModelProvider, - ScriptedToolCapability, + ScriptedToolSet, SequentialAgentIds, TestSettlementSignals, } from '@agent-harness/test-support'; @@ -851,7 +851,7 @@ describe('Agent', () => { { events: [{ type: 'text.delta', text: 'custom complete' }] }, ]); const executed: string[] = []; - const tools = new ScriptedToolCapability( + const tools = new ScriptedToolSet( [{ name: 'custom', description: '自定义工具', inputSchema: {} }], async (call) => { executed.push(call.name); @@ -1156,7 +1156,7 @@ describe('Agent', () => { }, { events: [{ type: 'text.delta', text: '继续执行' }] }, ]); - const tools = new ScriptedToolCapability( + const tools = new ScriptedToolSet( [{ name: 'wait', description: '等待', inputSchema: {} }], async (_call, context) => new Promise((_resolve, reject) => { @@ -1469,17 +1469,14 @@ describe('Agent', () => { it('工具抛错或取消时报告对应终态,终态 ToolResult 仍报告完成', async () => { const callId = brandId<'ToolCallId'>('call-1') as ToolCallId; - const createAgent = ( - execute: ScriptedToolCapability['execute'], - observed: AgentExecutionEvent[], - ) => + const createAgent = (execute: ScriptedToolSet['execute'], observed: AgentExecutionEvent[]) => new Agent({ model: new ScriptedModelProvider([ { events: [{ type: 'tool.call', call: { id: callId, name: 'lookup', input: {} } }] }, { events: [{ type: 'text.delta', text: 'done' }] }, ]), session: new InMemorySessionLog(sessionId), - tools: new ScriptedToolCapability( + tools: new ScriptedToolSet( [{ name: 'lookup', description: '查询', inputSchema: {} }], execute, ),