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
4 changes: 2 additions & 2 deletions docs/subagent.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ You are a code search specialist. Your job is to find specific code patterns, fu
```yaml
name: explore
description: 只读代码探索
tools: [read_file, search_files, search_code, fetch_url, tool_search]
tools: [read_file, search_files, search_code, fetch_url]
readonly: true
maxSteps: 180
```
Expand All @@ -101,7 +101,7 @@ maxSteps: 180
```yaml
name: plan
description: 只读代码研究和规划
tools: [read_file, search_files, search_code, fetch_url, tool_search, submit_plan, dispatch_agent]
tools: [read_file, search_files, search_code, fetch_url, submit_plan, dispatch_agent]
maxSteps: 180
```

Expand Down
13 changes: 4 additions & 9 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ Coding Code 的工具系统是 Agent 与外部世界交互的核心机制。本
| 工具 | 功能 | 关键参数 |
|---|---|---|
| `todo_write` | 修改代理的任务列表 | `plan: Array<{ step: string, status: 'pending' \| 'in_progress' \| 'completed' }>`(最大条目数有限制) |
| `tool_search` | 发现和加载可用工具 | `query: string`(搜索关键词,至少 1 字符) |

### 子智能体

Expand All @@ -46,13 +45,12 @@ Coding Code 的工具系统是 Agent 与外部世界交互的核心机制。本

## 工具加载机制

工具按加载时机分为三类
工具按加载时机分为两类

- **Core 工具**:始终可用,在启动时注册。包括上述所有内置工具。
- **Deferred 工具**:按需加载,通过 `tool_search` 发现后动态加载。这类工具标记了 `deferred: true`,不会在初始工具列表中暴露给 LLM,只有当 LLM 主动调用 `tool_search` 查询后才会加载。
- **MCP 工具**:从 MCP 服务自动导入和注册。名称空间化为 `serverName:toolName` 格式,避免不同服务间的工具名冲突。

工具解析流程:`createSessionToolResolver()` 合并 builtin + project MCP + tool_search + dispatch_agent,根据 `AgentProfile.tools` 和 `ToolVisibilityPolicy` 过滤后提供给 Agent
Agent 在一次运行开始时将内置工具、项目 MCP 工具和 `dispatch_agent` 注册到 `ToolRegistry`。每轮通过注册表按 `AgentProfile.tools` 和 `ToolVisibilityPolicy` 过滤,并生成 LLM 工具描述与执行查找结果

---

Expand All @@ -64,10 +62,7 @@ Coding Code 的工具系统是 Agent 与外部世界交互的核心机制。本
interface ToolDefinition {
name: string;
description: string;
shortDescription?: string; // 简短描述,用于工具列表展示
deferred?: boolean; // 是否延迟加载
parameters: z.ZodTypeAny; // Zod schema 定义参数
jsonSchema?: Record<string, unknown>; // 可选的 JSON Schema 覆盖
execute: (args: unknown, ctx?: ToolExecCtx) => Effect.Effect<string, AgentError, never>;
}

Expand All @@ -79,6 +74,8 @@ interface ToolExecCtx {
}
```

`execute` 保留在 `ToolDefinition` 中,因为执行器需要通过同一个定义完成参数校验、审批、取消和 hook,再调用工具的实际实现。`ToolExecCtx` 中,`signal` 用于取消;`sessionId` 用于会话级工具状态和子智能体关联,`projectPath` 用于限定工作目录,`turnId` 只用于执行 hook 的轮次追踪。

在 `cli.ts` 中向 `ToolService` 注册新工具,Agent 会自动将其暴露给 LLM。

### 工具可见性策略
Expand All @@ -89,8 +86,6 @@ interface ToolExecCtx {
interface ToolVisibilityPolicy {
allowedTools?: Set<string>; // 允许的工具白名单
allowedMcpServers?: Set<string>; // 允许的 MCP 服务白名单
allowToolSearch?: boolean; // 是否允许 tool_search
allowDeferredTools?: boolean; // 是否允许延迟工具
}
```

