Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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、文件系统或进程。

## 生命周期

Expand Down
35 changes: 35 additions & 0 deletions docs/decisions/0054-tool-contract-and-set.md
Original file line number Diff line number Diff line change
@@ -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`、版本或执行后端框架:会增加无当前消费者的抽象。
4 changes: 2 additions & 2 deletions packages/adapters/local/src/shell-tool.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 必须是绝对路径');
}
Expand Down
4 changes: 2 additions & 2 deletions packages/adapters/local/src/workspace-tools.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
8 changes: 2 additions & 6 deletions packages/adapters/mcp/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<string, McpToolDescriptor> | undefined;
#startPromise: Promise<void> | undefined;
Expand Down
4 changes: 2 additions & 2 deletions packages/adapters/memory-gateway/src/writer-tool.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/adapters/skill-tool/src/skill-loader.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
type SessionLog,
SettlementPersistenceError,
type SettlementSignalFactory,
type ToolCapability,
type ToolSet,
type ToolResult,
type TurnId,
type UserMessage,
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 7 additions & 7 deletions packages/hosts/cli/src/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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 [];
}
Expand Down
4 changes: 2 additions & 2 deletions packages/kernel/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions packages/kernel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
35 changes: 24 additions & 11 deletions packages/kernel/src/tools/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface ToolCall {
readonly input: JsonValue;
}

/** 暴露给模型的工具定义。 */
/** 暴露给模型的工具定义。它是工具契约的只读投影,不包含执行或输入校验行为。 */
export interface ModelToolDefinition {
readonly name: string;
readonly description: string;
Expand Down Expand Up @@ -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<Record<string, unknown>>;
parseInput(input: JsonValue): JsonValue;
}

/** 本地工具定义 —— 在工具规格上附加进程内执行函数。 */
export interface ToolDefinition extends ToolSpecification {
/** 直接附带执行器的工具。执行器可以访问任何由组合方显式注入的外部依赖。 */
export interface ExecutableTool extends ToolContract {
execute(input: JsonValue, context: ToolExecutionContext): Promise<JsonValue>;
}

/** 工具目录。 */
export interface ToolCatalog {
/** 一次 Agent 运行中固定的工具目录与执行路由。 */
export interface ToolSet {
list(): readonly ModelToolDefinition[];
execute(call: ToolCall, context: ToolExecutionContext): Promise<ToolResult>;
}

/** 工具能力 —— 工具的执行能力和目录。 */
export interface ToolCapability extends ToolCatalog {
execute(call: ToolCall, context: ToolExecutionContext): Promise<ToolResult>;
/** 从工具调用契约构造模型可见的只读定义。 */
export function toModelToolDefinition(tool: ToolContract): ModelToolDefinition {
return {
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
};
}

/** 工具授权决策。 */
Expand Down Expand Up @@ -132,7 +145,7 @@ export interface SandboxDispatcher {
}

/** 沙盒工具注册选项。 */
export interface SandboxToolRegistryOptions {
export interface SandboxToolSetOptions {
readonly workspace?: string;
readonly limits?: SandboxResourceLimits;
}
Loading
Loading