Expand Down
1 change: 0 additions & 1 deletion packages/codingcode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"./context/context": "./src/context/context.ts",
"./hooks/registry": "./src/hooks/registry.ts",
"./tools/executor": "./src/tools/executor.ts",
"./tools/tool-search-service": "./src/tools/tool-search-service.ts",
"./mcp/client": "./src/mcp/client.ts",
"./mcp/types": "./src/mcp/types.ts",
"./skills/types": "./src/skills/types.ts",
Expand Down
32 changes: 10 additions & 22 deletions packages/codingcode/src/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Effect, Queue, Stream, Fiber } from 'effect';
import { z } from 'zod';
import type { Message } from '../core/types.js';
import { AgentError } from '../core/error.js';
import { Result } from '../core/result.js';
import type { ToolDescription, ToolDefinition } from '../tools/types.js';
import type { LLMClient } from '../llm/client.js';
import { ToolExecutorService, type ToolLookup } from '../tools/executor.js';
import { SessionService } from '../session/store.js';
Expand All @@ -21,12 +19,12 @@
import { MemoryService } from '../memory/index.js';
import { createLogger } from '@codingcode/infra/logger';
import { resolveSubagentEnabled, resolveAgentDisabled } from '../subagent/registry.js';
import { ProjectRuntimeService, modeToProfile } from '../runtime/project-runtime.js';

Check warning on line 22 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'modeToProfile' is defined but never used. Allowed unused vars must match /^_/u
import { createDispatchAgentTool } from '../tools/domains/subagent/dispatch.js';
import { LLMFactoryService } from '../llm/factory.js';
import { getBuiltinTools } from '../tools/providers.js';
import { registerBuiltinTools } from '../tools/builtin-tools.js';
import { ToolRegistry } from '../tools/registry.js';
import { submitPlanTool } from '../tools/domains/subagent/submit-plan.js';
import { canonicalizeSchema } from '../tools/utils/canonicalize-schema.js';
import { normalizePath } from '../core/path.js';
import { isPlanProfile } from '../plan/index.js';
import type { SessionMode } from '../session/types.js';
Expand Down Expand Up @@ -134,12 +132,12 @@
const hooks = yield* HookService;
const mcp = yield* McpService;
const checkpoint = yield* CheckpointService;
const approval = yield* ApprovalService;

Check warning on line 135 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'approval' is assigned a value but never used. Allowed unused vars must match /^_/u
const skills = yield* SkillService;
const runtime = yield* ProjectRuntimeService;
const todo = yield* TodoService;

Check warning on line 138 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'todo' is assigned a value but never used. Allowed unused vars must match /^_/u
const rules = yield* RulesService;
const context = yield* ContextService;

Check warning on line 140 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'context' is assigned a value but never used. Allowed unused vars must match /^_/u
const memory = yield* MemoryService;
const factory = yield* LLMFactoryService;

Expand Down Expand Up @@ -291,6 +289,12 @@
let stopContinuations = 0;
const effectiveMaxStopContinuations = opts.maxStopContinuations ?? maxStopContinuations;

const registry = new ToolRegistry();
yield* registerBuiltinTools(registry);
registry.register(...(opts.mcpTools ?? []));
if (opts.dispatchTool && resolveSubagentEnabled(projectPath)) registry.register(opts.dispatchTool);
if (isPlanProfile(profile)) registry.register(submitPlanTool);

let messages: Message[] = [];
let submittedPlanTitle: string | null = null;

Expand All @@ -310,25 +314,9 @@
for (let step = 0; step < effectiveMaxSteps; step++) {
yield* q.offer({ _tag: 'Step', step: step + 1, max: effectiveMaxSteps });

const builtinTools = yield* getBuiltinTools();
let allToolDefs: ToolDefinition[] = [...builtinTools, ...(opts.mcpTools ?? [])];
if (opts.dispatchTool && resolveSubagentEnabled(projectPath))
allToolDefs = [...allToolDefs, opts.dispatchTool];
if (isPlanProfile(profile)) allToolDefs = [...allToolDefs, submitPlanTool];

const allowedByPolicy = opts.toolPolicy?.allowedTools;
let filteredDefs = allToolDefs;
if (allowedByPolicy) filteredDefs = filteredDefs.filter((t) => allowedByPolicy.has(t.name));

const tools: ToolDescription[] = filteredDefs.map((t) => ({
name: t.name,
description: t.description,
parameters:
t.jsonSchema ??
(canonicalizeSchema(z.toJSONSchema(t.parameters)) as Record<string, unknown>),
}));

const toolLookup: ToolLookup = (name: string) => filteredDefs.find((t) => t.name === name);
const tools = registry.describe(allowedByPolicy);
const toolLookup: ToolLookup = (name: string) => registry.get(name, allowedByPolicy);
const systemWithCatalog = system;

const stepBeforePayload = { sessionId, step: step + 1 };
Expand Down Expand Up @@ -495,7 +483,7 @@
}
}

const record = yield* session.recordAssistant(state, resp.content, toolCalls!, resp.usage);

Check warning on line 486 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'record' is assigned a value but never used. Allowed unused vars must match /^_/u
const allResults = yield* executor.executeBatch(toolCalls, state.sessionId, {
turnId: state.currentTurnId,
projectPath,
Expand Down
3 changes: 0 additions & 3 deletions packages/codingcode/src/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { ProjectRuntimeService } from './runtime/project-runtime.js';
import { LLMFactoryService } from './llm/factory.js';
import { WorkspaceService } from './core/workspace.js';
import { TodoService } from './agent/todo.js';
import { ToolSearchService } from './tools/tool-search-service.js';
import { SubagentService } from './subagent/registry.js';
import { SubagentRunnerService } from './subagent/runner-service.js';
import { RulesService } from './rules/index.js';
Expand All @@ -23,7 +22,6 @@ import { planModeGateHook } from './plan/index.js';

export const WorkspaceLayer = WorkspaceService.Default;
export const TodoLayer = TodoService.Default;
export const ToolSearchLayer = ToolSearchService.Default;
export const SubagentLayer = SubagentService.Default;
export const RulesLayer = RulesService.Default;
export const SessionLayer = SessionService.Default;
Expand Down Expand Up @@ -105,7 +103,6 @@ export const AppLayer = Layer.mergeAll(
LLMFactoryLayer,
WorkspaceLayer,
TodoLayer,
ToolSearchLayer,
SubagentLayer,
RulesLayer,
MemoryLayer,
Expand Down
11 changes: 5 additions & 6 deletions packages/codingcode/src/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Effect } from 'effect';
import { z } from 'zod';
import { resolveMcpConfig, resolveMcpDisabled } from './config.js';
import { McpClient, McpError } from './client.js';
import { McpClient } from './client.js';
import type { McpServerConfig, McpStatus } from './types.js';
import type { ToolDefinition, ToolExecCtx } from '../tools/types.js';
import type { ToolDefinition } from '../tools/types.js';
import { createLogger } from '@codingcode/infra/logger';
import { AgentError } from '../core/error.js';

Expand Down Expand Up @@ -31,7 +31,7 @@ type ProjectPath = string;
type ServerName = string;

export class McpService extends Effect.Service<McpService>()('Mcp', {
effect: Effect.gen(function* () {
effect: Effect.sync(() => {
const clientsByProject = new Map<ProjectPath, Map<ServerName, ServerEntry>>();
const leasesBySession = new Map<string, Set<LeaseEntry>>();
const disabledMcpByProject = new Map<ProjectPath, Set<ServerName>>();
Expand Down Expand Up @@ -334,9 +334,8 @@ function mcpToolToDefinition(
return {
name: `${serverName}:${mcpTool.name}`,
description: `[MCP:${serverName}] ${mcpTool.description || mcpTool.name}`,
parameters: z.object({}).passthrough(),
jsonSchema: mcpTool.inputSchema,
execute: (args: unknown, _ctx?: ToolExecCtx) => {
parameters: z.fromJSONSchema(mcpTool.inputSchema),
execute: (args) => {
if (isDisabledFn())
return Effect.fail(
new AgentError('TOOL_EXECUTION_FAILED', `MCP server '${serverName}' is disabled`)
Expand Down
2 changes: 0 additions & 2 deletions packages/codingcode/src/runtime/project-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ export class ProjectRuntimeService extends Effect.Service<ProjectRuntimeService>
getToolPolicy: (profile: AgentProfile | undefined): ToolVisibilityPolicy => ({
allowedTools: profile?.tools ? new Set(profile.tools) : undefined,
allowedMcpServers: profile?.mcpServers ? new Set(profile.mcpServers) : undefined,
allowToolSearch: true,
allowDeferredTools: false,
}),

setSessionProfile: (
Expand Down
4 changes: 1 addition & 3 deletions packages/codingcode/src/subagent/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export const EXPLORE_PROFILE: AgentProfile = {
- When referencing code, use the format \`file_path:line_number\`.
- Be thorough but concise in your findings. Focus on what the user asked for — structure your answer around the question, not around the files you read.
- If you cannot find the answer, say so clearly rather than guessing.`,
tools: ['read_file', 'search_files', 'search_code', 'fetch_url', 'tool_search'],
tools: ['read_file', 'search_files', 'search_code', 'fetch_url'],
readonly: true,
maxSteps: 180,
};
Expand Down Expand Up @@ -208,7 +208,6 @@ Never re-call submit_plan on your own initiative. Never treat an implement messa
'search_files',
'search_code',
'fetch_url',
'tool_search',
'submit_plan',
'dispatch_agent',
],
Expand All @@ -230,7 +229,6 @@ export const BUILD_PROFILE: AgentProfile = {
'fetch_url',
'web_search',
'todo_write',
'tool_search',
'dispatch_agent',
],
maxSteps: 180,
Expand Down
33 changes: 33 additions & 0 deletions packages/codingcode/src/tools/builtin-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Effect } from 'effect';
import type { ToolDefinition } from './types.js';
import { ToolRegistry } from './registry.js';
import { readFileTool } from './domains/fs/read.js';
import { writeFileTool } from './domains/fs/write.js';
import { editFileTool } from './domains/fs/edit.js';
import { bashTool } from './domains/bash/exec.js';
import { searchTool } from './domains/fs/grep.js';
import { globTool } from './domains/fs/glob.js';
import { webFetchTool } from './domains/web/fetch.js';
import { webSearchTool } from './domains/web/search.js';
import { createTodoWriteTool } from './domains/self/todo-write.js';
import { TodoService } from '../agent/todo.js';

const STATELESS_BUILTIN_TOOLS: ToolDefinition[] = [
readFileTool,
writeFileTool,
editFileTool,
bashTool,
searchTool,
globTool,
webFetchTool,
webSearchTool,
];

export function registerBuiltinTools(
registry: ToolRegistry
): Effect.Effect<void, never, TodoService> {
return Effect.gen(function* () {
const todoTool = yield* createTodoWriteTool();
registry.register(...STATELESS_BUILTIN_TOOLS, todoTool);
});
}
4 changes: 2 additions & 2 deletions packages/codingcode/src/tools/domains/bash/exec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from 'zod';
import { spawn } from 'child_process';
import { Effect } from 'effect';
import type { ToolDefinition, ToolExecCtx } from '../../types.js';
import type { ToolDefinition } from '../../types.js';

import { AgentError } from '../../../core/error.js';

Expand All @@ -14,7 +14,7 @@ export const bashTool: ToolDefinition = {
cwd: z.string().optional().describe('Working directory (defaults to project root)'),
timeout_ms: z.number().int().default(30000).describe('Timeout in milliseconds'),
}),
execute: (args: unknown, ctx?: ToolExecCtx) => {
execute: (args, ctx) => {
const { command, cwd, timeout_ms } = args as any;
const workDir = cwd || ctx?.projectPath || process.cwd();
return Effect.async<string, AgentError>((resume) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/codingcode/src/tools/domains/fs/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { readFile, writeFile } from 'fs/promises';
import { resolve } from 'path';
import { Effect } from 'effect';
import { AgentError } from '../../../core/error.js';
import type { ToolDefinition, ToolExecCtx } from '../../types.js';
import type { ToolDefinition } from '../../types.js';

export const editFileTool: ToolDefinition = {
name: 'edit_file',
Expand All @@ -17,7 +17,7 @@ export const editFileTool: ToolDefinition = {
.describe('Exact text to replace — must match exactly one location in the file'),
new_string: z.string().describe('Text to replace it with'),
}),
execute: (args: unknown, ctx?: ToolExecCtx) =>
execute: (args, ctx) =>
Effect.gen(function* () {
const { path, old_string, new_string } = args as {
path: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/codingcode/src/tools/domains/fs/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { globby } from 'globby';
import { relative, resolve } from 'path';
import { Effect } from 'effect';
import { AgentError } from '../../../core/error.js';
import type { ToolDefinition, ToolExecCtx } from '../../types.js';
import type { ToolDefinition } from '../../types.js';

export const globTool: ToolDefinition = {
name: 'search_files',
Expand All @@ -23,7 +23,7 @@ export const globTool: ToolDefinition = {
.default(50)
.describe('Maximum number of file paths to return'),
}),
execute: (args: unknown, ctx?: ToolExecCtx) =>
execute: (args, ctx) =>
Effect.gen(function* () {
const { pattern, path, max_results } = args as {
pattern: string;
Expand Down
6 changes: 3 additions & 3 deletions packages/codingcode/src/tools/domains/fs/grep.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { z } from 'zod';
import { globby } from 'globby';
import { readFile } from 'fs/promises';
import { relative, resolve } from 'path';
import { relative } from 'path';
import { Effect } from 'effect';
import { AgentError } from '../../../core/error.js';
import type { ToolDefinition, ToolExecCtx } from '../../types.js';
import type { ToolDefinition } from '../../types.js';

export const searchTool: ToolDefinition = {
name: 'search_code',
Expand All @@ -24,7 +24,7 @@ export const searchTool: ToolDefinition = {
.default(30)
.describe('Maximum number of matches to return'),
}),
execute: (args: unknown, ctx?: ToolExecCtx) =>
execute: (args, ctx) =>
Effect.gen(function* () {
const { pattern, glob, max_results } = args as any;
const base = ctx?.projectPath ?? process.cwd();
Expand Down
4 changes: 2 additions & 2 deletions packages/codingcode/src/tools/domains/fs/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { readFile } from 'fs/promises';
import { resolve } from 'path';
import { Effect } from 'effect';
import { AgentError } from '../../../core/error.js';
import type { ToolDefinition, ToolExecCtx } from '../../types.js';
import type { ToolDefinition } from '../../types.js';

export const readFileTool: ToolDefinition = {
name: 'read_file',
Expand All @@ -19,7 +19,7 @@ export const readFileTool: ToolDefinition = {
.default(200)
.describe('Maximum number of lines to read'),
}),
execute: (args: unknown, ctx?: ToolExecCtx) =>
execute: (args, ctx) =>
Effect.gen(function* () {
const { path, offset, limit } = args as any;
const filePath = resolve(ctx?.projectPath ?? process.cwd(), path);
Expand Down
4 changes: 2 additions & 2 deletions packages/codingcode/src/tools/domains/fs/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { writeFile, mkdir } from 'fs/promises';
import { dirname, relative, resolve } from 'path';
import { Effect } from 'effect';
import { AgentError } from '../../../core/error.js';
import type { ToolDefinition, ToolExecCtx } from '../../types.js';
import type { ToolDefinition } from '../../types.js';

export const writeFileTool: ToolDefinition = {
name: 'write_file',
Expand All @@ -13,7 +13,7 @@ export const writeFileTool: ToolDefinition = {
path: z.string().describe('Path to the file'),
content: z.string().describe('Content to write'),
}),
execute: (args: unknown, ctx?: ToolExecCtx) =>
execute: (args, ctx) =>
Effect.gen(function* () {
const { path, content } = args as any;
const base = ctx?.projectPath ?? process.cwd();
Expand Down
3 changes: 1 addition & 2 deletions packages/codingcode/src/tools/domains/self/todo-write.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from 'zod';
import { Effect } from 'effect';
import { AgentError } from '../../../core/error.js';
import type { ToolDefinition, ToolExecCtx } from '../../types.js';
import type { ToolDefinition } from '../../types.js';
import {
TodoService,
countByStatus,
Expand Down Expand Up @@ -29,7 +29,6 @@ export function createTodoWriteTool(): Effect.Effect<ToolDefinition, never, Todo
name: 'todo_write',
description:
'Replace the current task list. Use for multi-step work to track plan and progress. Pass the full updated plan; previous list is replaced entirely.',
shortDescription: 'Maintain task list for multi-step work',
parameters: todoSchema,
execute: (args, ctx) => {
const sessionId = ctx?.sessionId;
Expand Down
Loading
Loading