diff --git a/AGENTS.md b/AGENTS.md index 0488b1c2..c48964d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,92 @@ -不允许假设“这是未来需要扩展的”,所以现在就不做,应该贴合用户的实际要求 -不允许总是有阶段性计划,分阶段完成很容易导致过程产生一堆没用的死代码 -不许兼容、兜底旧代码 -每次执行完以后都要补充测试文件确保实际行为与预期相符 -修改过程中发现错误,如果是本次范围就修改(包括测试),否则要在最后指出 -在用户的最新的一条消息除非有显式命令(执行方案、修改代码等)要求修改代码,否则绝对不改代码,之前要求修改的指令全部不算数,别再根据之前的上下文或者当前不确定的指令猜是不是要直接修改代码了 -设计方案后,须深入解释每一步的理由 -仅允许使用简短注释 \ No newline at end of file +## 项目定位 + +Coding Code 是 AI 编程助手。 + +## 模块划分 + +包级(pnpm workspace): + +| 包 | 目录 | 职责 | +|---|---|---| +| `@codingcode/core` | `packages/codingcode` | 核心引擎:agent loop 与全部编排能力 | +| `@codingcode/infra` | `packages/infra` | 基础设施:配置加载、日志、禁用项存储 | +| `@codingcode/tui` | `packages/tui` | 终端界面(Ink + React) | +| `@codingcode/desktop` | `packages/desktop` | 桌面端(Electron + React) | + +`packages/sdk`、`packages/web` 目前只有空 `src/`,尚无实现。 + +`@codingcode/core` 内部特性目录: + +- `agent`:本项目核心,手写 ReAct loop 与编排;不持有 Session、不感知传输协议 +- `tools`:工具系统,`domains/` 下分 fs / bash / web / self / subagent 五个域 +- `llm`:模型调用与 provider 适配 +- `mcp`:Model Context Protocol 集成 +- `context`:上下文预算与压缩 +- `memory`:跨会话长期记忆 +- `checkpoint`:Shadow Git 变更跟踪与回滚 +- `hooks`:可插拔钩子点 +- `subagent`:子智能体委派 +- `skills`:Markdown 技能包装载 +- `approval`:审批决策链 +- `session`:会话持久化 +- `scheduler`:定时调度 +- `todo`:任务清单 +- `rules`:全局 / 项目级规则装载 +- `workspace`:工作区信息 +- `server`:HTTP / SSE 入口 +- `client`:HTTP 客户端(`AgentClient` 的实现) +- `direct`:进程内直连端口,免 HTTP 的 runtime / sessions / settings / models 接口 +- `core`:通用件(`error` / `result` / `path`),不指向任何功能模块 +- `contracts`:跨领域共享契约 +- `layer.ts`:组合根,全量装配 + +## 架构要求 + +**依赖倒置**:所有非叶子模块利用 `port.ts`(宽契约;agent 自持的装配端口也在 `agent/port.ts`)声明自己需要的接口和类型定义,使调用者不需要依赖实现方;只允许依赖下层模块。 + +**分层与允许依赖**: + +| 层 | 落点 | 允许依赖 | +|---|---|---| +| L0 通用件 | `core/` | node 内置 + 同目录 | +| L1 共享契约 | `contracts/` | `core/` + 同目录 + 第三方(type-only) | +| L1' 端口契约 | 各 `xxx/port.ts`(含 `agent/port.ts` 的装配端口) | `core/` + `contracts/` | +| L2 实现 | `tools/`、`hooks/`、`session/`、`approval/`、`llm/`、`mcp/`、`context/`、`workspace/` … | L0 + L1 | +| L3 组合根 | `layer.ts`、`agent/tool-env.ts` | 全部 | + +**架构边界硬规则**(由 `packages/codingcode/test/architecture/boundaries.test.ts` 静态断言,共 29 项): + +- **R1** 契约不得 import 实现:`contracts/` 与 `**/port.ts` 的相对 import 只能落在 `core/`、`contracts/` 或同目录 +- **R2** 实现不得依赖消费者模块:agent 自持的装配端口 `ToolEnvPort` 只在 `agent/` 内部出现 +- **R3** `core/` 零内部依赖:不引用 `core/` 之外的任何 src 模块 +- **R4** 一个概念只允许一处类型定义,canonical 落点为 `contracts/` +- **准入**:`core/` 的 import 只能是 node 内置与同目录;`contracts/` 只引用 `core/`、同目录与第三方 +- **可解析**:`src/**` 的每条相对 import 都必须能在仓库内找到落点 + +**类型落点判据**(先判归属,再判引用面): + +- 判据一 —— 有无领域归属:不指向任何功能模块的(错误基类、结果容器、路径运算)→ `core/`;指向某功能模块的 → 判据二 +- 判据二 —— 引用面,**只作用于领域件**:仅 1 个 src 领域引用 → 回该领域**已有**的归属文件;只出现在某调用方接口签名里 → 内联进调用方;≥2 个 src 领域,或 ≥1 个跨包 → `contracts/` + +**机制形状例外**:`z.ZodTypeAny`、SDK client、Effect 的 R 通道类型必须留在叶子模块,不得进 `contracts/`。契约只暴露窄的纯数据描述——MCP 契约返回 `McpToolSpec`,`z.fromJSONSchema` 的转换由拥有机制的 `tools/catalog.ts` 自己做。 + +## 开发规则 + +- 禁止用户当前轮未明确要求就主动修改仓库中任何内容,包括源代码、配置文件、文档等 +- 禁止用户当前轮未明确要求就主动进行 reset、commit、push 等相关会影响 git 历史或者当前仓库代码的操作,仅用户显式要求进行某类操作才能进行;仅允许 `git diff`、`git log` 等无副作用的操作可以自主进行 +- 禁止未在用户指示下补充测试,当开发任务完成后,给用户报告完成程度,由用户决定针对哪些部分写测试 +- 禁止将工具执行细节泄漏到 agent 编排层及其他模块,agent 只依赖端口契约,不得 import 工具实现 +- 禁止将传输协议细节(HTTP / SSE)泄漏到 agent 核心及其他模块,agent 不得依赖 `server/`、`client/`、`direct/` +- 不允许假设“这是未来需要扩展的”,所以现在就不做,应该贴合用户的实际要求 +- 不允许总是有阶段性计划,分阶段完成很容易导致过程产生一堆没用的死代码 +- 不许兼容、兜底旧代码 +- 修改过程中发现错误,如果是本次范围就修改(包括测试),否则要在最后指出 +- 仅允许使用简短注释 + +## 其他规则 + +- 用户要求回答问题时,必须清晰回答每一点问题,不得遗漏 +- 禁止编造任何证据、方案或者代码现状等内容 +- 关于 TypeScript 规范,参考 TypeScript 官方文档,不得使用非官方推荐的方案 +- 设计方案后,须深入解释每一步的理由 +- 关于方案设计,禁止自己编造,只允许查找社区中的成熟实现,且输出时必须贴出相应来源,来源必须真实,保证用户能够打开链接、经得起二次验证 diff --git a/CLAUDE.md b/CLAUDE.md index 0488b1c2..b6af698d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,8 +1,92 @@ -不允许假设“这是未来需要扩展的”,所以现在就不做,应该贴合用户的实际要求 -不允许总是有阶段性计划,分阶段完成很容易导致过程产生一堆没用的死代码 -不许兼容、兜底旧代码 -每次执行完以后都要补充测试文件确保实际行为与预期相符 -修改过程中发现错误,如果是本次范围就修改(包括测试),否则要在最后指出 -在用户的最新的一条消息除非有显式命令(执行方案、修改代码等)要求修改代码,否则绝对不改代码,之前要求修改的指令全部不算数,别再根据之前的上下文或者当前不确定的指令猜是不是要直接修改代码了 -设计方案后,须深入解释每一步的理由 -仅允许使用简短注释 \ No newline at end of file +## 项目定位 + +Coding Code 是 AI 编程助手。 + +## 模块划分 + +包级(pnpm workspace): + +| 包 | 目录 | 职责 | +|---|---|---| +| `@codingcode/core` | `packages/codingcode` | 核心引擎:agent loop 与全部编排能力 | +| `@codingcode/infra` | `packages/infra` | 基础设施:配置加载、日志、禁用项存储 | +| `@codingcode/tui` | `packages/tui` | 终端界面(Ink + React) | +| `@codingcode/desktop` | `packages/desktop` | 桌面端(Electron + React) | + +`packages/sdk`、`packages/web` 目前只有空 `src/`,尚无实现。 + +`@codingcode/core` 内部特性目录: + +- `agent`:本项目核心,手写 ReAct loop 与编排;不持有 Session、不感知传输协议 +- `tools`:工具系统,`domains/` 下分 fs / bash / web / self / subagent 五个域 +- `llm`:模型调用与 provider 适配 +- `mcp`:Model Context Protocol 集成 +- `context`:上下文预算与压缩 +- `memory`:跨会话长期记忆 +- `checkpoint`:Shadow Git 变更跟踪与回滚 +- `hooks`:可插拔钩子点 +- `subagent`:子智能体委派 +- `skills`:Markdown 技能包装载 +- `approval`:审批决策链 +- `session`:会话持久化 +- `scheduler`:定时调度 +- `todo`:任务清单 +- `rules`:全局 / 项目级规则装载 +- `workspace`:工作区信息 +- `server`:HTTP / SSE 入口 +- `client`:HTTP 客户端(`AgentClient` 的实现) +- `direct`:进程内直连端口,免 HTTP 的 runtime / sessions / settings / models 接口 +- `core`:通用件(`error` / `result` / `path`),不指向任何功能模块 +- `contracts`:跨领域共享契约 +- `layer.ts`:组合根,全量装配 + +## 架构要求 + +**依赖倒置**:所有非叶子模块利用 `deps.ts`(消费者侧窄端口)或 `port.ts`(叶子侧宽契约)声明自己需要的接口和类型定义,使调用者不需要依赖实现方;只允许依赖下层模块。 + +**分层与允许依赖**: + +| 层 | 落点 | 允许依赖 | +|---|---|---| +| L0 通用件 | `core/` | node 内置 + 同目录 | +| L1 共享契约 | `contracts/` | `core/` + 同目录 + 第三方(type-only) | +| L1' 端口契约 | `agent/deps.ts`、各 `xxx/port.ts` | `core/` + `contracts/` | +| L2 实现 | `tools/`、`hooks/`、`session/`、`approval/`、`llm/`、`mcp/`、`context/`、`workspace/` … | L0 + L1 | +| L3 组合根 | `layer.ts`、`agent/tool-catalog.ts`、`agent/tool-env.ts` | 全部 | + +**架构边界硬规则**(由 `packages/codingcode/test/architecture/boundaries.test.ts` 静态断言,共 28 项): + +- **R1** 契约不得 import 实现:`contracts/` 与 `**/port.ts` 的相对 import 只能落在 `core/`、`contracts/` 或同目录 +- **R2** 实现不得依赖消费者模块:窄端口 `agent/deps.ts` 只对 `agent/agent.ts`、`agent/tool-catalog.ts`、`agent/tool-env.ts`、`layer.ts` 可见 +- **R3** `core/` 零内部依赖:不引用 `core/` 之外的任何 src 模块 +- **R4** 一个概念只允许一处类型定义,canonical 落点为 `contracts/` +- **准入**:`core/` 的 import 只能是 node 内置与同目录;`contracts/` 只引用 `core/`、同目录与第三方 +- **可解析**:`src/**` 的每条相对 import 都必须能在仓库内找到落点 + +**类型落点判据**(先判归属,再判引用面): + +- 判据一 —— 有无领域归属:不指向任何功能模块的(错误基类、结果容器、路径运算)→ `core/`;指向某功能模块的 → 判据二 +- 判据二 —— 引用面,**只作用于领域件**:仅 1 个 src 领域引用 → 回该领域**已有**的归属文件;只出现在某调用方接口签名里 → 内联进调用方;≥2 个 src 领域,或 ≥1 个跨包 → `contracts/` + +**机制形状例外**:`z.ZodTypeAny`、SDK client、Effect 的 R 通道类型必须留在叶子模块,不得进 `contracts/`。契约只暴露窄的纯数据描述——MCP 契约返回 `McpToolSpec`,`z.fromJSONSchema` 的转换由拥有机制的 `tools/catalog.ts` 自己做。 + +## 开发规则 + +- 禁止用户当前轮未明确要求就主动修改仓库中任何内容,包括源代码、配置文件、文档等 +- 禁止用户当前轮未明确要求就主动进行 reset、commit、push 等相关会影响 git 历史或者当前仓库代码的操作,仅用户显式要求进行某类操作才能进行;仅允许 `git diff`、`git log` 等无副作用的操作可以自主进行 +- 禁止未在用户指示下补充测试,当开发任务完成后,给用户报告完成程度,由用户决定针对哪些部分写测试 +- 禁止将工具执行细节泄漏到 agent 编排层及其他模块,agent 只依赖端口契约,不得 import 工具实现 +- 禁止将传输协议细节(HTTP / SSE)泄漏到 agent 核心及其他模块,agent 不得依赖 `server/`、`client/`、`direct/` +- 不允许假设“这是未来需要扩展的”,所以现在就不做,应该贴合用户的实际要求 +- 不允许总是有阶段性计划,分阶段完成很容易导致过程产生一堆没用的死代码 +- 不许兼容、兜底旧代码 +- 修改过程中发现错误,如果是本次范围就修改(包括测试),否则要在最后指出 +- 仅允许使用简短注释 + +## 其他规则 + +- 用户要求回答问题时,必须清晰回答每一点问题,不得遗漏 +- 禁止编造任何证据、方案或者代码现状等内容 +- 关于 TypeScript 规范,参考 TypeScript 官方文档,不得使用非官方推荐的方案 +- 设计方案后,须深入解释每一步的理由 +- 关于方案设计,禁止自己编造,只允许查找社区中的成熟实现,且输出时必须贴出相应来源,来源必须真实,保证用户能够打开链接、经得起二次验证 diff --git a/packages/codingcode/package.json b/packages/codingcode/package.json index 61c3045d..47a02180 100644 --- a/packages/codingcode/package.json +++ b/packages/codingcode/package.json @@ -16,14 +16,20 @@ "./direct/models": "./src/direct/models.ts", "./approval/types": "./src/approval/types.ts", "./agent/profile": "./src/agent/profile.ts", - "./core/frame": "./src/core/frame.ts", + "./contracts/frame": "./src/contracts/frame.ts", "./core/error": "./src/core/error.ts", - "./core/types": "./src/core/types.ts", + "./contracts/error": "./src/contracts/error.ts", + "./contracts/types": "./src/contracts/types.ts", + "./contracts/permission": "./src/contracts/permission.ts", + "./contracts/hooks": "./src/contracts/hooks.ts", + "./contracts/session": "./src/contracts/session.ts", + "./contracts/provider": "./src/contracts/provider.ts", + "./contracts/mcp": "./src/contracts/mcp.ts", + "./contracts/skill": "./src/contracts/skill.ts", + "./contracts/tool": "./src/contracts/tool.ts", "./checkpoint/types": "./src/checkpoint/types.ts", "./session/port": "./src/session/port.ts", - "./mcp/types": "./src/mcp/types.ts", - "./hooks/types": "./src/hooks/types.ts", - "./llm/client": "./src/llm/client.ts" + "./hooks/types": "./src/hooks/types.ts" }, "dependencies": { "@ai-sdk/deepseek": "^2.0.35", diff --git a/packages/codingcode/src/agent/agent.ts b/packages/codingcode/src/agent/agent.ts index 906ded72..050471a1 100644 --- a/packages/codingcode/src/agent/agent.ts +++ b/packages/codingcode/src/agent/agent.ts @@ -1,24 +1,37 @@ import { Effect, Either, Queue, Stream, Fiber, Layer } from 'effect'; import { AgentError } from '../core/error.js'; import { Result } from '../core/result.js'; -import { AgentService } from './port.js'; -import type { RunTurnOptions } from './port.js'; -import { - SessionPort, ToolExecutorPort, CheckpointPort, HookPort, - ApprovalPort, SkillPort, McpPort, ContextPort, MemoryPort, - LlmPort, RulesPort, TodoPort, ToolEnvPort, ToolCatalogPort, -} from './deps.js'; -import type { ToolEnv, ToolCatalog } from './deps.js'; +import { AgentService, ToolEnvPort } from './port.js'; +import type { RunTurnOptions, ToolEnv } from './port.js'; +import { ApprovalService } from '../approval/port.js'; +import { CheckpointService } from '../checkpoint/port.js'; +import { ContextService } from '../context/port.js'; +import { HookService } from '../hooks/port.js'; +import { LLMFactoryService } from '../llm/port.js'; +import { McpService } from '../mcp/port.js'; +import { MemoryService } from '../memory/port.js'; +import { RulesService } from '../rules/port.js'; +import { SessionService } from '../session/port.js'; +import { SkillService } from '../skills/port.js'; +import { TodoService } from '../todo/port.js'; +import { ToolExecutorService } from '../tools/port.js'; import { buildSystemPrompt } from './prompt.js'; -import type { FrameBody, FrameError, ResponseMeta, Transition, ToolOutcome } from '../core/frame.js'; -import { isTurnEnd } from '../core/frame.js'; -import type { ToolCall } from '../core/types.js'; +import type { FrameBody, FrameError, ResponseMeta, ToolOutcome, Transition } from '../contracts/frame.js'; +import { isTurnEnd } from '../contracts/frame.js'; +import type { ToolCatalog, ToolResult } from '../contracts/tool.js'; +import type { ToolCall } from '../contracts/types.js'; import { loadConfig } from '@codingcode/infra/config'; import { createLogger } from '@codingcode/infra/logger'; import { normalizePath, computePaths } from '../core/path.js'; import { resolveProfile, getToolNames } from './profile.js'; + +function toolOutcomeOf(result: ToolResult): ToolOutcome { + return result.status === 'denied' + ? { status: 'denied', reason: result.reason } + : { status: result.status, output: result.output }; +} import type { AgentProfile } from './profile.js'; -import type { PermissionMode } from '../approval/types.js'; +import type { PermissionMode } from '../contracts/permission.js'; const logger = createLogger(); @@ -27,20 +40,19 @@ function toFrameError(e: AgentError): FrameError { } export const AgentLayer = Layer.effect(AgentService, Effect.gen(function* () { - const session = yield* SessionPort; - const executor = yield* ToolExecutorPort; - const checkpoint = yield* CheckpointPort; - const hooks = yield* HookPort; - const approval = yield* ApprovalPort; - const skills = yield* SkillPort; - const mcp = yield* McpPort; - const context = yield* ContextPort; - const memory = yield* MemoryPort; - const llmFactory = yield* LlmPort; - const rules = yield* RulesPort; - const todo = yield* TodoPort; + const session = yield* SessionService; + const executor = yield* ToolExecutorService; + const checkpoint = yield* CheckpointService; + const hooks = yield* HookService; + const approval = yield* ApprovalService; + const skills = yield* SkillService; + const mcp = yield* McpService; + const context = yield* ContextService; + const memory = yield* MemoryService; + const llmFactory = yield* LLMFactoryService; + const rules = yield* RulesService; + const todo = yield* TodoService; const toolEnvPort = yield* ToolEnvPort; - const toolCatalog = yield* ToolCatalogPort; const cfg = loadConfig(); const maxSteps = cfg.maxSteps ?? 250; const maxStopContinuations = cfg.maxStopContinuations ?? 3; @@ -50,6 +62,7 @@ export const AgentLayer = Layer.effect(AgentService, Effect.gen(function* () { const normalizedCwd = normalizePath(opts.cwd); rules.evictProjectRules(normalizedCwd); + yield* hooks.reloadUserHooks(normalizedCwd).pipe(Effect.catchAll(() => Effect.void)); yield* hooks.emit('agent.turn.start', { sessionId: '' }).pipe(Effect.catchAll(() => Effect.void)); yield* mcp.syncConnections(normalizedCwd).pipe(Effect.catchAll(() => Effect.void)); @@ -86,19 +99,17 @@ export const AgentLayer = Layer.effect(AgentService, Effect.gen(function* () { const profile: AgentProfile | undefined = profileName ? resolveProfile(profileName) : undefined; - // get MCP tools - const mcpTools = mcp.listProjectMcpTools(normalizedCwd); - - const catalog = toolCatalog.register(getToolNames(profile), mcpTools); + const mcpTools = yield* mcp.listProjectMcpTools(normalizedCwd); + const catalog = yield* executor.prepare(getToolNames(profile), mcpTools); const toolEnv = yield* toolEnvPort.getToolEnv(); // record user (increments turn) + extract skill const [, actualInput] = yield* skills.extractSkill(state.cwd, input); - const userEvent = yield* session.recordUser(state, actualInput); + const turnId = (yield* session.recordUser(state, actualInput)).turnId; // checkpoint baseline - yield* checkpoint.snapshotBaseline(state.cwd, sessionId, userEvent.turnId); + yield* checkpoint.snapshotBaseline(state.cwd, sessionId, turnId); // get rules text const rulesText = rules.getAllRules(state.cwd); @@ -124,26 +135,14 @@ export const AgentLayer = Layer.effect(AgentService, Effect.gen(function* () { }): AsyncGenerator { const q = Effect.runSync(Queue.unbounded()); - const program: any = Effect.scoped( + // agentLoopInternal 只经闭包引用服务,不消费任何 Tag;工具执行所需的服务由 toolEnv 注入 + const program = Effect.scoped( Effect.gen(function* () { yield* Effect.addFinalizer(() => Effect.sync(() => { hooks.disposeSession(opts.sid); }) ); return yield* agentLoopInternal(opts, q); - }).pipe( - Effect.provideService(SessionPort, session), - Effect.provideService(ToolExecutorPort, executor), - Effect.provideService(CheckpointPort, checkpoint), - Effect.provideService(HookPort, hooks), - Effect.provideService(ApprovalPort, approval), - Effect.provideService(SkillPort, skills), - Effect.provideService(McpPort, mcp), - Effect.provideService(ContextPort, context), - Effect.provideService(MemoryPort, memory), - Effect.provideService(LlmPort, llmFactory), - Effect.provideService(RulesPort, rules), - Effect.provideService(TodoPort, todo), - ) + }) ); return (async function* () { @@ -170,7 +169,7 @@ export const AgentLayer = Layer.effect(AgentService, Effect.gen(function* () { catalog: ToolCatalog; rulesText: string; sid: string; projectPath: string; permissionMode: PermissionMode; - }, q: Queue.Queue): any { + }, q: Queue.Queue): Effect.Effect, AgentError> { const { state, llm, profile, abortSignal, catalog, rulesText, sid, projectPath, permissionMode } = opts; const { tools, lookup: toolLookup } = catalog; @@ -313,7 +312,7 @@ export const AgentLayer = Layer.effect(AgentService, Effect.gen(function* () { profile: profile?.name, }); if (decision.type === 'deny') { - deniedResults.push({ type: 'denied', id: tc.id, name: tc.name, reason: decision.reason }); + deniedResults.push({ status: 'denied', id: tc.id, name: tc.name, reason: decision.reason }); } else { approvedCalls.push(tc); } @@ -329,14 +328,10 @@ export const AgentLayer = Layer.effect(AgentService, Effect.gen(function* () { let todoPrinted = false; for (const r of allResults) { - const resultOut = r.type === 'denied' ? '' : r.output; + const resultOut = r.status === 'denied' ? '' : r.output; yield* session.recordToolResult(state, r.name, r.id, resultOut); - const outcome: ToolOutcome = r.type === 'denied' - ? { status: 'denied', reason: r.reason } - : r.type === 'ok' - ? { status: 'ok', output: resultOut } - : { status: 'error', output: resultOut }; - const todos = !todoPrinted && r.type === 'ok' && r.name === 'todo_write' + const outcome = toolOutcomeOf(r); + const todos = !todoPrinted && r.status === 'ok' && r.name === 'todo_write' ? todo.read(sid) : undefined; if (todos) todoPrinted = true; @@ -382,4 +377,4 @@ export const AgentLayer = Layer.effect(AgentService, Effect.gen(function* () { } return { runTurn }; -} as any)); +})); diff --git a/packages/codingcode/src/agent/deps.ts b/packages/codingcode/src/agent/deps.ts deleted file mode 100644 index 2da55f6b..00000000 --- a/packages/codingcode/src/agent/deps.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Context } from 'effect'; -import type { Effect } from 'effect'; -import type { AgentError } from '../core/error.js'; -import type { ToolCall, Message, ToolDescription, TodoItem } from '../core/types.js'; -import type { AssistantEvent, ToolResultEvent, TokenUsage, UserEvent } from '../session/types.js'; -import type { SessionStoreState } from '../session/types.js'; -import type { HookDecision } from '../hooks/types.js'; -import type { Skill } from '../skills/types.js'; -import type { ToolResultUnion, ToolLookup } from '../tools/port.js'; -import type { ToolDefinition } from '../tools/types.js'; -import type { LLMClient } from '../llm/client.js'; -import type { ProfileName } from '../core/types.js'; -import type { PermissionMode } from '../approval/types.js'; -import type { ApprovalDecision } from '../approval/types.js'; - -export class SessionPort extends Context.Tag('AgentSessionPort'); - create(cwd: string, opts: { model: string; activeProfile: ProfileName; permissionMode: PermissionMode }, extra?: { parentSessionId?: string; agentName?: string }): Effect.Effect; - recordUser(state: SessionStoreState, content: string): Effect.Effect; - recordSystem(state: SessionStoreState, content: string): Effect.Effect; - recordAssistant(state: SessionStoreState, content: string, toolCalls: ToolCall[], usage?: TokenUsage): Effect.Effect; - recordToolResult(state: SessionStoreState, name: string, id: string, output: string): Effect.Effect; - setPermissionMode(cwd: string, sid: string, mode: PermissionMode): Effect.Effect; - setActiveProfile(cwd: string, sid: string, profile: ProfileName): Effect.Effect; -}>() {} - -export class ToolExecutorPort extends Context.Tag('AgentToolExecutorPort'); -}>() {} - -export class CheckpointPort extends Context.Tag('AgentCheckpointPort'); - snapshotFinal(cwd: string, sid: string, turnId: number): Effect.Effect; -}>() {} - -export class HookPort extends Context.Tag('AgentHookPort')): Effect.Effect; - emitDecision(point: string, payload: Record): Effect.Effect; - disposeSession(sid: string): Effect.Effect; -}>() {} - -export class ApprovalPort extends Context.Tag('AgentApprovalPort'); callId?: string; sessionId: string; projectPath?: string; permissionMode?: PermissionMode; profile?: ProfileName }): Effect.Effect; -}>() {} - -export class SkillPort extends Context.Tag('AgentSkillPort'); -}>() {} - -export class McpPort extends Context.Tag('AgentMcpPort'); -}>() {} - -export class ContextPort extends Context.Tag('AgentContextPort'); - assemblePayload( - transcriptPath: string, - contextWindow: number, - llm: LLMClient | null - ): Promise; -}>() {} - -export class MemoryPort extends Context.Tag('AgentMemoryPort'); -}>() {} - -export class LlmPort extends Context.Tag('AgentLlmPort'); -}>() {} - -export class RulesPort extends Context.Tag('AgentRulesPort')() {} - -export class TodoPort extends Context.Tag('AgentTodoPort')() {} - -export interface ToolEnv { - provide(effect: Effect.Effect): Effect.Effect; -} - -export class ToolEnvPort extends Context.Tag('AgentToolEnvPort'); -}>() {} - -export interface ToolCatalog { - tools: ToolDescription[]; - lookup: ToolLookup; -} - -export class ToolCatalogPort extends Context.Tag('AgentToolCatalogPort')() {} diff --git a/packages/codingcode/src/agent/port.ts b/packages/codingcode/src/agent/port.ts index a7fe3ebb..74fd6658 100644 --- a/packages/codingcode/src/agent/port.ts +++ b/packages/codingcode/src/agent/port.ts @@ -1,8 +1,9 @@ import { Context } from 'effect'; import type { Effect } from 'effect'; -import type { FrameBody } from '../core/frame.js'; -import type { ProfileName } from '../core/types.js'; -import type { PermissionMode } from '../approval/types.js'; +import type { FrameBody } from '../contracts/frame.js'; +import type { ProfileName } from '../contracts/types.js'; +import type { PermissionMode } from '../contracts/permission.js'; +import type { AgentError } from '../core/error.js'; export interface RunTurnOptions { sessionId?: string; @@ -17,10 +18,22 @@ export interface AgentShape { runTurn( input: string, opts: RunTurnOptions - ): Effect.Effect<{ - stream: AsyncGenerator; - sessionId: string; - }>; + ): Effect.Effect< + { + stream: AsyncGenerator; + sessionId: string; + }, + AgentError + >; } export class AgentService extends Context.Tag('AgentService')() {} + +// agent 自持的装配端口:工具执行期注入能力,不离开 agent/ +export interface ToolEnv { + provide(effect: Effect.Effect): Effect.Effect; +} + +export class ToolEnvPort extends Context.Tag('AgentToolEnvPort'); +}>() {} diff --git a/packages/codingcode/src/agent/profile.ts b/packages/codingcode/src/agent/profile.ts index 50d6adf4..948a9dd8 100644 --- a/packages/codingcode/src/agent/profile.ts +++ b/packages/codingcode/src/agent/profile.ts @@ -1,11 +1,11 @@ -import type { ProfileName } from '../core/types.js'; +import type { ProfileName } from '../contracts/types.js'; export interface AgentProfile { name: ProfileName; systemPrompt?: string; } -import { PLAN_ALLOWED_TOOLS } from '../approval/types.js'; +import { PLAN_ALLOWED_TOOLS } from '../contracts/permission.js'; export const PLAN_PROFILE_NAME = 'plan' as const; export const BUILD_PROFILE_NAME = 'build' as const; @@ -115,8 +115,6 @@ export const BUILD_PROFILE: AgentProfile = { systemPrompt: BUILD_PROMPT, }; -// 各 profile 的工具名字名单:agent 只把这份名单交给工具模块注册,工具模块按名查表装配。 -// build 含写工具、不含 submit_plan;plan 相反(只读 + submit_plan),名单即审批层白名单。 export const PLAN_TOOL_NAMES: readonly string[] = [...PLAN_ALLOWED_TOOLS]; export const BUILD_TOOL_NAMES: readonly string[] = [ @@ -132,7 +130,6 @@ export const BUILD_TOOL_NAMES: readonly string[] = [ 'dispatch_agent', ]; -// 运行时审批兜底(plan 模式 deny 非名单工具),从名单派生 export function isPlanProfile(p: { name: string } | null | undefined): boolean { return p?.name === PLAN_PROFILE_NAME; } diff --git a/packages/codingcode/src/agent/tool-catalog.ts b/packages/codingcode/src/agent/tool-catalog.ts deleted file mode 100644 index dbaeede1..00000000 --- a/packages/codingcode/src/agent/tool-catalog.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Layer } from 'effect'; -import { ToolCatalogPort } from './deps.js'; -import type { ToolCatalog } from './deps.js'; -import { createToolCatalog } from '../tools/catalog.js'; - -export const ToolCatalogLayer: Layer.Layer = Layer.succeed( - ToolCatalogPort, - { - register: (toolNames, mcpTools): ToolCatalog => createToolCatalog(toolNames, mcpTools), - } -); diff --git a/packages/codingcode/src/agent/tool-env.ts b/packages/codingcode/src/agent/tool-env.ts index 974f8a8b..59e0d0b3 100644 --- a/packages/codingcode/src/agent/tool-env.ts +++ b/packages/codingcode/src/agent/tool-env.ts @@ -1,28 +1,18 @@ import { Effect, Layer } from 'effect'; -import { TodoService } from '../todo/port.js'; -import { HookService } from '../hooks/port.js'; -import { McpService } from '../mcp/port.js'; -import { SubagentRunnerService } from '../subagent/port.js'; -import { ToolEnvPort } from './deps.js'; -import type { ToolEnv } from './deps.js'; +import type { Context } from 'effect'; +import { ToolEnvPort } from './port.js'; +import type { ToolEnv } from './port.js'; -export const ToolEnvLayer: Layer.Layer = Layer.effect( +export const ToolEnvLayer = Layer.effect( ToolEnvPort, Effect.succeed({ - getToolEnv: (): Effect.Effect => + getToolEnv: (): Effect.Effect => Effect.gen(function* () { - const todoSvc = yield* TodoService; - const hookSvc = yield* HookService; - const mcpSvc = yield* McpService; - const subagentSvc = yield* SubagentRunnerService; + + const ctx = yield* (Effect.context() as Effect.Effect>); const env: ToolEnv = { - provide: (effect: Effect.Effect) => - effect.pipe( - Effect.provideService(TodoService, todoSvc), - Effect.provideService(HookService, hookSvc), - Effect.provideService(McpService, mcpSvc), - Effect.provideService(SubagentRunnerService, subagentSvc), - ) as Effect.Effect, + provide: (effect: Effect.Effect): Effect.Effect => + Effect.provide(effect, ctx) as Effect.Effect, }; return env; }), diff --git a/packages/codingcode/src/approval/approval.ts b/packages/codingcode/src/approval/approval.ts index f9851522..984dc4d1 100644 --- a/packages/codingcode/src/approval/approval.ts +++ b/packages/codingcode/src/approval/approval.ts @@ -1,12 +1,15 @@ import { Layer, Effect } from 'effect'; import { HookService } from '../hooks/port.js'; -import type { ApprovalDecision, PermissionMode, PermissionRule, ToolCallRequest } from './types.js'; -import type { ProfileName } from '../core/types.js'; -import { PLAN_ALLOWED_TOOLS } from './types.js'; +import type { HookShape } from '../hooks/port.js'; +import type { ApprovalDecision, PermissionMode } from '../contracts/permission.js'; +import type { PermissionRule, ToolCallRequest } from './types.js'; +import type { ProfileName } from '../contracts/types.js'; +import { PLAN_ALLOWED_TOOLS } from '../contracts/permission.js'; import { createRuleEngine, type RuleEngine } from './rule-engine.js'; import { userConfirmAsync } from './confirmation.js'; import { ApprovalWaitService } from './wait-port.js'; import { ApprovalService } from './port.js'; +import type { ApprovalRequest } from './port.js'; const DANGEROUS_TOOL_NAMES = ['execute_command']; @@ -64,11 +67,11 @@ function applyPermissionMode( } function recordAuditAndReturn( - hooks: any, + hooks: HookShape, request: ToolCallRequest, decision: ApprovalDecision, passedLayers: string[] -): any { +): Effect.Effect { return Effect.gen(function* () { passedLayers.push(LAYER_NAMES[4]); yield* hooks.emit('tool.approval.post', { @@ -84,10 +87,10 @@ function recordAuditAndReturn( export function runPipeline( request: ToolCallRequest, opts: PipelineOptions -): any { +): Effect.Effect { return Effect.gen(function* () { - const hooks: any = yield* HookService; - const approvalWait: any = yield* ApprovalWaitService; + const hooks = yield* HookService; + const approvalWait = yield* ApprovalWaitService; const asyncConfirm = yield* approvalWait.hasEmitter(opts.sessionId); const layers: string[] = []; @@ -206,16 +209,7 @@ export const ApprovalLayer = Layer.effect(ApprovalService, Effect.gen(function* const destructiveTools = new Set(DANGEROUS_TOOL_NAMES); return { - evaluate: (request: { - tool: string; - input: Record; - context?: Record; - callId?: string; - sessionId: string; - projectPath?: string; - permissionMode?: PermissionMode; - profile?: ProfileName; - }): any => + evaluate: (request: ApprovalRequest): Effect.Effect => runPipeline( { tool: request.tool, @@ -239,4 +233,4 @@ export const ApprovalLayer = Layer.effect(ApprovalService, Effect.gen(function* Effect.provideService(ApprovalWaitService, approvalWait) ), }; -} as any)); +})); diff --git a/packages/codingcode/src/approval/port.ts b/packages/codingcode/src/approval/port.ts index 993a2297..83f3184a 100644 --- a/packages/codingcode/src/approval/port.ts +++ b/packages/codingcode/src/approval/port.ts @@ -1,10 +1,19 @@ import { Context } from 'effect'; import type { Effect } from 'effect'; -import type { ProfileName } from '../core/types.js'; -import type { PermissionMode, ApprovalDecision } from './types.js'; +import type { ProfileName } from '../contracts/types.js'; +import type { PermissionMode, ApprovalDecision } from '../contracts/permission.js'; +import type { ToolCallRequest } from './types.js'; + +/** 审批请求:工具调用 + 审批上下文。 */ +export interface ApprovalRequest extends ToolCallRequest { + sessionId: string; + projectPath?: string; + permissionMode?: PermissionMode; + profile?: ProfileName; +} export interface ApprovalShape { - evaluate(request: { tool: string; input: Record; context?: Record; callId?: string; sessionId: string; projectPath?: string; permissionMode?: PermissionMode; profile?: ProfileName }): Effect.Effect; + evaluate(request: ApprovalRequest): Effect.Effect; } export class ApprovalService extends Context.Tag('Approval')() {} diff --git a/packages/codingcode/src/approval/rule-engine.ts b/packages/codingcode/src/approval/rule-engine.ts index b131483c..53041669 100644 --- a/packages/codingcode/src/approval/rule-engine.ts +++ b/packages/codingcode/src/approval/rule-engine.ts @@ -1,4 +1,5 @@ -import type { PermissionRule, RuleAction, ApprovalDecision } from './types.js'; +import type { ApprovalDecision } from '../contracts/permission.js'; +import type { PermissionRule, RuleAction } from './types.js'; /** * Convert a simple glob pattern to a RegExp. diff --git a/packages/codingcode/src/approval/types.ts b/packages/codingcode/src/approval/types.ts index 834afe62..aed50c04 100644 --- a/packages/codingcode/src/approval/types.ts +++ b/packages/codingcode/src/approval/types.ts @@ -1,25 +1,10 @@ -export type PermissionMode = 'default' | 'acceptEdits' | 'bypass'; - -export const PERMISSION_MODES: readonly PermissionMode[] = [ - 'default', - 'acceptEdits', - 'bypass', -] as const; - -// plan 权限模式只允许这组工具(只读 + submit_plan),其余一律 deny。 -// 作为审批层的权威白名单,agent 侧的工具可见性名单也从这里派生。 -export const PLAN_ALLOWED_TOOLS: ReadonlySet = new Set([ - 'read_file', - 'search_files', - 'search_code', - 'fetch_url', - 'submit_plan', -]); +import { PERMISSION_MODES, type PermissionMode } from '../contracts/permission.js'; export function isPermissionMode(value: unknown): value is PermissionMode { return typeof value === 'string' && (PERMISSION_MODES as readonly string[]).includes(value); } +/** 工具调用请求:工具名 + 入参 + 可选调用上下文。 */ export interface ToolCallRequest { tool: string; input: Record; @@ -27,10 +12,6 @@ export interface ToolCallRequest { callId?: string; } -export type ApprovalDecision = - | { type: 'deny'; reason: string; source: string } - | { type: 'allow'; source: string }; - export type RuleAction = 'deny' | 'allow' | 'ask'; export interface PermissionRule { diff --git a/packages/codingcode/src/cli.ts b/packages/codingcode/src/cli.ts index f3fa0eff..8cf03c28 100644 --- a/packages/codingcode/src/cli.ts +++ b/packages/codingcode/src/cli.ts @@ -4,7 +4,7 @@ import { LLMFactoryService } from './llm/port.js'; import { createServer } from './server/index.js'; import { createAppRuntime } from './layer.js'; import { loadConfig, ensureUserConfig } from '@codingcode/infra/config'; -import { WorkspaceService, parseWorkspaceArgs } from './core/workspace.js'; +import { WorkspaceService, parseWorkspaceArgs } from './workspace/workspace.js'; import { findAvailablePort } from './server/port-discovery.js'; import { AgentError } from './core/error.js'; import { SchedulerService } from './scheduler/port.js'; diff --git a/packages/codingcode/src/client/contracts.ts b/packages/codingcode/src/client/contracts.ts index d4c18487..5492dcf1 100644 --- a/packages/codingcode/src/client/contracts.ts +++ b/packages/codingcode/src/client/contracts.ts @@ -1,17 +1,16 @@ -import type { PermissionMode } from '../approval/types.js'; -import type { ProfileName, TokenUsage } from '../core/types.js'; +import type { PermissionMode } from '../contracts/permission.js'; +import type { ProfileName, TokenUsage } from '../contracts/types.js'; import type { CheckpointDiff, CodeRollbackResult, RollbackPreviewDiff, } from '../checkpoint/types.js'; -import type { SelectableModel } from '../llm/port.js'; -import type { McpServerConfig, McpStatus } from '../mcp/types.js'; -import type { UserHookConfig } from '../hooks/types.js'; -import type {SessionIndex } from '../session/types.js'; -import type { UITurn } from '../session/port.js'; +import type { SelectableModel } from '../contracts/provider.js'; +import type { McpServerConfig, McpStatus } from '../contracts/mcp.js'; +import type { UserHookConfig } from '../contracts/hooks.js'; +import type { SessionIndex, UITurn } from '../contracts/session.js'; import type { AVAILABLE_PROFILES } from '../agent/profile.js'; -import type { Frame } from '../core/frame.js'; +import type { Frame } from '../contracts/frame.js'; export type { TokenUsage, CheckpointDiff, CodeRollbackResult, RollbackPreviewDiff }; diff --git a/packages/codingcode/src/client/http/agent-runtime.ts b/packages/codingcode/src/client/http/agent-runtime.ts index cc3b4c4c..f0001be9 100644 --- a/packages/codingcode/src/client/http/agent-runtime.ts +++ b/packages/codingcode/src/client/http/agent-runtime.ts @@ -1,5 +1,5 @@ import type { AgentRuntimeClient } from '../contracts.js'; -import { decodeFrame } from '../../core/frame-io.js'; +import { decodeFrame } from '../../contracts/frame-io.js'; import { parseSseStream } from '../sse.js'; import type { createRequestHelpers } from './request.js'; diff --git a/packages/codingcode/src/client/http/request.ts b/packages/codingcode/src/client/http/request.ts index 1b2b8b78..4d1bcb2d 100644 --- a/packages/codingcode/src/client/http/request.ts +++ b/packages/codingcode/src/client/http/request.ts @@ -1,4 +1,4 @@ -import { ApiError } from '../../core/error.js'; +import { ApiError } from '../../contracts/error.js'; async function parseErrorBody( res: Response diff --git a/packages/codingcode/src/client/http/sessions.ts b/packages/codingcode/src/client/http/sessions.ts index 11da400b..9427de96 100644 --- a/packages/codingcode/src/client/http/sessions.ts +++ b/packages/codingcode/src/client/http/sessions.ts @@ -1,5 +1,5 @@ -import type { PermissionMode } from '../../approval/types.js'; -import type { SessionIndex } from '../../session/types.js'; +import type { PermissionMode } from '../../contracts/permission.js'; +import type { SessionIndex } from '../../contracts/session.js'; import type { SessionClient } from '../contracts.js'; import type { createRequestHelpers } from './request.js'; diff --git a/packages/codingcode/src/client/http/settings.ts b/packages/codingcode/src/client/http/settings.ts index e06ef459..526b8791 100644 --- a/packages/codingcode/src/client/http/settings.ts +++ b/packages/codingcode/src/client/http/settings.ts @@ -1,5 +1,5 @@ -import type { PermissionMode } from '../../approval/types.js'; -import type { McpStatus } from '../../mcp/types.js'; +import type { PermissionMode } from '../../contracts/permission.js'; +import type { McpStatus } from '../../contracts/mcp.js'; import type { SettingsClient } from '../contracts.js'; import type { createRequestHelpers } from './request.js'; diff --git a/packages/codingcode/src/context/context.ts b/packages/codingcode/src/context/context.ts index e11f151f..88ddea82 100644 --- a/packages/codingcode/src/context/context.ts +++ b/packages/codingcode/src/context/context.ts @@ -2,20 +2,14 @@ import { Layer, Effect } from 'effect'; import { randomUUID } from 'crypto'; import { readFileSync, existsSync } from 'fs'; import { loadConfig } from '@codingcode/infra/config'; -import type { Message } from '../core/types.js'; +import type { Message } from '../contracts/types.js'; import { SessionService } from '../session/port.js'; -import { estimateTokens, estimateMessageTokens } from '../core/util.js'; +import { estimateTokens, estimateMessageTokens } from './tokens.js'; import { resolveLLM } from '../llm/llm-resolver.js'; import { LLMFactoryService } from '../llm/port.js'; import { COMPACTION_SYSTEM_PROMPT } from './compaction-prompt.js'; -import type { - SessionEvent, - AssistantEvent, - ToolResultEvent, - CompactEvent, - SummaryEvent, -} from '../session/types.js'; -import type { LLMClient } from '../llm/client.js'; +import type { SessionEvent, AssistantEvent, ToolResultEvent, CompactEvent, SummaryEvent } from '../contracts/session.js'; +import type { LLMClient } from '../contracts/provider.js'; import { ContextService } from './port.js'; import type { CompressResult } from './port.js'; diff --git a/packages/codingcode/src/context/port.ts b/packages/codingcode/src/context/port.ts index 569106dc..d42168a4 100644 --- a/packages/codingcode/src/context/port.ts +++ b/packages/codingcode/src/context/port.ts @@ -1,6 +1,6 @@ import { Context } from 'effect'; -import type { Message } from '../core/types.js'; -import type { LLMClient } from '../llm/client.js'; +import type { Message } from '../contracts/types.js'; +import type { LLMClient } from '../contracts/provider.js'; export interface CompressResult { didCompress: boolean; diff --git a/packages/codingcode/src/core/util.ts b/packages/codingcode/src/context/tokens.ts similarity index 94% rename from packages/codingcode/src/core/util.ts rename to packages/codingcode/src/context/tokens.ts index 9c5415ae..bfe729fb 100644 --- a/packages/codingcode/src/core/util.ts +++ b/packages/codingcode/src/context/tokens.ts @@ -1,4 +1,4 @@ -import type { Message } from './types.js'; +import type { Message } from '../contracts/types.js'; export function estimateMessageTokens(m: Message): number { let tokens = estimateTokensForContent(m.content ?? ''); diff --git a/packages/codingcode/src/contracts/error.ts b/packages/codingcode/src/contracts/error.ts new file mode 100644 index 00000000..3dcf8109 --- /dev/null +++ b/packages/codingcode/src/contracts/error.ts @@ -0,0 +1,32 @@ +export class AlreadyExistsError extends Error { + readonly code = 'ALREADY_EXISTS'; + constructor(message: string) { + super(message); + this.name = 'AlreadyExistsError'; + } + httpStatus(): 409 { + return 409; + } +} + +export class ApiError extends Error { + constructor( + public readonly status: number, + public readonly path: string, + public readonly body?: { code: string; message: string } + ) { + super(body?.message ?? `HTTP ${status}: ${path}`); + this.name = 'ApiError'; + } +} + +export class NotFoundError extends Error { + readonly code = 'NOT_FOUND'; + constructor(message: string) { + super(message); + this.name = 'NotFoundError'; + } + httpStatus(): 404 { + return 404; + } +} diff --git a/packages/codingcode/src/core/frame-io.ts b/packages/codingcode/src/contracts/frame-io.ts similarity index 100% rename from packages/codingcode/src/core/frame-io.ts rename to packages/codingcode/src/contracts/frame-io.ts diff --git a/packages/codingcode/src/core/frame.ts b/packages/codingcode/src/contracts/frame.ts similarity index 100% rename from packages/codingcode/src/core/frame.ts rename to packages/codingcode/src/contracts/frame.ts diff --git a/packages/codingcode/src/contracts/hooks.ts b/packages/codingcode/src/contracts/hooks.ts new file mode 100644 index 00000000..6837773e --- /dev/null +++ b/packages/codingcode/src/contracts/hooks.ts @@ -0,0 +1,39 @@ +export type HookPoint = + | 'tool.execute.before' + | 'tool.execute.after' + | 'tool.execute.error' + | 'tool.execute.denied' + | 'tool.approval.pre' + | 'tool.approval.post' + | 'llm.request.before' + | 'llm.response.after' + | 'llm.response.error' + | 'session.save.before' + | 'session.save.after' + | 'agent.turn.start' + | 'agent.step.before' + | 'agent.turn.stop' + | 'agent.turn.end' + | 'agent.subagent.spawn.before' + | 'agent.subagent.spawn.after' + | 'agent.subagent.complete'; + +export interface HookDecision { + decision?: 'allow' | 'deny' | 'ask' | 'continue'; + reason?: string; + injection?: string; + modifiedInput?: Record; + modifiedOutput?: unknown; +} + +export interface UserHookConfig { + name: string; + description?: string; + point: HookPoint; + type: 'observer' | 'decision'; + command: string; + args?: string[]; + env?: Record; + priority?: number; + enabled: boolean; +} diff --git a/packages/codingcode/src/mcp/types.ts b/packages/codingcode/src/contracts/mcp.ts similarity index 64% rename from packages/codingcode/src/mcp/types.ts rename to packages/codingcode/src/contracts/mcp.ts index c4c6fa49..5f694653 100644 --- a/packages/codingcode/src/mcp/types.ts +++ b/packages/codingcode/src/contracts/mcp.ts @@ -1,3 +1,6 @@ +import type { Effect } from 'effect'; +import type { AgentError } from '../core/error.js'; + export interface McpServerConfig { name: string; /** stdio: executable command */ @@ -25,3 +28,12 @@ export interface McpStatus { reconnectAttempts: number; leaseCount: number; } + +/** MCP 远端工具的纯数据描述:zod schema 与 SDK client 等机制形状由实现层持有 */ +export interface McpToolSpec { + server: string; + name: string; + description: string; + inputSchema: Record; + execute(args: Record): Effect.Effect; +} diff --git a/packages/codingcode/src/contracts/permission.ts b/packages/codingcode/src/contracts/permission.ts new file mode 100644 index 00000000..612b24d9 --- /dev/null +++ b/packages/codingcode/src/contracts/permission.ts @@ -0,0 +1,19 @@ +export const PERMISSION_MODES = [ + 'default', + 'acceptEdits', + 'bypass', +] as const; + +export type PermissionMode = (typeof PERMISSION_MODES)[number]; + +export const PLAN_ALLOWED_TOOLS: ReadonlySet = new Set([ + 'read_file', + 'search_files', + 'search_code', + 'fetch_url', + 'submit_plan', +]); + +export type ApprovalDecision = + | { type: 'deny'; reason: string; source: string } + | { type: 'allow'; source: string }; diff --git a/packages/codingcode/src/llm/types.ts b/packages/codingcode/src/contracts/provider.ts similarity index 57% rename from packages/codingcode/src/llm/types.ts rename to packages/codingcode/src/contracts/provider.ts index c04fce3d..9b1c5d3b 100644 --- a/packages/codingcode/src/llm/types.ts +++ b/packages/codingcode/src/contracts/provider.ts @@ -1,4 +1,6 @@ -import type { Message, ToolCall, ToolDescription, TokenUsage } from '../core/types.js'; +import type { Effect } from 'effect'; +import type { Message, ToolCall, ToolDescription, TokenUsage } from './types.js'; +import type { AgentError } from '../core/error.js'; export interface LLMRequest { messages: Message[]; @@ -32,3 +34,21 @@ export interface ModelInfo { supportsToolCalling: boolean; supportsStreaming: boolean; } + +export interface LLMClient { + complete(req: LLMRequest, signal?: AbortSignal): Effect.Effect; + /** 产出 SDK 流部件;失败时在迭代中抛出 AgentError */ + completeStream(req: LLMRequest, signal?: AbortSignal): AsyncIterable; + readonly modelInfo: ModelInfo; +} + +export interface SelectableModel { + id: string; + provider: string; + driver: string; + name: string; + model: string; + base_url: string; + api_key_env: string; + context_window: number; +} diff --git a/packages/codingcode/src/session/types.ts b/packages/codingcode/src/contracts/session.ts similarity index 60% rename from packages/codingcode/src/session/types.ts rename to packages/codingcode/src/contracts/session.ts index 0797c67b..4bed2181 100644 --- a/packages/codingcode/src/session/types.ts +++ b/packages/codingcode/src/contracts/session.ts @@ -1,7 +1,5 @@ -import type { ProfileName, TokenUsage, ToolCall } from '../core/types.js'; -import type { PermissionMode } from '../approval/types.js'; - -export type { TokenUsage }; +import type { ProfileName, TokenUsage, ToolCall } from './types.js'; +import type { PermissionMode } from './permission.js'; export interface SessionMetaEvent { type: 'session_meta'; @@ -97,3 +95,46 @@ export interface SessionStoreState { memorySnapshot: string; parentSessionId?: string; } + +export type UITurnItem = + | { id: string; type: 'message'; role: 'user' | 'assistant'; content: string; partial?: boolean } + | { + id: string; + type: 'tool_call'; + name: string; + args: Record; + status: 'pending' | 'approved' | 'rejected' | 'running'; + } + | { + id: string; + type: 'tool_result'; + callId: string; + name: string; + output: string; + exitCode?: number; + filePath?: string; + diff?: string; + insertions?: number; + deletions?: number; + } + | { + id: string; + type: 'summary'; + content: string; + startTurnId: number; + endTurnId: number; + } + | { id: string; type: 'reasoning'; content: string; isVisible: boolean } + | { id: string; type: 'error'; message: string; code?: string }; + +export interface UITurn { + id: string; + items: UITurnItem[]; + status: 'running' | 'completed' | 'error'; +} + +export interface SessionCreateOptions { + model: string; + activeProfile: ProfileName; + permissionMode: PermissionMode; +} diff --git a/packages/codingcode/src/skills/types.ts b/packages/codingcode/src/contracts/skill.ts similarity index 68% rename from packages/codingcode/src/skills/types.ts rename to packages/codingcode/src/contracts/skill.ts index 17d4fcd1..b0613302 100644 --- a/packages/codingcode/src/skills/types.ts +++ b/packages/codingcode/src/contracts/skill.ts @@ -1,6 +1,5 @@ export interface Skill { readonly name: string; readonly description: string; - /** Absolute path to the skill's SKILL.md file. */ readonly skillPath: string; } diff --git a/packages/codingcode/src/contracts/tool.ts b/packages/codingcode/src/contracts/tool.ts new file mode 100644 index 00000000..2407af71 --- /dev/null +++ b/packages/codingcode/src/contracts/tool.ts @@ -0,0 +1,26 @@ +import type { Effect } from 'effect'; +import type { AgentError } from '../core/error.js'; +import type { ToolOutcome } from './frame.js'; +import type { ToolDescription } from './types.js'; + +export interface ToolExecCtx { + signal?: AbortSignal; + sessionId?: string; + projectPath?: string; +} + +export type ToolResult = { readonly id: string; readonly name: string } & ToolOutcome; + +export interface ToolRunner { + readonly name: string; + parse(args: unknown): unknown; + execute(args: unknown, ctx?: ToolExecCtx): Effect.Effect; +} + +export type ToolLookup = (name: string) => ToolRunner | undefined; + +// 一次装配出的工具集:tools 为纯描述喂模型,lookup 供执行器解析 +export interface ToolCatalog { + tools: ToolDescription[]; + lookup: ToolLookup; +} diff --git a/packages/codingcode/src/core/types.ts b/packages/codingcode/src/contracts/types.ts similarity index 100% rename from packages/codingcode/src/core/types.ts rename to packages/codingcode/src/contracts/types.ts diff --git a/packages/codingcode/src/core/error.ts b/packages/codingcode/src/core/error.ts index 2f124edd..e4975ae1 100644 --- a/packages/codingcode/src/core/error.ts +++ b/packages/codingcode/src/core/error.ts @@ -17,39 +17,6 @@ export type ErrorCode = | 'AGENT_LOOP_DETECTED' | 'SESSION_IO_ERROR'; -export class AlreadyExistsError extends Error { - readonly code = 'ALREADY_EXISTS'; - constructor(message: string) { - super(message); - this.name = 'AlreadyExistsError'; - } - httpStatus(): 409 { - return 409; - } -} - -export class ApiError extends Error { - constructor( - public readonly status: number, - public readonly path: string, - public readonly body?: { code: string; message: string } - ) { - super(body?.message ?? `HTTP ${status}: ${path}`); - this.name = 'ApiError'; - } -} - -export class NotFoundError extends Error { - readonly code = 'NOT_FOUND'; - constructor(message: string) { - super(message); - this.name = 'NotFoundError'; - } - httpStatus(): 404 { - return 404; - } -} - export class AgentError extends Error { constructor( public readonly code: ErrorCode, diff --git a/packages/codingcode/src/core/path.ts b/packages/codingcode/src/core/path.ts index cbcb7757..a2253983 100644 --- a/packages/codingcode/src/core/path.ts +++ b/packages/codingcode/src/core/path.ts @@ -17,24 +17,15 @@ export function encodeProjectPath(p: string): string { } let _projectBaseOverride: string | undefined; -let _projectPlansBaseOverride: string | undefined; export function setProjectBaseDir(dir: string | undefined): void { _projectBaseOverride = dir; } -export function setProjectPlansBaseDir(dir: string | undefined): void { - _projectPlansBaseOverride = dir; -} - export function getProjectBaseDir(): string { return _projectBaseOverride ?? join(homedir(), '.codingcode', 'project'); } -export function getProjectPlansBaseDir(): string { - return _projectPlansBaseOverride ?? join(homedir(), '.codingcode', 'projects'); -} - export interface SessionPaths { sessionId: string; cwd: string; @@ -47,10 +38,6 @@ export function projectSessionsDir(encodedProjectPath: string): string { return join(getProjectBaseDir(), encodedProjectPath, 'sessions'); } -export function sessionJsonlPathFromCwd(cwd: string, sessionId: string): string { - return computePaths(cwd, sessionId).transcriptPath; -} - export function computePaths( cwd: string, sessionId: string, diff --git a/packages/codingcode/src/direct/agent-runtime.ts b/packages/codingcode/src/direct/agent-runtime.ts index dbe60b1b..30986f13 100644 --- a/packages/codingcode/src/direct/agent-runtime.ts +++ b/packages/codingcode/src/direct/agent-runtime.ts @@ -4,13 +4,12 @@ import { ApprovalWaitService } from '../approval/wait-port.js'; import { parseApprovalResponse } from '../approval/confirmation.js'; import { ContextService } from '../context/port.js'; import { SessionService } from '../session/port.js'; -import type { SessionStatePort } from '../session/port.js'; import { computePaths } from '../core/path.js'; import type { AgentRuntimeClient } from '../client/contracts.js'; -import type { FrameBody } from '../core/frame.js'; -import { createFrameAssembler } from '../core/frame-io.js'; +import type { FrameBody } from '../contracts/frame.js'; +import { createFrameAssembler } from '../contracts/frame-io.js'; import type { AppRuntime } from '../layer.js'; -import type { LLMClient } from '../llm/client.js'; +import type { LLMClient } from '../contracts/provider.js'; export function createDirectAgentClient(llm: LLMClient, rt: AppRuntime): AgentRuntimeClient { return { @@ -95,7 +94,7 @@ export function createDirectAgentClient(llm: LLMClient, rt: AppRuntime): AgentRu async compact({ sessionId, cwd }) { await rt.runPromise( Effect.gen(function* () { - const session: SessionStatePort = yield* SessionService; + const session = yield* SessionService; const context = yield* ContextService; const state = yield* session.load(cwd, sessionId); return yield* Effect.promise(() => diff --git a/packages/codingcode/src/direct/sessions.ts b/packages/codingcode/src/direct/sessions.ts index e0f52ee4..d854f085 100644 --- a/packages/codingcode/src/direct/sessions.ts +++ b/packages/codingcode/src/direct/sessions.ts @@ -2,9 +2,8 @@ import { Effect } from 'effect'; import { readFileSync, readdirSync, statSync, existsSync } from 'fs'; import { join } from 'path'; import { SessionService } from '../session/port.js'; -import type { SessionStorePort } from '../session/port.js'; import { encodeProjectPath, getProjectBaseDir } from '../core/path.js'; -import type { PermissionMode } from '../approval/types.js'; +import type { PermissionMode } from '../contracts/permission.js'; import { AVAILABLE_PROFILES } from '../agent/profile.js'; import type { SessionClient } from '../client/contracts.js'; import type { AppRuntime } from '../layer.js'; @@ -14,7 +13,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient { async createSession({ cwd, activeProfile, permissionMode, model }) { return rt.runPromise( Effect.gen(function* () { - const session: SessionStorePort = yield* SessionService; + const session = yield* SessionService; const state = yield* session.create(cwd, { model, activeProfile, @@ -28,7 +27,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient { async resumeSession({ sessionId, cwd }) { return rt.runPromise( Effect.gen(function* () { - const session: SessionStorePort = yield* SessionService; + const session = yield* SessionService; return yield* session.readUITurns(sessionId, cwd); }) ); @@ -37,7 +36,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient { async listSessions({ cwd }) { return rt.runPromise( Effect.gen(function* () { - const session: SessionStorePort = yield* SessionService; + const session = yield* SessionService; return yield* session.listSessions(cwd); }) ); @@ -46,7 +45,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient { async getSessionHistory({ sessionId, cwd }) { return rt.runPromise( Effect.gen(function* () { - const session: SessionStorePort = yield* SessionService; + const session = yield* SessionService; return yield* session.readUITurns(sessionId, cwd); }) ); @@ -55,7 +54,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient { async deleteSession({ sessionId, cwd }) { await rt.runPromise( Effect.gen(function* () { - const session: SessionStorePort = yield* SessionService; + const session = yield* SessionService; yield* session.deleteSession(sessionId, cwd); }) ); @@ -64,7 +63,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient { async getSessionProfile({ sessionId, cwd }) { return rt.runPromise( Effect.gen(function* () { - const session: SessionStorePort = yield* SessionService; + const session = yield* SessionService; const state = yield* session.load(cwd, sessionId); return { activeProfile: state.activeProfile, @@ -79,7 +78,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient { async setSessionProfile({ sessionId, cwd, activeProfile }) { return rt.runPromise( Effect.gen(function* () { - const session: SessionStorePort = yield* SessionService; + const session = yield* SessionService; yield* session.setActiveProfile(cwd, sessionId, activeProfile); const state = yield* session.load(cwd, sessionId); return { activeProfile: state.activeProfile, permissionMode: state.permissionMode }; @@ -90,7 +89,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient { async getSessionPermissionMode({ sessionId, cwd }): Promise { const mode = await rt.runPromise( Effect.gen(function* () { - const session: SessionStorePort = yield* SessionService; + const session = yield* SessionService; const state = yield* session.load(cwd, sessionId); return state.permissionMode; }) @@ -101,7 +100,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient { async setSessionPermissionMode({ sessionId, cwd, mode }) { return rt.runPromise( Effect.gen(function* () { - const session: SessionStorePort = yield* SessionService; + const session = yield* SessionService; yield* session.setPermissionMode(cwd, sessionId, mode); }) ); @@ -167,7 +166,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient { async forkSession({ sessionId, cwd, atTurnId }) { const newSessionId = await rt.runPromise( Effect.gen(function* () { - const session: SessionStorePort = yield* SessionService; + const session = yield* SessionService; const state = yield* session.load(cwd, sessionId); return yield* session.forkSession(state, atTurnId ?? 0); }) diff --git a/packages/codingcode/src/direct/settings.ts b/packages/codingcode/src/direct/settings.ts index 0629c0d4..dd915829 100644 --- a/packages/codingcode/src/direct/settings.ts +++ b/packages/codingcode/src/direct/settings.ts @@ -1,10 +1,10 @@ import { Effect } from 'effect'; import { McpService } from '../mcp/port.js'; -import type { McpServerConfig, McpStatus } from '../mcp/types.js'; +import type { McpServerConfig, McpStatus } from '../contracts/mcp.js'; import { SkillService } from '../skills/port.js'; -import type { PermissionMode } from '../approval/types.js'; -import type { UserHookConfig } from '../hooks/types.js'; -import { isGlobalCwd } from '../core/workspace.js'; +import type { PermissionMode } from '../contracts/permission.js'; +import type { UserHookConfig } from '../contracts/hooks.js'; +import { isGlobalCwd } from '../workspace/workspace.js'; import { loadMcpConfig, writeMcpConfig, @@ -28,7 +28,7 @@ import { import { setHookRuntimeEnabled } from '../hooks/executor.js'; import { getMemoryConfig } from '../memory/config.js'; import { MemoryService } from '../memory/port.js'; -import { AlreadyExistsError, NotFoundError } from '../core/error.js'; +import { AlreadyExistsError, NotFoundError } from '../contracts/error.js'; import { loadConfig, updateMemoryModel, diff --git a/packages/codingcode/src/hooks/config.ts b/packages/codingcode/src/hooks/config.ts index df7cf68f..822dd11f 100644 --- a/packages/codingcode/src/hooks/config.ts +++ b/packages/codingcode/src/hooks/config.ts @@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; -import type { HookPoint, UserHookConfig } from './types.js'; +import type { HookPoint, UserHookConfig } from '../contracts/hooks.js'; import { createDisabledStore } from '@codingcode/infra/disabled-store'; let _globalConfigDirOverride: string | undefined; diff --git a/packages/codingcode/src/hooks/hooks.ts b/packages/codingcode/src/hooks/hooks.ts index c4a1703a..f1923901 100644 --- a/packages/codingcode/src/hooks/hooks.ts +++ b/packages/codingcode/src/hooks/hooks.ts @@ -7,16 +7,8 @@ import { } from './executor.js'; import { createLogger } from '@codingcode/infra/logger'; import { HookService } from './port.js'; -import type { - HookPoint, - HookDecision, - ObserverHandler, - DecisionHandler, - HandlerEntry, - ProjectPath, - SessionId, - HookName, -} from './types.js'; +import type { HookPoint, HookDecision } from '../contracts/hooks.js'; +import type { ObserverHandler, DecisionHandler, HandlerEntry, ProjectPath, SessionId, HookName } from './types.js'; const logger = createLogger(); diff --git a/packages/codingcode/src/hooks/port.ts b/packages/codingcode/src/hooks/port.ts index f3efa197..0df128e5 100644 --- a/packages/codingcode/src/hooks/port.ts +++ b/packages/codingcode/src/hooks/port.ts @@ -1,6 +1,7 @@ import { Context } from 'effect'; import type { Effect } from 'effect'; -import type { HookPoint, HookDecision, ObserverHandler, DecisionHandler } from './types.js'; +import type { HookPoint, HookDecision } from '../contracts/hooks.js'; +import type { ObserverHandler, DecisionHandler } from './types.js'; export interface HookShape { register(point: HookPoint, handler: ObserverHandler, opts?: { source?: 'system' | 'user' }): Effect.Effect<() => void>; diff --git a/packages/codingcode/src/hooks/types.ts b/packages/codingcode/src/hooks/types.ts index 981d94c0..f560b5ca 100644 --- a/packages/codingcode/src/hooks/types.ts +++ b/packages/codingcode/src/hooks/types.ts @@ -1,32 +1,5 @@ import type { Effect } from 'effect'; - -export type HookPoint = - | 'tool.execute.before' - | 'tool.execute.after' - | 'tool.execute.error' - | 'tool.execute.denied' - | 'tool.approval.pre' - | 'tool.approval.post' - | 'llm.request.before' - | 'llm.response.after' - | 'llm.response.error' - | 'session.save.before' - | 'session.save.after' - | 'agent.turn.start' - | 'agent.step.before' - | 'agent.turn.stop' - | 'agent.turn.end' - | 'agent.subagent.spawn.before' - | 'agent.subagent.spawn.after' - | 'agent.subagent.complete'; - -export interface HookDecision { - decision?: 'allow' | 'deny' | 'ask' | 'continue'; - reason?: string; - injection?: string; - modifiedInput?: Record; - modifiedOutput?: unknown; -} +import type { HookDecision } from '../contracts/hooks.js'; export type ObserverHandler = ( payload: Record @@ -47,15 +20,3 @@ export interface HandlerEntry { export type ProjectPath = string; export type SessionId = string; export type HookName = string; - -export interface UserHookConfig { - name: string; - description?: string; - point: HookPoint; - type: 'observer' | 'decision'; - command: string; - args?: string[]; - env?: Record; - priority?: number; - enabled: boolean; -} diff --git a/packages/codingcode/src/layer.ts b/packages/codingcode/src/layer.ts index 15cba804..60b62a6b 100644 --- a/packages/codingcode/src/layer.ts +++ b/packages/codingcode/src/layer.ts @@ -1,4 +1,4 @@ -import { Layer, Effect, ManagedRuntime } from 'effect'; +import { Layer, ManagedRuntime } from 'effect'; import { HookLayer } from './hooks/hooks.js'; import { RulesLayer } from './rules/rules.js'; import { SkillLayer } from './skills/skills.js'; @@ -14,107 +14,9 @@ import { ContextLayer } from './context/context.js'; import { MemoryLayer } from './memory/memory.js'; import { AgentLayer } from './agent/agent.js'; import { ToolEnvLayer } from './agent/tool-env.js'; -import { ToolCatalogLayer } from './agent/tool-catalog.js'; import { SubagentRunnerLayer } from './subagent/subagent.js'; import { SchedulerLayer } from './scheduler/scheduler.js'; -import { WorkspaceService } from './core/workspace.js'; - -import { HookService } from './hooks/port.js'; -import { RulesService } from './rules/port.js'; -import { SkillService } from './skills/port.js'; -import { LLMFactoryService } from './llm/port.js'; -import { McpService } from './mcp/port.js'; -import { CheckpointService } from './checkpoint/port.js'; -import { ApprovalService } from './approval/port.js'; -import { ApprovalWaitService } from './approval/wait-port.js'; -import { TodoService } from './todo/port.js'; -import { SessionService } from './session/port.js'; -import { ToolExecutorService } from './tools/port.js'; -import { ContextService } from './context/port.js'; -import { MemoryService } from './memory/port.js'; - -import { - SessionPort, ToolExecutorPort, CheckpointPort, HookPort, - ApprovalPort, SkillPort, McpPort, ContextPort, MemoryPort, - LlmPort, RulesPort, TodoPort, -} from './agent/deps.js'; - -// adapter layers: map full services to agent's narrow ports -const AgentSessionAdapter = Layer.effect(SessionPort, Effect.gen(function* () { - const s = yield* SessionService; - return { - load: s.load.bind(s), create: s.create.bind(s), - recordUser: s.recordUser.bind(s), recordSystem: s.recordSystem.bind(s), recordAssistant: s.recordAssistant.bind(s), - recordToolResult: s.recordToolResult.bind(s), - setPermissionMode: s.setPermissionMode.bind(s), - setActiveProfile: s.setActiveProfile.bind(s), - }; -})); - -const AgentToolExecutorAdapter = Layer.effect(ToolExecutorPort, Effect.gen(function* () { - const e = yield* ToolExecutorService; - return { executeBatch: e.executeBatch.bind(e) }; -})); - -const AgentCheckpointAdapter = Layer.effect(CheckpointPort, Effect.gen(function* () { - const c = yield* CheckpointService; - return { snapshotBaseline: c.snapshotBaseline.bind(c), snapshotFinal: c.snapshotFinal.bind(c) }; -})); - -const AgentHookAdapter = Layer.effect(HookPort, Effect.gen(function* () { - const h = yield* HookService; - return { emit: h.emit.bind(h), emitDecision: h.emitDecision.bind(h), disposeSession: h.disposeSession.bind(h) }; -})); - -const AgentApprovalAdapter = Layer.effect(ApprovalPort, Effect.gen(function* () { - const a = yield* ApprovalService; - return { evaluate: a.evaluate.bind(a) }; -})); - -const AgentSkillAdapter = Layer.effect(SkillPort, Effect.gen(function* () { - const s = yield* SkillService; - return { extractSkill: s.extractSkill.bind(s) }; -})); - -const AgentMcpAdapter = Layer.effect(McpPort, Effect.gen(function* () { - const m = yield* McpService; - return { listProjectMcpTools: m.listProjectMcpTools.bind(m), syncConnections: m.syncConnections.bind(m) }; -})); - -const AgentContextAdapter = Layer.effect(ContextPort, Effect.gen(function* () { - const c = yield* ContextService; - return { - willCompact: c.willCompact.bind(c), - assemblePayload: c.assemblePayload.bind(c), - }; -})); - -const AgentMemoryAdapter = Layer.effect(MemoryPort, Effect.gen(function* () { - const m = yield* MemoryService; - return { loadMemoryForPrompt: m.loadMemoryForPrompt.bind(m), flushSessionToMemory: m.flushSessionToMemory.bind(m) }; -})); - -const AgentLlmAdapter = Layer.effect(LlmPort, Effect.gen(function* () { - const f = yield* LLMFactoryService; - return { getLLMClient: f.getLLMClient.bind(f) }; -})); - -const AgentRulesAdapter = Layer.effect(RulesPort, Effect.gen(function* () { - const r = yield* RulesService; - return { getAllRules: r.getAllRules.bind(r), evictProjectRules: r.evictProjectRules.bind(r) }; -})); - -const AgentTodoAdapter = Layer.effect(TodoPort, Effect.gen(function* () { - const t = yield* TodoService; - return { read: t.read.bind(t) }; -})); - -const AgentDepsAdapter = Layer.mergeAll( - AgentSessionAdapter, AgentToolExecutorAdapter, AgentCheckpointAdapter, - AgentHookAdapter, AgentApprovalAdapter, AgentSkillAdapter, AgentMcpAdapter, - AgentContextAdapter, AgentMemoryAdapter, AgentLlmAdapter, AgentRulesAdapter, - AgentTodoAdapter, -); +import { WorkspaceService } from './workspace/workspace.js'; // base layers const InfraLayer = Layer.mergeAll( @@ -123,21 +25,21 @@ const InfraLayer = Layer.mergeAll( const LlmWithDeps = LlmLayer.pipe(Layer.provide(WorkspaceService.Default)); const ApprovalWithDeps = ApprovalLayer.pipe(Layer.provide(Layer.mergeAll(HookLayer, ApprovalWaitLayer))); -const ToolExecutorWithDeps = ToolExecutorLayer.pipe(Layer.provide(Layer.mergeAll(HookLayer, ApprovalWithDeps))); +const ToolExecutorWithDeps = ToolExecutorLayer.pipe( + Layer.provide(Layer.mergeAll(HookLayer, ApprovalWithDeps)) +); const ContextWithDeps = ContextLayer.pipe(Layer.provide(Layer.mergeAll(SessionLayer, LlmWithDeps))); const MemoryWithDeps = MemoryLayer.pipe(Layer.provide(LlmWithDeps)); -// agent deps adapters wrap concrete services, so provide them first -const AgentDepsWithDeps = AgentDepsAdapter.pipe( - Layer.provide(Layer.mergeAll( - InfraLayer, SessionLayer, ToolExecutorWithDeps, ApprovalWithDeps, - ContextWithDeps, MemoryWithDeps, CheckpointLayer, LlmWithDeps, - )) +// agent 直接消费的宽服务集合 +const AgentServiceLayers = Layer.mergeAll( + InfraLayer, SessionLayer, ToolExecutorWithDeps, ApprovalWithDeps, + ContextWithDeps, MemoryWithDeps, CheckpointLayer, LlmWithDeps, ); // agent with deps const AgentWithDeps = AgentLayer.pipe( - Layer.provide(Layer.mergeAll(AgentDepsWithDeps, ToolEnvLayer, ToolCatalogLayer)) + Layer.provide(Layer.mergeAll(AgentServiceLayers, ToolEnvLayer)) ); // subagent runner (depends on agent) @@ -157,5 +59,5 @@ export const AppLayer = Layer.mergeAll( SchedulerLayer, ); -export const createAppRuntime = () => ManagedRuntime.make(AppLayer as any); +export const createAppRuntime = () => ManagedRuntime.make(AppLayer); export type AppRuntime = ManagedRuntime.ManagedRuntime; diff --git a/packages/codingcode/src/llm/client.ts b/packages/codingcode/src/llm/client.ts deleted file mode 100644 index 6c90db44..00000000 --- a/packages/codingcode/src/llm/client.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Effect } from 'effect'; -import type { AgentError } from '../core/error.js'; -import type { LLMRequest, LLMResponse, LLMStreamPart, ModelInfo } from './types.js'; - -export interface LLMClient { - complete(req: LLMRequest, signal?: AbortSignal): Effect.Effect; - /** 产出 SDK 流部件;失败时在迭代中抛出 AgentError */ - completeStream(req: LLMRequest, signal?: AbortSignal): AsyncIterable; - readonly modelInfo: ModelInfo; -} diff --git a/packages/codingcode/src/llm/llm-resolver.ts b/packages/codingcode/src/llm/llm-resolver.ts index 35481ff7..62778ed1 100644 --- a/packages/codingcode/src/llm/llm-resolver.ts +++ b/packages/codingcode/src/llm/llm-resolver.ts @@ -1,7 +1,7 @@ import { Effect } from 'effect'; import { AgentError } from '../core/error.js'; import { LLMFactoryService } from './port.js'; -import type { LLMClient } from './client.js'; +import type { LLMClient } from '../contracts/provider.js'; export function resolveLLM( target: string | null | undefined, diff --git a/packages/codingcode/src/llm/llm.ts b/packages/codingcode/src/llm/llm.ts index abe8431e..31a3d521 100644 --- a/packages/codingcode/src/llm/llm.ts +++ b/packages/codingcode/src/llm/llm.ts @@ -2,8 +2,8 @@ import { readFileSync, existsSync } from 'fs'; import { resolve } from 'path'; import { Layer, Effect } from 'effect'; import { AgentError } from '../core/error.js'; -import { WorkspaceService } from '../core/workspace.js'; -import type { LLMClient } from './client.js'; +import { WorkspaceService } from '../workspace/workspace.js'; +import type { LLMClient, SelectableModel } from '../contracts/provider.js'; import { OpenAIProvider } from './providers/openai.js'; import { DeepSeekProvider } from './providers/deepseek.js'; import { updateActiveModel } from '@codingcode/infra/config'; @@ -28,15 +28,8 @@ interface ProviderCatalog { providers: ProviderEntry[]; } -export interface SelectableModel { - id: string; - provider: string; - driver: string; - name: string; - model: string; - base_url: string; - api_key_env: string; - context_window: number; +interface ProviderCatalog { + providers: ProviderEntry[]; } function flattenModels(cat: ProviderCatalog): SelectableModel[] { diff --git a/packages/codingcode/src/llm/port.ts b/packages/codingcode/src/llm/port.ts index e17ce6ca..44a3d35c 100644 --- a/packages/codingcode/src/llm/port.ts +++ b/packages/codingcode/src/llm/port.ts @@ -1,11 +1,7 @@ import { Context } from 'effect'; import type { Effect } from 'effect'; import type { AgentError } from '../core/error.js'; -import type { LLMClient } from './client.js'; - -export interface ModelDescriptor { id: string; name: string; context_window?: number } -export interface ProviderEntry { name: string; driver: string; base_url: string; api_key_env: string; default_model: string; models: ModelDescriptor[] } -export interface SelectableModel { id: string; provider: string; driver: string; name: string; model: string; base_url: string; api_key_env: string; context_window: number } +import type { LLMClient, SelectableModel } from '../contracts/provider.js'; export interface LLMFactoryShape { listModels(): Effect.Effect; diff --git a/packages/codingcode/src/llm/providers/deepseek.ts b/packages/codingcode/src/llm/providers/deepseek.ts index 001777d8..4b26e4a0 100644 --- a/packages/codingcode/src/llm/providers/deepseek.ts +++ b/packages/codingcode/src/llm/providers/deepseek.ts @@ -3,9 +3,9 @@ import type { LanguageModelV3 } from '@ai-sdk/provider'; import { Effect } from 'effect'; import type { AgentError } from '../../core/error.js'; import { mapLlmError } from '../errors.js'; -import type { LLMClient } from '../client.js'; -import type { LLMRequest, LLMResponse, LLMStreamPart } from '../types.js'; -import type { SelectableModel } from '../port.js'; +import type { LLMClient } from '../../contracts/provider.js'; +import type { LLMRequest, LLMResponse, LLMStreamPart } from '../../contracts/provider.js'; +import type { SelectableModel } from '../../contracts/provider.js'; import { convertMessages, convertTools, toTokenUsage } from './shared.js'; export class DeepSeekProvider implements LLMClient { diff --git a/packages/codingcode/src/llm/providers/openai.ts b/packages/codingcode/src/llm/providers/openai.ts index cdad0862..b33faaa7 100644 --- a/packages/codingcode/src/llm/providers/openai.ts +++ b/packages/codingcode/src/llm/providers/openai.ts @@ -3,9 +3,9 @@ import type { LanguageModelV3 } from '@ai-sdk/provider'; import { Effect } from 'effect'; import type { AgentError } from '../../core/error.js'; import { mapLlmError } from '../errors.js'; -import type { LLMClient } from '../client.js'; -import type { LLMRequest, LLMResponse, LLMStreamPart } from '../types.js'; -import type { SelectableModel } from '../port.js'; +import type { LLMClient } from '../../contracts/provider.js'; +import type { LLMRequest, LLMResponse, LLMStreamPart } from '../../contracts/provider.js'; +import type { SelectableModel } from '../../contracts/provider.js'; import { convertMessages, convertTools, toTokenUsage } from './shared.js'; export class OpenAIProvider implements LLMClient { diff --git a/packages/codingcode/src/llm/providers/shared.ts b/packages/codingcode/src/llm/providers/shared.ts index 3c9cd674..df63b7c9 100644 --- a/packages/codingcode/src/llm/providers/shared.ts +++ b/packages/codingcode/src/llm/providers/shared.ts @@ -1,5 +1,5 @@ import { jsonSchema, type LanguageModelUsage, type ModelMessage } from 'ai'; -import type { TokenUsage } from '../../core/types.js'; +import type { TokenUsage } from '../../contracts/types.js'; export function convertMessages( messages: Array<{ role: string; content: string; tool_calls?: unknown[]; tool_call_id?: string }> diff --git a/packages/codingcode/src/mcp/client.ts b/packages/codingcode/src/mcp/client.ts index 3ddebf0c..fb7860a7 100644 --- a/packages/codingcode/src/mcp/client.ts +++ b/packages/codingcode/src/mcp/client.ts @@ -2,7 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { Effect, TSemaphore, STM } from 'effect'; -import type { McpServerConfig } from './types.js'; +import type { McpServerConfig } from '../contracts/mcp.js'; export class McpError extends Error { constructor( diff --git a/packages/codingcode/src/mcp/config.ts b/packages/codingcode/src/mcp/config.ts index ffde3c4d..9a13bf65 100644 --- a/packages/codingcode/src/mcp/config.ts +++ b/packages/codingcode/src/mcp/config.ts @@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; -import type { McpServerConfig } from './types.js'; +import type { McpServerConfig } from '../contracts/mcp.js'; import { createDisabledStore } from '@codingcode/infra/disabled-store'; function resolveEnvVars(value: unknown): unknown { diff --git a/packages/codingcode/src/mcp/mcp.ts b/packages/codingcode/src/mcp/mcp.ts index 4a74bffd..4777f3f5 100644 --- a/packages/codingcode/src/mcp/mcp.ts +++ b/packages/codingcode/src/mcp/mcp.ts @@ -1,10 +1,8 @@ import { Effect, Layer } from 'effect'; -import { z } from 'zod'; import { resolveMcpConfig, resolveMcpDisabled } from './config.js'; import { McpClient } from './client.js'; import { McpService } from './port.js'; -import type { McpServerConfig, McpStatus } from './types.js'; -import type { ToolDefinition } from '../tools/types.js'; +import type { McpServerConfig, McpStatus, McpToolSpec } from '../contracts/mcp.js'; import { createLogger } from '@codingcode/infra/logger'; import { AgentError } from '../core/error.js'; @@ -242,21 +240,22 @@ export const McpLayer = Layer.effect(McpService, Effect.sync(() => { return entry ? [...entry.toolNames] : []; }, - listProjectMcpTools: (projectPath: string): ToolDefinition[] => { - const projectClients = clientsByProject.get(projectPath); - if (!projectClients) return []; - const tools: ToolDefinition[] = []; - for (const [serverName, entry] of projectClients) { - for (const raw of entry.rawTools) { - tools.push( - mcpToolToDefinition(serverName, raw, entry.client, () => - isDisabled(projectPath, serverName) - ) - ); + listProjectMcpTools: (projectPath: string): Effect.Effect => + Effect.sync(() => { + const projectClients = clientsByProject.get(projectPath); + if (!projectClients) return []; + const specs: McpToolSpec[] = []; + for (const [serverName, entry] of projectClients) { + for (const raw of entry.rawTools) { + specs.push( + mcpToolToSpec(serverName, raw, entry.client, () => + isDisabled(projectPath, serverName) + ) + ); + } } - } - return tools; - }, + return specs; + }), status: (projectPath: string): Effect.Effect => Effect.sync(() => { @@ -325,16 +324,17 @@ function namespacedName(serverName: string, toolName: string): string { return `${serverName}:${toolName}`; } -function mcpToolToDefinition( +function mcpToolToSpec( serverName: string, mcpTool: { name: string; description: string; inputSchema: Record }, client: McpClient, isDisabledFn: () => boolean -): ToolDefinition { +): McpToolSpec { return { - name: `${serverName}:${mcpTool.name}`, - description: `[MCP:${serverName}] ${mcpTool.description || mcpTool.name}`, - parameters: z.fromJSONSchema(mcpTool.inputSchema), + server: serverName, + name: mcpTool.name, + description: mcpTool.description, + inputSchema: mcpTool.inputSchema, execute: (args) => { if (isDisabledFn()) return Effect.fail( diff --git a/packages/codingcode/src/mcp/port.ts b/packages/codingcode/src/mcp/port.ts index 6caac1b7..7ded6c98 100644 --- a/packages/codingcode/src/mcp/port.ts +++ b/packages/codingcode/src/mcp/port.ts @@ -1,14 +1,13 @@ import { Context } from 'effect'; import type { Effect } from 'effect'; -import type { ToolDefinition } from '../tools/types.js'; -import type { McpStatus } from './types.js'; +import type { McpStatus, McpToolSpec } from '../contracts/mcp.js'; export interface McpShape { syncConnections(projectPath: string): Effect.Effect; connectServers(projectPath: string, sessionId: string, names: string[]): Effect.Effect; disconnectServers(projectPath: string, sessionId: string, names: string[]): Effect.Effect; getServerToolNames(projectPath: string, name: string): string[]; - listProjectMcpTools(projectPath: string): ToolDefinition[]; + listProjectMcpTools(projectPath: string): Effect.Effect; status(projectPath: string): Effect.Effect; disable(projectPath: string, name: string): Effect.Effect; enable(projectPath: string, name: string): Effect.Effect; diff --git a/packages/codingcode/src/memory/extractor.ts b/packages/codingcode/src/memory/extractor.ts index 40970b17..b3743c63 100644 --- a/packages/codingcode/src/memory/extractor.ts +++ b/packages/codingcode/src/memory/extractor.ts @@ -1,4 +1,4 @@ -import type { LLMClient } from '../llm/client.js'; +import type { LLMClient } from '../contracts/provider.js'; export async function extractMemory(opts: { currentMemory: string; diff --git a/packages/codingcode/src/memory/memory.ts b/packages/codingcode/src/memory/memory.ts index 27ecdfd2..1626cd83 100644 --- a/packages/codingcode/src/memory/memory.ts +++ b/packages/codingcode/src/memory/memory.ts @@ -1,7 +1,7 @@ import { Layer, Effect } from 'effect'; -import type { LLMClient } from '../llm/client.js'; +import type { LLMClient } from '../contracts/provider.js'; import { readTranscript } from '../session/file-ops.js'; -import type { SessionEvent } from '../session/types.js'; +import type { SessionEvent } from '../contracts/session.js'; import { readMemoryFile, resolveMemoryPath, diff --git a/packages/codingcode/src/memory/port.ts b/packages/codingcode/src/memory/port.ts index e8bc034e..e64faa43 100644 --- a/packages/codingcode/src/memory/port.ts +++ b/packages/codingcode/src/memory/port.ts @@ -1,5 +1,5 @@ import { Context } from 'effect'; -import type { LLMClient } from '../llm/client.js'; +import type { LLMClient } from '../contracts/provider.js'; export interface MemoryShape { getMemoryEnabled(): boolean; diff --git a/packages/codingcode/src/server/handler.ts b/packages/codingcode/src/server/handler.ts index 486ca5c6..7eb52828 100644 --- a/packages/codingcode/src/server/handler.ts +++ b/packages/codingcode/src/server/handler.ts @@ -2,8 +2,8 @@ import type { Context } from 'hono'; import { Effect, ManagedRuntime } from 'effect'; import { ApprovalWaitService } from '../approval/wait-port.js'; import { AgentError } from '../core/error.js'; -import type { FrameBody } from '../core/frame.js'; -import { createFrameAssembler, encodeFrame } from '../core/frame-io.js'; +import type { FrameBody } from '../contracts/frame.js'; +import { createFrameAssembler, encodeFrame } from '../contracts/frame-io.js'; type ManagedRt = ManagedRuntime.ManagedRuntime; diff --git a/packages/codingcode/src/server/routes/automations.ts b/packages/codingcode/src/server/routes/automations.ts index 996e178b..94876938 100644 --- a/packages/codingcode/src/server/routes/automations.ts +++ b/packages/codingcode/src/server/routes/automations.ts @@ -2,7 +2,7 @@ import type { Hono } from 'hono'; import { Effect, ManagedRuntime } from 'effect'; import { SchedulerService } from '../../scheduler/port.js'; import { errorResponse } from '../util.js'; -import { NotFoundError } from '../../core/error.js'; +import { NotFoundError } from '../../contracts/error.js'; import type { CreateAutomationInput, UpdateAutomationInput } from '../../scheduler/types.js'; type ManagedRt = ManagedRuntime.ManagedRuntime; diff --git a/packages/codingcode/src/server/routes/messages.ts b/packages/codingcode/src/server/routes/messages.ts index 5f883aad..d022d78a 100644 --- a/packages/codingcode/src/server/routes/messages.ts +++ b/packages/codingcode/src/server/routes/messages.ts @@ -1,7 +1,7 @@ import type { Hono } from 'hono'; import { Effect, ManagedRuntime } from 'effect'; import { AgentService } from '../../agent/port.js'; -import { WorkspaceService } from '../../core/workspace.js'; +import { WorkspaceService } from '../../workspace/workspace.js'; import { errorResponse } from '../util.js'; import { createSseHandler } from '../handler.js'; diff --git a/packages/codingcode/src/server/routes/sessions.ts b/packages/codingcode/src/server/routes/sessions.ts index ed6d55af..76865716 100644 --- a/packages/codingcode/src/server/routes/sessions.ts +++ b/packages/codingcode/src/server/routes/sessions.ts @@ -2,20 +2,21 @@ import type { Hono } from 'hono'; import { Effect, ManagedRuntime } from 'effect'; import { existsSync, readFileSync, readdirSync, statSync } from 'fs'; import { join } from 'path'; -import type { SessionStoreState } from '../../session/types.js'; -import type { ProfileName } from '../../core/types.js'; +import type { SessionStoreState } from '../../contracts/session.js'; +import type { ProfileName } from '../../contracts/types.js'; import { SessionService } from '../../session/port.js'; import { computePaths } from '../../core/path.js'; import { ContextService } from '../../context/port.js'; import { estimatePromptTokensFrom } from '../../context/context.js'; import { CheckpointService } from '../../checkpoint/port.js'; -import { WorkspaceService } from '../../core/workspace.js'; +import { WorkspaceService } from '../../workspace/workspace.js'; import { LLMFactoryService } from '../../llm/port.js'; -import type { LLMClient } from '../../llm/client.js'; +import type { LLMClient } from '../../contracts/provider.js'; import { errorResponse } from '../util.js'; import { encodeProjectPath, getProjectBaseDir } from '../../core/path.js'; import { AVAILABLE_PROFILES, isAgentProfileName } from '../../agent/profile.js'; -import { isPermissionMode, type PermissionMode } from '../../approval/types.js'; +import { isPermissionMode } from '../../approval/types.js'; +import type { PermissionMode } from '../../contracts/permission.js'; type ManagedRt = ManagedRuntime.ManagedRuntime; diff --git a/packages/codingcode/src/server/routes/settings.ts b/packages/codingcode/src/server/routes/settings.ts index 29e98fca..e8ab3dda 100644 --- a/packages/codingcode/src/server/routes/settings.ts +++ b/packages/codingcode/src/server/routes/settings.ts @@ -1,10 +1,10 @@ import type { Hono } from 'hono'; import { Effect, ManagedRuntime } from 'effect'; import { SkillService } from '../../skills/port.js'; -import { WorkspaceService, isGlobalCwd } from '../../core/workspace.js'; -import { AlreadyExistsError, NotFoundError } from '../../core/error.js'; -import type { McpServerConfig } from '../../mcp/types.js'; -import type { UserHookConfig } from '../../hooks/types.js'; +import { WorkspaceService, isGlobalCwd } from '../../workspace/workspace.js'; +import { AlreadyExistsError, NotFoundError } from '../../contracts/error.js'; +import type { McpServerConfig } from '../../contracts/mcp.js'; +import type { UserHookConfig } from '../../contracts/hooks.js'; import { loadMcpConfig, writeMcpConfig, diff --git a/packages/codingcode/src/session/file-ops.ts b/packages/codingcode/src/session/file-ops.ts index 335e0eed..e74abfd9 100644 --- a/packages/codingcode/src/session/file-ops.ts +++ b/packages/codingcode/src/session/file-ops.ts @@ -13,10 +13,14 @@ import { } from 'fs'; import { homedir } from 'os'; import { join, dirname } from 'path'; -import { getProjectBaseDir } from '../core/path.js'; -import { sessionJsonlPathFromCwd } from '../core/path.js'; -import type { PermissionMode } from '../approval/types.js'; -import type { SessionEvent, SessionMetaEvent, SessionIndex } from './types.js'; +import { getProjectBaseDir, computePaths } from '../core/path.js'; +import type { PermissionMode } from '../contracts/permission.js'; +import type { SessionEvent, SessionMetaEvent, SessionIndex } from '../contracts/session.js'; + +/** session 的 jsonl 转录路径:由 cwd + sessionId 推出。 */ +export function sessionJsonlPathFromCwd(cwd: string, sessionId: string): string { + return computePaths(cwd, sessionId).transcriptPath; +} export function ensureDirs(transcriptPath: string): void { const codingcodeDir = join(homedir(), '.codingcode'); diff --git a/packages/codingcode/src/session/port.ts b/packages/codingcode/src/session/port.ts index e6ee3c35..7a9735dd 100644 --- a/packages/codingcode/src/session/port.ts +++ b/packages/codingcode/src/session/port.ts @@ -1,59 +1,12 @@ import { Context } from 'effect'; import type { Effect } from 'effect'; import type { AgentError } from '../core/error.js'; -import type { - AssistantEvent, - RollbackEvent, - SessionEvent, - SessionIndex, - SessionStoreState, - SummaryEvent, - TokenUsage, - ToolResultEvent, - UserEvent, -} from './types.js'; -import type { ProfileName } from '../core/types.js'; -import type { PermissionMode } from '../approval/types.js'; - -export type UITurnItem = - | { id: string; type: 'message'; role: 'user' | 'assistant'; content: string; partial?: boolean } - | { - id: string; - type: 'tool_call'; - name: string; - args: Record; - status: 'pending' | 'approved' | 'rejected' | 'running'; - } - | { - id: string; - type: 'tool_result'; - callId: string; - name: string; - output: string; - exitCode?: number; - filePath?: string; - diff?: string; - insertions?: number; - deletions?: number; - } - | { - id: string; - type: 'summary'; - content: string; - startTurnId: number; - endTurnId: number; - } - | { id: string; type: 'reasoning'; content: string; isVisible: boolean } - | { id: string; type: 'error'; message: string; code?: string }; - -export interface UITurn { - id: string; - items: UITurnItem[]; - status: 'running' | 'completed' | 'error'; -} +import type { AssistantEvent, RollbackEvent, SessionCreateOptions, SessionEvent, SessionIndex, SessionStoreState, SummaryEvent, ToolResultEvent, UITurn, UserEvent } from '../contracts/session.js'; +import type { TokenUsage, ProfileName } from '../contracts/types.js'; +import type { PermissionMode } from '../contracts/permission.js'; export interface SessionShape { - create(cwd: string, options: { model: string; activeProfile: ProfileName; permissionMode: PermissionMode }, opts?: { parentSessionId?: string; agentName?: string }): Effect.Effect; + create(cwd: string, options: SessionCreateOptions, opts?: { parentSessionId?: string; agentName?: string }): Effect.Effect; load(cwd: string, sessionId: string): Effect.Effect; deleteSession(sessionId: string, cwd: string): Effect.Effect; forkSession(state: SessionStoreState, atTurnId: number): Effect.Effect; @@ -74,19 +27,3 @@ export interface SessionShape { } export class SessionService extends Context.Tag('Session')() {} - -// direct/sessions.ts 实际使用的消费视图,编译期锁定真实耦合面 -export type SessionStorePort = Pick< - SessionShape, - | 'create' - | 'load' - | 'deleteSession' - | 'forkSession' - | 'listSessions' - | 'readUITurns' - | 'setActiveProfile' - | 'setPermissionMode' ->; - -// direct/agent-runtime.ts 与 direct/settings.ts 只读/只写权限模式 -export type SessionStatePort = Pick; diff --git a/packages/codingcode/src/session/session.ts b/packages/codingcode/src/session/session.ts index 9c6bffb9..3a1c75c6 100644 --- a/packages/codingcode/src/session/session.ts +++ b/packages/codingcode/src/session/session.ts @@ -4,23 +4,10 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; import { join, dirname } from 'path'; import { AgentError } from '../core/error.js'; import { encodeProjectPath } from '../core/path.js'; -import type { - SessionMetaEvent, - UserEvent, - AssistantEvent, - ToolResultEvent, - SummaryEvent, - RollbackEvent, - SessionIndex, - TokenUsage, - SessionEvent, - SessionStoreState, - CompactEvent, -} from './types.js'; -import type { ProfileName } from '../core/types.js'; -import type { PermissionMode } from '../approval/types.js'; +import type { SessionMetaEvent, UserEvent, AssistantEvent, ToolResultEvent, SummaryEvent, RollbackEvent, SessionIndex, SessionEvent, SessionStoreState, CompactEvent, UITurn } from '../contracts/session.js'; +import type { TokenUsage, ProfileName } from '../contracts/types.js'; +import type { PermissionMode } from '../contracts/permission.js'; import { SessionService } from './port.js'; -import type { UITurn } from './port.js'; import { ensureDirs, readHistory, @@ -33,8 +20,9 @@ import { truncateTitle, findFirstUserContent, deleteSession as deleteSessionImpl, + sessionJsonlPathFromCwd, } from './file-ops.js'; -import { computePaths, sessionJsonlPathFromCwd } from '../core/path.js'; +import { computePaths } from '../core/path.js'; function pathsFromState(state: SessionStoreState) { return computePaths(state.cwd, state.sessionId, state.parentSessionId); diff --git a/packages/codingcode/src/skills/loader.ts b/packages/codingcode/src/skills/loader.ts index 6c142b44..f4dc3199 100644 --- a/packages/codingcode/src/skills/loader.ts +++ b/packages/codingcode/src/skills/loader.ts @@ -1,5 +1,5 @@ import { basename, resolve } from 'path'; -import type { Skill } from './types.js'; +import type { Skill } from '../contracts/skill.js'; import { readSkillFrontMatter } from './source.js'; export function loadSkill(dirPath: string): Skill | null { diff --git a/packages/codingcode/src/skills/port.ts b/packages/codingcode/src/skills/port.ts index 38be6e3c..5fbea5d8 100644 --- a/packages/codingcode/src/skills/port.ts +++ b/packages/codingcode/src/skills/port.ts @@ -1,6 +1,6 @@ import { Context } from 'effect'; import type { Effect } from 'effect'; -import type { Skill } from './types.js'; +import type { Skill } from '../contracts/skill.js'; export interface SkillShape { getAll(projectPath: string): Effect.Effect; diff --git a/packages/codingcode/src/skills/skills.ts b/packages/codingcode/src/skills/skills.ts index 608a49fc..7cfe4ff5 100644 --- a/packages/codingcode/src/skills/skills.ts +++ b/packages/codingcode/src/skills/skills.ts @@ -1,7 +1,7 @@ import { Layer, Effect } from 'effect'; import { discoverSkillDirs } from './source.js'; import { loadSkill } from './loader.js'; -import type { Skill } from './types.js'; +import type { Skill } from '../contracts/skill.js'; import { SkillService } from './port.js'; export const SkillLayer = Layer.effect(SkillService, Effect.gen(function* () { diff --git a/packages/codingcode/src/subagent/port.ts b/packages/codingcode/src/subagent/port.ts index 27d0bb1e..282f8c8c 100644 --- a/packages/codingcode/src/subagent/port.ts +++ b/packages/codingcode/src/subagent/port.ts @@ -1,6 +1,6 @@ import { Context } from 'effect'; import type { Effect } from 'effect'; -import type { FrameBody } from '../core/frame.js'; +import type { FrameBody } from '../contracts/frame.js'; import type { AgentError } from '../core/error.js'; import type { Result } from '../core/result.js'; @@ -8,18 +8,22 @@ export interface RunSubagentOptions { sessionId?: string; cwd: string; signal?: AbortSignal; - activeProfile?: import('../core/types.js').ProfileName; - permissionMode?: import('../approval/types.js').PermissionMode; + activeProfile?: import('../contracts/types.js').ProfileName; + permissionMode?: import('../contracts/permission.js').PermissionMode; model?: string; parentSessionId?: string; agentName?: string; } export interface SubagentRunnerShape { - runSubagent(input: string, opts: RunSubagentOptions): Effect.Effect<{ - stream: AsyncGenerator, unknown>; - sessionId: string; - }>; + runSubagent(input: string, opts: RunSubagentOptions): Effect.Effect< + { + stream: AsyncGenerator, unknown>; + sessionId: string; + }, + // E:沿用 agent.runTurn 的错误通道(原先声明 never,靠 runTurn 的 as any 掩盖) + AgentError + >; } export class SubagentRunnerService extends Context.Tag('SubagentRunner')() {} diff --git a/packages/codingcode/src/subagent/subagent.ts b/packages/codingcode/src/subagent/subagent.ts index 5372e278..5a691270 100644 --- a/packages/codingcode/src/subagent/subagent.ts +++ b/packages/codingcode/src/subagent/subagent.ts @@ -2,7 +2,7 @@ import { Layer, Effect } from 'effect'; import { SubagentRunnerService } from './port.js'; import type { RunSubagentOptions } from './port.js'; import { AgentService } from '../agent/port.js'; -import type { FrameBody } from '../core/frame.js'; +import type { FrameBody } from '../contracts/frame.js'; import type { Result } from '../core/result.js'; export const SubagentRunnerLayer = Layer.effect( diff --git a/packages/codingcode/src/todo/port.ts b/packages/codingcode/src/todo/port.ts index cf7341b7..c05a9d00 100644 --- a/packages/codingcode/src/todo/port.ts +++ b/packages/codingcode/src/todo/port.ts @@ -1,8 +1,5 @@ import { Context } from 'effect'; -import type { TodoItem } from '../core/types.js'; - -export type { TodoItem }; -export type Todo = TodoItem; +import type { TodoItem } from '../contracts/types.js'; export interface TodoCounts { pending: number; @@ -11,8 +8,8 @@ export interface TodoCounts { } export interface TodoShape { - read(sessionId: string): Todo[]; - write(sessionId: string, plan: Todo[]): void; + read(sessionId: string): TodoItem[]; + write(sessionId: string, plan: TodoItem[]): void; reset(): void; } @@ -21,7 +18,7 @@ export class TodoService extends Context.Tag('Todo')() { export const TODO_MAX_ITEMS = 20; export const TODO_MAX_STEP_LEN = 60; -export function countByStatus(plan: Todo[]): TodoCounts { +export function countByStatus(plan: TodoItem[]): TodoCounts { const c: TodoCounts = { pending: 0, in_progress: 0, completed: 0 }; for (const t of plan) c[t.status]++; return c; diff --git a/packages/codingcode/src/todo/todo.ts b/packages/codingcode/src/todo/todo.ts index fffa8450..e1e8836b 100644 --- a/packages/codingcode/src/todo/todo.ts +++ b/packages/codingcode/src/todo/todo.ts @@ -1,11 +1,12 @@ import { Layer, Effect } from 'effect'; import { TodoService } from './port.js'; +import type { TodoItem } from '../contracts/types.js'; export const TodoLayer = Layer.effect(TodoService, Effect.sync(() => { - const store = new Map(); + const store = new Map(); return { read: (sessionId: string) => store.get(sessionId) ?? [], - write: (sessionId: string, plan: import('./port.js').Todo[]) => { store.set(sessionId, plan); }, + write: (sessionId: string, plan: TodoItem[]) => { store.set(sessionId, plan); }, reset: () => { store.clear(); }, }; })); diff --git a/packages/codingcode/src/tools/catalog.ts b/packages/codingcode/src/tools/catalog.ts index 509bd0db..f4648986 100644 --- a/packages/codingcode/src/tools/catalog.ts +++ b/packages/codingcode/src/tools/catalog.ts @@ -1,6 +1,10 @@ +import { z } from 'zod'; +import type { Effect } from 'effect'; import type { ToolDefinition } from './types.js'; -import type { ToolDescription } from '../core/types.js'; -import type { ToolLookup } from './port.js'; +import type { ToolDescription } from '../contracts/types.js'; +import type { ToolExecCtx, ToolLookup } from '../contracts/tool.js'; +import type { McpToolSpec } from '../contracts/mcp.js'; +import type { AgentError } from '../core/error.js'; import { ToolRegistry } from './registry.js'; import { readFileTool } from './domains/fs/read.js'; import { writeFileTool } from './domains/fs/write.js'; @@ -14,8 +18,6 @@ import { todoWriteTool } from './domains/self/todo-write.js'; import { dispatchAgentTool } from './domains/subagent/dispatch.js'; import { submitPlanTool } from './domains/subagent/submit-plan.js'; -// 全量静态工具表:名字 -> 工具定义。agent 只传名字名单,这里按名查表装配, -// 不感知 profile / allowedTools 的取舍(取舍由 agent 侧的名单本身决定)。 const ALL_TOOLS: ToolDefinition[] = [ readFileTool, writeFileTool, @@ -32,9 +34,18 @@ const ALL_TOOLS: ToolDefinition[] = [ const TOOLS_BY_NAME = new Map(ALL_TOOLS.map((tool) => [tool.name, tool])); +function specToDefinition(spec: McpToolSpec): ToolDefinition { + return { + name: `${spec.server}:${spec.name}`, + description: `[MCP:${spec.server}] ${spec.description || spec.name}`, + parameters: z.fromJSONSchema(spec.inputSchema), + execute: (args) => spec.execute(args as Record), + }; +} + export function createToolCatalog( toolNames: readonly string[], - mcpTools: ToolDefinition[] = [] + mcpTools: McpToolSpec[] = [] ): { tools: ToolDescription[]; lookup: ToolLookup } { const registry = new ToolRegistry(); for (const name of toolNames) { @@ -42,10 +53,21 @@ export function createToolCatalog( if (!definition) throw new Error(`Unknown tool: ${name}`); registry.register(definition); } - registry.register(...mcpTools); + registry.register(...mcpTools.map(specToDefinition)); return { tools: registry.describe(), - lookup: (name) => registry.get(name), + lookup: (name) => { + const definition = registry.get(name); + if (!definition) return undefined; + return { + name: definition.name, + parse: (args: unknown) => definition.parameters.parse(args), + // 装配点:工具声明的服务依赖(ToolDefinition)由执行器在运行时经 ToolEnv 注入, + // 故在此把 R 收敛为 never,与 ToolRunner 的可执行形态对齐 + execute: (args: unknown, ctx?: ToolExecCtx) => + definition.execute(args, ctx) as Effect.Effect, + }; + }, }; } diff --git a/packages/codingcode/src/tools/domains/self/todo-write.ts b/packages/codingcode/src/tools/domains/self/todo-write.ts index 12942003..e6f5887f 100644 --- a/packages/codingcode/src/tools/domains/self/todo-write.ts +++ b/packages/codingcode/src/tools/domains/self/todo-write.ts @@ -8,7 +8,7 @@ import { TODO_MAX_ITEMS, TODO_MAX_STEP_LEN, } from '../../../todo/port.js'; -import type { Todo } from '../../../todo/port.js'; +import type { TodoItem } from '../../../contracts/types.js'; const todoSchema = z.object({ plan: z @@ -34,7 +34,7 @@ export const todoWriteTool: ToolDefinition = { return yield* Effect.fail( new AgentError('TOOL_EXECUTION_FAILED', 'todo_write requires sessionId') ); - const { plan } = args as { plan: Todo[] }; + const { plan } = args as { plan: TodoItem[] }; todoSvc.write(sessionId, plan); const c = countByStatus(plan); return `pending=${c.pending} in_progress=${c.in_progress} completed=${c.completed}`; diff --git a/packages/codingcode/src/tools/domains/subagent/dispatch.ts b/packages/codingcode/src/tools/domains/subagent/dispatch.ts index acdf5e5e..f84c37b2 100644 --- a/packages/codingcode/src/tools/domains/subagent/dispatch.ts +++ b/packages/codingcode/src/tools/domains/subagent/dispatch.ts @@ -46,7 +46,7 @@ export const dispatchAgentTool: ToolDefinition< const { stream, sessionId: childUuid } = yield* runner.runSubagent(prompt, { cwd: projectPath, signal: ctx?.signal, - activeProfile: profile.name as any, + activeProfile: profile.name, parentSessionId: ctx?.sessionId, agentName, }); diff --git a/packages/codingcode/src/tools/port.ts b/packages/codingcode/src/tools/port.ts index a161f753..026f8b01 100644 --- a/packages/codingcode/src/tools/port.ts +++ b/packages/codingcode/src/tools/port.ts @@ -1,22 +1,18 @@ import { Context } from 'effect'; import type { Effect } from 'effect'; -import type { ToolCall } from '../core/types.js'; -import type { ToolDefinition } from './types.js'; - -export type ToolResultUnion = - | { type: 'ok'; id: string; name: string; output: string } - | { type: 'denied'; id: string; name: string; reason: string } - | { type: 'error'; id: string; name: string; output: string }; - -export type ToolLookup = (name: string) => ToolDefinition | undefined; +import type { ToolCall } from '../contracts/types.js'; +import type { McpToolSpec } from '../contracts/mcp.js'; +import type { ToolCatalog, ToolLookup, ToolResult } from '../contracts/tool.js'; export interface ToolExecutorShape { + // 按工具名 + 调用方提供的 MCP 工具装配出本轮可用的工具集 + prepare(toolNames: readonly string[], mcpTools?: McpToolSpec[]): Effect.Effect; executeBatch(toolCalls: ToolCall[], sessionId?: string, opts?: { turnId?: number; projectPath?: string; signal?: AbortSignal; toolLookup?: ToolLookup; - }): Effect.Effect; + }): Effect.Effect; } export class ToolExecutorService extends Context.Tag('ToolExecutor')() {} diff --git a/packages/codingcode/src/tools/registry.ts b/packages/codingcode/src/tools/registry.ts index e16fc6b6..adb69f55 100644 --- a/packages/codingcode/src/tools/registry.ts +++ b/packages/codingcode/src/tools/registry.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; -import type { ToolDefinition, ToolDescription } from './types.js'; +import type { ToolDescription } from '../contracts/types.js'; +import type { ToolDefinition } from './types.js'; import { canonicalizeSchema } from './utils/canonicalize-schema.js'; export class ToolRegistry { diff --git a/packages/codingcode/src/tools/tools.ts b/packages/codingcode/src/tools/tools.ts index 414ea780..f6cebdc7 100644 --- a/packages/codingcode/src/tools/tools.ts +++ b/packages/codingcode/src/tools/tools.ts @@ -1,16 +1,11 @@ import { Layer, Effect } from 'effect'; import { AgentError } from '../core/error.js'; import { HookService } from '../hooks/port.js'; -import type { ToolDefinition } from './types.js'; -import type { ToolCall } from '../core/types.js'; +import type { ToolCall } from '../contracts/types.js'; +import type { McpToolSpec } from '../contracts/mcp.js'; +import type { ToolCatalog, ToolLookup, ToolResult } from '../contracts/tool.js'; import { ToolExecutorService } from './port.js'; - -export type ToolResultUnion = - | { type: 'ok'; id: string; name: string; output: string } - | { type: 'denied'; id: string; name: string; reason: string } - | { type: 'error'; id: string; name: string; output: string }; - -export type ToolLookup = (name: string) => ToolDefinition | undefined; +import { createToolCatalog } from './catalog.js'; export const ToolExecutorLayer = Layer.effect(ToolExecutorService, Effect.gen(function* () { const hooks = yield* HookService; @@ -26,7 +21,7 @@ export const ToolExecutorLayer = Layer.effect(ToolExecutorService, Effect.gen(fu callId?: string; toolLookup?: ToolLookup; } - ): any { + ): Effect.Effect<{ output: string }, AgentError> { return Effect.gen(function* () { const tool = opts?.toolLookup?.(name); if (!tool) return yield* Effect.fail(AgentError.toolNotFound(name)); @@ -44,7 +39,7 @@ export const ToolExecutorLayer = Layer.effect(ToolExecutorService, Effect.gen(fu callId, }); - const parsedArgs = yield* Effect.sync(() => tool.parameters.parse(finalArgs)); + const parsedArgs = yield* Effect.sync(() => tool.parse(finalArgs)); const start = Date.now(); // Execute tool — now returns Effect directly @@ -106,20 +101,20 @@ export const ToolExecutorLayer = Layer.effect(ToolExecutorService, Effect.gen(fu signal?: AbortSignal; toolLookup?: ToolLookup; } - ): Effect.Effect { + ): Effect.Effect { return execute(tc.name, tc.arguments ?? {}, { sessionId, callId: tc.id, ...opts }).pipe( Effect.matchEffect({ - onSuccess: (result: any): Effect.Effect => + onSuccess: (result: any): Effect.Effect => Effect.succeed({ - type: 'ok' as const, + status: 'ok' as const, id: tc.id, name: tc.name, output: result.output, }), - onFailure: (err): Effect.Effect => { + onFailure: (err): Effect.Effect => { if (err instanceof AgentError && err.code === 'TOOL_NOT_ALLOWED') { return Effect.succeed({ - type: 'denied' as const, + status: 'denied' as const, id: tc.id, name: tc.name, reason: err.message, @@ -128,7 +123,7 @@ export const ToolExecutorLayer = Layer.effect(ToolExecutorService, Effect.gen(fu const code = err instanceof AgentError ? err.code : 'TOOL_EXECUTION_FAILED'; const msg = err instanceof AgentError ? err.message : String(err); return Effect.succeed({ - type: 'error' as const, + status: 'error' as const, id: tc.id, name: tc.name, output: `[Error: ${code}] ${msg}`, @@ -137,7 +132,7 @@ export const ToolExecutorLayer = Layer.effect(ToolExecutorService, Effect.gen(fu }), Effect.catchAllDefect((defect) => Effect.succeed({ - type: 'error' as const, + status: 'error' as const, id: tc.id, name: tc.name, output: `[Unexpected] ${String(defect)}`, @@ -155,14 +150,14 @@ export const ToolExecutorLayer = Layer.effect(ToolExecutorService, Effect.gen(fu signal?: AbortSignal; toolLookup?: ToolLookup; } - ): Effect.Effect { + ): Effect.Effect { return Effect.gen(function* () { // Separate safe & destructive tools: safe tools run in parallel, Bash runs serially const safeTools: ToolCall[] = []; const bashTools: ToolCall[] = []; for (const tc of toolCalls) { - if (tc.name === 'execute_command' || tc.name === 'Bash') { + if (tc.name === 'execute_command') { bashTools.push(tc); } else { safeTools.push(tc); @@ -176,7 +171,7 @@ export const ToolExecutorLayer = Layer.effect(ToolExecutorService, Effect.gen(fu // Check abort before each tool if (opts?.signal?.aborted) { return Effect.succeed({ - type: 'denied' as const, + status: 'denied' as const, id: tc.id, name: tc.name, reason: 'aborted', @@ -188,12 +183,12 @@ export const ToolExecutorLayer = Layer.effect(ToolExecutorService, Effect.gen(fu ); // Bash tools — serial (avoid race conditions) - const bashResults: ToolResultUnion[] = []; + const bashResults: ToolResult[] = []; for (const tc of bashTools) { // Check abort before each tool if (opts?.signal?.aborted) { bashResults.push({ - type: 'denied' as const, + status: 'denied' as const, id: tc.id, name: tc.name, reason: 'aborted', @@ -208,5 +203,9 @@ export const ToolExecutorLayer = Layer.effect(ToolExecutorService, Effect.gen(fu }); } - return { executeBatch }; -} as any)); + function prepare(toolNames: readonly string[], mcpTools: McpToolSpec[] = []): Effect.Effect { + return Effect.sync(() => createToolCatalog(toolNames, mcpTools)); + } + + return { prepare, executeBatch }; +})); diff --git a/packages/codingcode/src/tools/types.ts b/packages/codingcode/src/tools/types.ts index a132f345..e54bffc5 100644 --- a/packages/codingcode/src/tools/types.ts +++ b/packages/codingcode/src/tools/types.ts @@ -1,13 +1,7 @@ import { z } from 'zod'; import { Effect } from 'effect'; import { AgentError } from '../core/error.js'; -export type { ToolDescription } from '../core/types.js'; - -export interface ToolExecCtx { - signal?: AbortSignal; - sessionId?: string; - projectPath?: string; -} +import type { ToolExecCtx } from '../contracts/tool.js'; export interface ToolDefinition { name: string; diff --git a/packages/codingcode/src/core/workspace.ts b/packages/codingcode/src/workspace/workspace.ts similarity index 96% rename from packages/codingcode/src/core/workspace.ts rename to packages/codingcode/src/workspace/workspace.ts index f66d3cd9..2025fec4 100644 --- a/packages/codingcode/src/core/workspace.ts +++ b/packages/codingcode/src/workspace/workspace.ts @@ -1,8 +1,8 @@ import { Effect } from 'effect'; import { existsSync, statSync } from 'fs'; import { resolve } from 'path'; -import { AgentError } from './error.js'; -import { encodeProjectPath } from './path.js'; +import { AgentError } from '../core/error.js'; +import { encodeProjectPath } from '../core/path.js'; import { loadConfig, type AppConfig } from '@codingcode/infra/config'; export interface WorkspaceInit { diff --git a/packages/codingcode/test/agent/abort.test.ts b/packages/codingcode/test/agent/abort.test.ts index 5294ab0b..69c01138 100644 --- a/packages/codingcode/test/agent/abort.test.ts +++ b/packages/codingcode/test/agent/abort.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; import { makeState, runAgentTurn, textDeltas } from '../helpers/agent-harness.js'; -import type { FrameBody, Transition } from '../../src/core/frame.js'; +import type { FrameBody, Transition } from '../../src/contracts/frame.js'; vi.mock('@codingcode/infra/config', () => ({ loadConfig: () => ({ diff --git a/packages/codingcode/test/agent/agent-concurrent.test.ts b/packages/codingcode/test/agent/agent-concurrent.test.ts index 4179c066..42f87510 100644 --- a/packages/codingcode/test/agent/agent-concurrent.test.ts +++ b/packages/codingcode/test/agent/agent-concurrent.test.ts @@ -33,6 +33,7 @@ const mockState = makeState({ sessionId: 'test-sid', cwd: '/tmp', title: 'concur function makeConcurrentExecutor(opts: { barrierPromise?: Promise; failTool?: string }) { const executionOrder: string[] = []; const executor = { + prepare: () => Effect.succeed({ tools: [], lookup: () => undefined }), execute: (name: string, _args: Record) => { if (opts.failTool && name === opts.failTool) { return Effect.fail(new Error('Simulated failure') as any); @@ -56,10 +57,10 @@ function makeConcurrentExecutor(opts: { barrierPromise?: Promise; failTool executor.execute(tc.name, tc.arguments ?? {}).pipe( (Effect.matchEffect as any)({ onSuccess: (output: any) => - Effect.succeed({ type: 'ok' as const, id: tc.id, name: tc.name, output }), + Effect.succeed({ status: 'ok' as const, id: tc.id, name: tc.name, output }), onFailure: (err: any) => Effect.succeed({ - type: 'error' as const, + status: 'error' as const, id: tc.id, name: tc.name, output: String(err), @@ -67,7 +68,7 @@ function makeConcurrentExecutor(opts: { barrierPromise?: Promise; failTool }), (Effect.catchAllDefect as any)((defect: any) => Effect.succeed({ - type: 'error' as const, + status: 'error' as const, id: tc.id, name: tc.name, output: String(defect), diff --git a/packages/codingcode/test/agent/agent-todo-event.test.ts b/packages/codingcode/test/agent/agent-todo-event.test.ts index ad63a980..e15c2704 100644 --- a/packages/codingcode/test/agent/agent-todo-event.test.ts +++ b/packages/codingcode/test/agent/agent-todo-event.test.ts @@ -45,10 +45,11 @@ function makeLlm(firstToolName: string) { function makeExecutor(output: string) { return { + prepare: () => Effect.succeed({ tools: [], lookup: () => undefined }), executeBatch: (calls: any[]) => Effect.succeed( calls.map((c: any) => ({ - type: 'ok' as const, + status: 'ok' as const, id: c.id, name: c.name, output, diff --git a/packages/codingcode/test/agent/agent.test.ts b/packages/codingcode/test/agent/agent.test.ts index f2a9d032..4331ab62 100644 --- a/packages/codingcode/test/agent/agent.test.ts +++ b/packages/codingcode/test/agent/agent.test.ts @@ -67,10 +67,11 @@ describe('agent runTurn loop', () => { modelInfo: { maxTokens: 1000 }, } as any; const executor = { + prepare: () => Effect.succeed({ tools: [], lookup: () => undefined }), executeBatch: (calls: any[]) => Effect.succeed( calls.map((tc: any) => ({ - type: 'ok' as const, + status: 'ok' as const, id: tc.id, name: tc.name, output: 'On branch main\nnothing to commit', diff --git a/packages/codingcode/test/agent/message-tool-calls.test.ts b/packages/codingcode/test/agent/message-tool-calls.test.ts index 08c3b55e..43fd5091 100644 --- a/packages/codingcode/test/agent/message-tool-calls.test.ts +++ b/packages/codingcode/test/agent/message-tool-calls.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import type { Message } from '../../src/core/types.js'; +import type { Message } from '../../src/contracts/types.js'; describe('Message tool_calls assignment', () => { it('should allow direct assignment without as any cast', () => { diff --git a/packages/codingcode/test/agent/stop-decision-type.test.ts b/packages/codingcode/test/agent/stop-decision-type.test.ts index b6cd5eca..8b7c6096 100644 --- a/packages/codingcode/test/agent/stop-decision-type.test.ts +++ b/packages/codingcode/test/agent/stop-decision-type.test.ts @@ -1,8 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; import { Effect } from 'effect'; -import { Result } from '../../src/core/result'; import { HookService } from '../../src/hooks/port.js'; -import type { HookDecision } from '../../src/hooks/types.js'; import { HookLayer } from '../../src/hooks/hooks.js'; describe('agent.turn.stop decision type inference', () => { diff --git a/packages/codingcode/test/agent/stop-hook.test.ts b/packages/codingcode/test/agent/stop-hook.test.ts index ab599dd5..ec854f77 100644 --- a/packages/codingcode/test/agent/stop-hook.test.ts +++ b/packages/codingcode/test/agent/stop-hook.test.ts @@ -120,7 +120,7 @@ describe('agent runTurn stop hook', () => { const hooks = { emit: vi.fn(() => Effect.succeed(undefined)), emitDecision } as any; await runAgentTurn( - { llm, state: mockState, hooks, sessionPort: { recordSystem } }, + { llm, state: mockState, hooks, session: { recordSystem } }, { sessionId: 'test-sid', cwd: '/tmp' } ); @@ -135,7 +135,7 @@ describe('agent runTurn stop hook', () => { const hooks = { emit: vi.fn(() => Effect.succeed(undefined)), emitDecision } as any; await runAgentTurn( - { llm, state: mockState, hooks, sessionPort: { recordSystem } }, + { llm, state: mockState, hooks, session: { recordSystem } }, { sessionId: 'test-sid', cwd: '/tmp' } ); diff --git a/packages/codingcode/test/agent/submit-plan-turn-end.test.ts b/packages/codingcode/test/agent/submit-plan-turn-end.test.ts index ebc1b672..7663fbbe 100644 --- a/packages/codingcode/test/agent/submit-plan-turn-end.test.ts +++ b/packages/codingcode/test/agent/submit-plan-turn-end.test.ts @@ -51,10 +51,11 @@ function makeSubmitPlanLlm() { function makeOkExecutor() { return { + prepare: () => Effect.succeed({ tools: [], lookup: () => undefined }), executeBatch: (calls: any[]) => Effect.succeed( calls.map((tc: any) => ({ - type: 'ok' as const, + status: 'ok' as const, id: tc.id, name: tc.name, output: 'Plan written to /tmp/plans/my-plan.md', @@ -99,7 +100,7 @@ describe('agent treats submit_plan as an ordinary tool', () => { state: mockState, hooks, executor: makeOkExecutor(), - sessionPort: { setActiveProfile }, + session: { setActiveProfile }, }, { sessionId: 'test-session', cwd: '/tmp' } ); diff --git a/packages/codingcode/test/approval/pipeline.test.ts b/packages/codingcode/test/approval/pipeline.test.ts index 96a84fc4..984daa1f 100644 --- a/packages/codingcode/test/approval/pipeline.test.ts +++ b/packages/codingcode/test/approval/pipeline.test.ts @@ -12,11 +12,7 @@ const mockHookService = { emit: () => Effect.succeed(undefined), emitDecision: () => Effect.succeed(null), reloadUserHooks: () => Effect.succeed(undefined), - attachSessionHooks: () => Effect.succeed(undefined), - disableHook: () => Effect.succeed(undefined), - enableHook: () => Effect.succeed(undefined), disposeSession: () => Effect.succeed(undefined), - disposeProject: () => Effect.succeed(undefined), }; const mockApprovalWaitService = { @@ -29,12 +25,12 @@ const mockApprovalWaitService = { hasEmitter: () => Effect.succeed(false), }; -const HookTestLayer = Layer.succeed(HookService, mockHookService as any); -const WaitTestLayer = Layer.succeed(ApprovalWaitService, mockApprovalWaitService as any); +const HookTestLayer = Layer.succeed(HookService, mockHookService); +const WaitTestLayer = Layer.succeed(ApprovalWaitService, mockApprovalWaitService); const TestLayer = Layer.mergeAll(HookTestLayer, WaitTestLayer); function runWithLayer(eff: Effect.Effect): Promise { - return Effect.runPromise(eff.pipe(Effect.provide(TestLayer) as any)); + return Effect.runPromise(eff.pipe(Effect.provide(TestLayer))); } describe('Approval Pipeline — PermissionMode auto-allow (merged from ReadonlyWhitelist + acceptEdits)', () => { diff --git a/packages/codingcode/test/architecture/boundaries.test.ts b/packages/codingcode/test/architecture/boundaries.test.ts new file mode 100644 index 00000000..20b190b4 --- /dev/null +++ b/packages/codingcode/test/architecture/boundaries.test.ts @@ -0,0 +1,181 @@ +import { describe, expect, it } from 'vitest'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// 架构边界回归:把 issue 里的 R1–R4 固化成断言,防止再次出现"半倒置"。 +const HERE = dirname(fileURLToPath(import.meta.url)); +const SRC = resolve(HERE, '../../src'); + +const norm = (p: string) => p.replace(/\\/g, '/'); + +function listTs(dir: string): string[] { + const out: string[] = []; + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, e.name); + if (e.isDirectory()) out.push(...listTs(p)); + else if (e.name.endsWith('.ts')) out.push(norm(p)); + } + return out; +} + +const FILES = listTs(SRC); + +/** 抽取文件里的模块说明符(静态 import 与动态 import) */ +function specifiersOf(file: string): string[] { + const src = readFileSync(file, 'utf8'); + const specs: string[] = []; + for (const m of src.matchAll(/\bfrom\s+['"]([^'"]+)['"]/g)) specs.push(m[1]!); + for (const m of src.matchAll(/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) specs.push(m[1]!); + return specs; +} + +/** 相对说明符 → 仓库内绝对路径(去 .js);第三方返回 null */ +function resolveSpec(file: string, spec: string): string | null { + if (!spec.startsWith('.')) return null; + return norm(resolve(dirname(file), spec)).replace(/\.js$/, ''); +} + +const relSrc = (abs: string) => norm(relative(SRC, abs)); +const isCore = (abs: string) => abs.startsWith(norm(join(SRC, 'core')) + '/'); +const isContracts = (abs: string) => abs.startsWith(norm(join(SRC, 'contracts')) + '/'); + +/** 契约文件:每个特性目录的 port.ts(宽 Tag,agent 自持的装配端口也在 agent/port.ts) */ +const CONTRACT_FILES = FILES.filter((f) => f.endsWith('/port.ts')); + +describe('R1 契约不得 import 实现', () => { + it('所有 port.ts 的跨模块引用只有 core/、contracts/ 与同目录类型', () => { + const violations: string[] = []; + for (const file of CONTRACT_FILES) { + for (const spec of specifiersOf(file)) { + const target = resolveSpec(file, spec); + if (target === null) continue; // 第三方库 + const sameDir = dirname(target) === dirname(file); + if (!isCore(target) && !isContracts(target) && !sameDir) { + violations.push(`${relSrc(file)} → ${spec}`); + } + } + } + expect(violations).toEqual([]); + }); +}); + +describe('R2 实现不得依赖消费者模块', () => { + it('agent 自持的装配端口只在 agent/ 内部出现', () => { + const hits = FILES.filter((f) => + /\bToolEnvPort\b/.test(readFileSync(f, 'utf8')) + ).map(relSrc).sort(); + expect(hits).toEqual([ + 'agent/agent.ts', + 'agent/port.ts', + 'agent/tool-env.ts', + ]); + }); +}); + +describe('R3 core 零内部依赖', () => { + it('core/ 不引用 core/ 之外的任何 src 模块', () => { + const violations: string[] = []; + for (const file of FILES.filter((f) => isCore(f))) { + for (const spec of specifiersOf(file)) { + const target = resolveSpec(file, spec); + if (target === null) continue; // 第三方 / 其他 workspace 包 + if (!isCore(target)) violations.push(`${relSrc(file)} → ${spec}`); + } + } + expect(violations).toEqual([]); + }); +}); + +/** core/ 的准入:只放不指向任何功能模块的通用件 */ +const NODE_BUILTINS = new Set([ + 'os', 'path', 'fs', 'url', 'crypto', 'util', 'stream', 'events', 'buffer', 'child_process', 'process', +]); + +describe('core/ 准入:只承载通用件', () => { + it('core/ 的 import 只能是 node 内置与同目录相对路径', () => { + const violations: string[] = []; + for (const file of FILES.filter((f) => isCore(f))) { + for (const spec of specifiersOf(file)) { + if (spec.startsWith('.')) { + const target = resolveSpec(file, spec)!; + if (dirname(target) !== dirname(file)) violations.push(`${relSrc(file)} → ${spec}`); + continue; + } + if (!NODE_BUILTINS.has(spec.replace(/^node:/, ''))) { + violations.push(`${relSrc(file)} → ${spec}`); + } + } + } + expect(violations).toEqual([]); + }); +}); + +describe('contracts/ 准入:不得依赖领域实现', () => { + it('contracts/ 的文件只引用 core/、同目录与第三方', () => { + const violations: string[] = []; + for (const file of FILES.filter((f) => isContracts(f))) { + for (const spec of specifiersOf(file)) { + const target = resolveSpec(file, spec); + if (target === null) continue; + const sameDir = dirname(target) === dirname(file); + if (!isCore(target) && !sameDir) violations.push(`${relSrc(file)} → ${spec}`); + } + } + expect(violations).toEqual([]); + }); +}); + +describe('R4 一个概念只允许一处类型定义', () => { + const CANONICAL: Record = { + ApprovalRequest: 'approval/port.ts', + TokenUsage: 'contracts/types.ts', + TodoItem: 'contracts/types.ts', + ProfileName: 'contracts/types.ts', + UITurn: 'contracts/session.ts', + UITurnItem: 'contracts/session.ts', + SessionEvent: 'contracts/session.ts', + SessionStoreState: 'contracts/session.ts', + SessionCreateOptions: 'contracts/session.ts', + ToolOutcome: 'contracts/frame.ts', + ToolCatalog: 'contracts/tool.ts', + ToolResult: 'contracts/tool.ts', + ToolRunner: 'contracts/tool.ts', + ToolLookup: 'contracts/tool.ts', + LLMClient: 'contracts/provider.ts', + SelectableModel: 'contracts/provider.ts', + PermissionMode: 'contracts/permission.ts', + ApprovalDecision: 'contracts/permission.ts', + HookPoint: 'contracts/hooks.ts', + HookDecision: 'contracts/hooks.ts', + Skill: 'contracts/skill.ts', + McpServerConfig: 'contracts/mcp.ts', + McpStatus: 'contracts/mcp.ts', + }; + + it.each(Object.entries(CANONICAL))('%s 只在 %s 声明一次', (name, expected) => { + const re = new RegExp( + `^export\\s+(?:declare\\s+)?(?:abstract\\s+)?(?:interface|type|class)\\s+${name}\\b`, + 'm' + ); + const owners = FILES.filter((f) => re.test(readFileSync(f, 'utf8'))).map(relSrc).sort(); + expect(owners).toEqual([norm(expected)]); + }); +}); + +describe('相对 import 必须可解析', () => { + it('每条相对 import 都能在仓库内找到落点(覆盖 layer.ts 等测试不加载的模块)', () => { + const missing: string[] = []; + for (const file of FILES) { + for (const spec of specifiersOf(file)) { + const target = resolveSpec(file, spec); + if (target === null) continue; + const ok = ['.ts', '.tsx', '/index.ts', '/index.tsx'].some((ext) => + existsSync(target + ext) + ); + if (!ok) missing.push(`${relSrc(file)} → ${spec}`); + } + } + expect(missing).toEqual([]); + }); +}); diff --git a/packages/codingcode/test/client/direct-types.test.ts b/packages/codingcode/test/client/direct-types.test.ts index c5009fbb..2cdb4757 100644 --- a/packages/codingcode/test/client/direct-types.test.ts +++ b/packages/codingcode/test/client/direct-types.test.ts @@ -6,9 +6,9 @@ import { createDirectSessionClient } from '../../src/direct/sessions.js'; import { createDirectModelClient } from '../../src/direct/models.js'; import { createDirectSettingsClient } from '../../src/direct/settings.js'; import type { AppRuntime } from '../../src/layer.js'; -import type { LLMClient } from '../../src/llm/client.js'; +import type { LLMClient } from '../../src/contracts/provider.js'; import { ApprovalWaitService } from '../../src/approval/wait-port.js'; -import { WorkspaceService } from '../../src/core/workspace.js'; +import { WorkspaceService } from '../../src/workspace/workspace.js'; import { LLMFactoryService } from '../../src/llm/port.js'; import { AgentError } from '../../src/core/error.js'; import { ApprovalWaitLayer } from '../../src/approval/wait.js'; diff --git a/packages/codingcode/test/client/direct.test.ts b/packages/codingcode/test/client/direct.test.ts index 016719e3..e9a2f462 100644 --- a/packages/codingcode/test/client/direct.test.ts +++ b/packages/codingcode/test/client/direct.test.ts @@ -4,7 +4,7 @@ import { Effect, Layer, ManagedRuntime } from 'effect'; import { createDirectModelClient } from '../../src/direct/models.js'; import { ApprovalWaitService } from '../../src/approval/wait-port.js'; import { AgentError } from '../../src/core/error.js'; -import { WorkspaceService } from '../../src/core/workspace.js'; +import { WorkspaceService } from '../../src/workspace/workspace.js'; import { LLMFactoryService } from '../../src/llm/port.js'; import { ApprovalWaitLayer } from '../../src/approval/wait.js'; diff --git a/packages/codingcode/test/client/http/agent-runtime.test.ts b/packages/codingcode/test/client/http/agent-runtime.test.ts index 4a068499..107f06d6 100644 --- a/packages/codingcode/test/client/http/agent-runtime.test.ts +++ b/packages/codingcode/test/client/http/agent-runtime.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from 'vitest'; import { createHttpAgentClient } from '../../../src/client/http/agent-runtime.js'; import { createRequestHelpers } from '../../../src/client/http/request.js'; -import type { Frame } from '../../../src/core/frame.js'; +import type { Frame } from '../../../src/contracts/frame.js'; function createSseResponse(lines: unknown[]) { const encoder = new TextEncoder(); diff --git a/packages/codingcode/test/context/append-turn-end.test.ts b/packages/codingcode/test/context/append-turn-end.test.ts index 008bd59c..2bdd08de 100644 --- a/packages/codingcode/test/context/append-turn-end.test.ts +++ b/packages/codingcode/test/context/append-turn-end.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs'; import { join } from 'path'; import { randomUUID } from 'crypto'; -import { estimateTokensForContent } from '../../src/core/util.js'; +import { estimateTokensForContent } from '../../src/context/tokens.js'; import { useTempProjectBase } from '../helpers/project-base.js'; vi.mock('@codingcode/infra/config', () => ({ diff --git a/packages/codingcode/test/context/budget-integration.test.ts b/packages/codingcode/test/context/budget-integration.test.ts index 2713c053..f80f75a4 100644 --- a/packages/codingcode/test/context/budget-integration.test.ts +++ b/packages/codingcode/test/context/budget-integration.test.ts @@ -8,7 +8,7 @@ import type { ContextShape } from '../../src/context/port.js'; import { SessionService } from '../../src/session/port.js'; import { SessionLayer } from '../../src/session/session.js'; import { LLMFactoryService } from '../../src/llm/port.js'; -import type { SessionEvent } from '../../src/session/types.js'; +import type { SessionEvent } from '../../src/contracts/session.js'; import { useTempProjectBase } from '../helpers/project-base.js'; import { ContextLayer } from '../../src/context/context.js'; diff --git a/packages/codingcode/test/context/compressor/behavior.test.ts b/packages/codingcode/test/context/compressor/behavior.test.ts index 2336f209..e05e900f 100644 --- a/packages/codingcode/test/context/compressor/behavior.test.ts +++ b/packages/codingcode/test/context/compressor/behavior.test.ts @@ -8,11 +8,11 @@ import type { ContextShape } from '../../../src/context/port.js'; import { SessionService } from '../../../src/session/port.js'; import { SessionLayer } from '../../../src/session/session.js'; import { LLMFactoryService } from '../../../src/llm/port.js'; -import type { LLMClient } from '../../../src/llm/client.js'; -import type { SessionIndex, SessionEvent, SummaryEvent } from '../../../src/session/types.js'; +import type { LLMClient } from '../../../src/contracts/provider.js'; +import type { SessionIndex, SessionEvent, SummaryEvent } from '../../../src/contracts/session.js'; import { filterForContext, buildContextMessages } from '../../../src/context/context.js'; import { readHistory } from '../../../src/session/file-ops.js'; -import { estimateTokens } from '../../../src/core/util.js'; +import { estimateTokens } from '../../../src/context/tokens.js'; import { useTempProjectBase } from '../../helpers/project-base.js'; import { ContextLayer } from '../../../src/context/context.js'; diff --git a/packages/codingcode/test/context/compressor/llm-resolver.test.ts b/packages/codingcode/test/context/compressor/llm-resolver.test.ts index 302c44a6..0275977c 100644 --- a/packages/codingcode/test/context/compressor/llm-resolver.test.ts +++ b/packages/codingcode/test/context/compressor/llm-resolver.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { Effect } from 'effect'; import { LLMFactoryService } from '../../../src/llm/port.js'; import { AgentError } from '../../../src/core/error.js'; -import type { LLMClient } from '../../../src/llm/client.js'; -import type { SelectableModel } from '../../../src/llm/port.js'; +import type { LLMClient } from '../../../src/contracts/provider.js'; +import type { SelectableModel } from '../../../src/contracts/provider.js'; const { mockFindModel, mockCreateClient } = vi.hoisted(() => ({ mockFindModel: vi.fn(() => Effect.succeed(null)), diff --git a/packages/codingcode/test/context/organizer.test.ts b/packages/codingcode/test/context/organizer.test.ts index 54666da3..f1373131 100644 --- a/packages/codingcode/test/context/organizer.test.ts +++ b/packages/codingcode/test/context/organizer.test.ts @@ -4,7 +4,7 @@ import { ContextService } from '../../src/context/port.js'; import { SessionService } from '../../src/session/port.js'; import { SessionLayer } from '../../src/session/session.js'; import { LLMFactoryService } from '../../src/llm/port.js'; -import type { SessionEvent, ToolResultEvent } from '../../src/session/types.js'; +import type { SessionEvent, ToolResultEvent } from '../../src/contracts/session.js'; import { ContextLayer } from '../../src/context/context.js'; const baseConfig = { diff --git a/packages/codingcode/test/context/tokens.test.ts b/packages/codingcode/test/context/tokens.test.ts index a88b8fc2..0ffadb2a 100644 --- a/packages/codingcode/test/context/tokens.test.ts +++ b/packages/codingcode/test/context/tokens.test.ts @@ -3,7 +3,7 @@ import { estimateTokensForContent, estimateTokens, estimateMessageTokens, -} from '../../src/core/util.js'; +} from '../../src/context/tokens.js'; describe('token estimation', () => { it('empty content returns 0', () => { diff --git a/packages/codingcode/test/core/error-code.test.ts b/packages/codingcode/test/core/error-code.test.ts index 878f71b7..6d9d04fe 100644 --- a/packages/codingcode/test/core/error-code.test.ts +++ b/packages/codingcode/test/core/error-code.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { NotFoundError, AlreadyExistsError, AgentError } from '../../src/core/error.js'; +import { AgentError } from '../../src/core/error.js'; +import { NotFoundError, AlreadyExistsError } from '../../src/contracts/error.js'; describe('NotFoundError has code+httpStatus', () => { it('code is NOT_FOUND and httpStatus returns 404', () => { diff --git a/packages/codingcode/test/core/error.test.ts b/packages/codingcode/test/core/error.test.ts index ee20513e..7e4bd280 100644 --- a/packages/codingcode/test/core/error.test.ts +++ b/packages/codingcode/test/core/error.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { AgentError, ApiError } from '../../src/core/error.js'; +import { AgentError } from '../../src/core/error.js'; +import { ApiError } from '../../src/contracts/error.js'; describe('AgentError.httpStatus', () => { it('returns 400 for CONFIG_MISSING', () => { diff --git a/packages/codingcode/test/core/frame-io.test.ts b/packages/codingcode/test/core/frame-io.test.ts index 44c914c2..46958f8e 100644 --- a/packages/codingcode/test/core/frame-io.test.ts +++ b/packages/codingcode/test/core/frame-io.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { createFrameAssembler, decodeFrame, encodeFrame } from '../../src/core/frame-io.js'; -import type { FrameBody } from '../../src/core/frame.js'; +import { createFrameAssembler, decodeFrame, encodeFrame } from '../../src/contracts/frame-io.js'; +import type { FrameBody } from '../../src/contracts/frame.js'; // ---- body builders ---- diff --git a/packages/codingcode/test/core/frame-protocol.test.ts b/packages/codingcode/test/core/frame-protocol.test.ts index eb37cfff..4da491d1 100644 --- a/packages/codingcode/test/core/frame-protocol.test.ts +++ b/packages/codingcode/test/core/frame-protocol.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { isTurnEnd } from '../../src/core/frame.js'; -import type { Frame, FrameBody } from '../../src/core/frame.js'; +import { isTurnEnd } from '../../src/contracts/frame.js'; +import type { Frame, FrameBody } from '../../src/contracts/frame.js'; const turnId = 1; diff --git a/packages/codingcode/test/core/workspace.test.ts b/packages/codingcode/test/core/workspace.test.ts index 46e77ff3..6092fa94 100644 --- a/packages/codingcode/test/core/workspace.test.ts +++ b/packages/codingcode/test/core/workspace.test.ts @@ -4,7 +4,7 @@ import { mkdirSync, rmSync, writeFileSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { randomUUID } from 'crypto'; -import { WorkspaceService, parseWorkspaceArgs } from '../../src/core/workspace.js'; +import { WorkspaceService, parseWorkspaceArgs } from '../../src/workspace/workspace.js'; import { encodeProjectPath } from '../../src/core/path.js'; describe('core/workspace', () => { diff --git a/packages/codingcode/test/helpers/agent-harness.ts b/packages/codingcode/test/helpers/agent-harness.ts index 69fb813a..cca465e8 100644 --- a/packages/codingcode/test/helpers/agent-harness.ts +++ b/packages/codingcode/test/helpers/agent-harness.ts @@ -1,35 +1,30 @@ // Agent 循环测试基座:通过公开的 AgentService.runTurn 驱动 agent, -// 替代已删除的 agentLoop 自由函数。所有 agent 内部服务均以窄端口 mock 注入。 +// 替代已删除的 agentLoop 自由函数。agent 依赖均以宽服务 mock 注入。 // // 自 frame 协议重构后,runTurn 产出 FrameBody(信封由装配器另盖), // 本文件同时提供从 FrameBody[] 中抽取内容的纯函数,供各测试断言使用。 import { Effect, Layer } from 'effect'; import { AgentLayer } from '../../src/agent/agent.js'; import { ToolEnvLayer } from '../../src/agent/tool-env.js'; -import { ToolCatalogLayer } from '../../src/agent/tool-catalog.js'; +import { createToolCatalog } from '../../src/tools/catalog.js'; import { AgentService } from '../../src/agent/port.js'; -import { - ApprovalPort, - CheckpointPort, - ContextPort, - HookPort, - LlmPort, - McpPort, - MemoryPort, - RulesPort, - SessionPort, - SkillPort, - TodoPort, - ToolExecutorPort, -} from '../../src/agent/deps.js'; +import { ApprovalService } from '../../src/approval/port.js'; +import { CheckpointService } from '../../src/checkpoint/port.js'; +import { ContextService } from '../../src/context/port.js'; import { HookService } from '../../src/hooks/port.js'; +import { LLMFactoryService } from '../../src/llm/port.js'; import { McpService } from '../../src/mcp/port.js'; +import { MemoryService } from '../../src/memory/port.js'; +import { RulesService } from '../../src/rules/port.js'; +import { SessionService } from '../../src/session/port.js'; +import { SkillService } from '../../src/skills/port.js'; import { SubagentRunnerService } from '../../src/subagent/port.js'; import { TodoService } from '../../src/todo/port.js'; -import type { FrameBody, RuntimeEvent, Transition } from '../../src/core/frame.js'; -import type { TokenUsage } from '../../src/core/types.js'; -import type { LLMStreamPart } from '../../src/llm/types.js'; -import type { SessionStoreState } from '../../src/session/types.js'; +import { ToolExecutorService } from '../../src/tools/port.js'; +import type { FrameBody, RuntimeEvent, Transition } from '../../src/contracts/frame.js'; +import type { TokenUsage } from '../../src/contracts/types.js'; +import type { LLMStreamPart } from '../../src/contracts/provider.js'; +import type { SessionStoreState } from '../../src/contracts/session.js'; // ---- LLM 部件构造器 ---- @@ -140,12 +135,12 @@ export interface HarnessMocks { }; todo?: Map>; memorySnapshot?: string; - /** 可选:覆盖 ContextPort.assemblePayload 的返回(默认一条 user 消息)。 */ + /** 可选:覆盖 ContextService.assemblePayload 的返回(默认一条 user 消息)。 */ contextAssemble?: () => Promise>; - /** 可选:覆盖 ContextPort.willCompact(默认 false)。 */ + /** 可选:覆盖 ContextService.willCompact(默认 false)。 */ contextWillCompact?: () => Promise; - /** 可选:覆盖 SessionPort 窄端口的个别方法(默认实现见 makeAgentLayer)。 */ - sessionPort?: Partial<{ + /** 可选:覆盖 SessionService 的个别方法(默认实现见 makeAgentLayer)。 */ + session?: Partial<{ load: (cwd: string, sid: string) => any; create: (cwd: string, opts: any, extra?: any) => any; recordUser: (state: any, content: string) => any; @@ -193,7 +188,7 @@ export function makeDefaultMocks(overrides: Partial = {}): Harness executor: overrides.executor, todo, memorySnapshot: overrides.memorySnapshot ?? '', - sessionPort: overrides.sessionPort, + session: overrides.session, }; } @@ -216,10 +211,11 @@ export function makeAgentLayer(mocks: HarnessMocks): Layer.Layer { const executor = mocks.executor ?? ({ + prepare: (names: readonly string[], mcpTools: any[] = []) => Effect.succeed(createToolCatalog(names, mcpTools)), executeBatch: (calls: any[]) => Effect.succeed( calls.map((c: any) => ({ - type: 'ok' as const, + status: 'ok' as const, id: c.id, name: c.name, output: '', @@ -235,19 +231,15 @@ export function makeAgentLayer(mocks: HarnessMocks): Layer.Layer { sessionId: opts.sessionId ?? 'created-sid', activeProfile: opts.activeProfile ?? 'build', }), - recordUser: () => Effect.succeed({}), + recordUser: () => Effect.succeed({ turnId: 1 }), recordSystem: () => Effect.succeed({}), recordAssistant: () => Effect.succeed({}), recordToolResult: () => Effect.succeed({}), setPermissionMode: () => Effect.void, setActiveProfile: () => Effect.void, - ...(mocks.sessionPort ?? {}), + ...(mocks.session ?? {}), }; - const mcpPort = { - syncConnections: () => Effect.void, - listProjectMcpTools: () => [], - }; const skills = { extractSkill: (_cwd: string, query: string) => Effect.succeed([undefined, query]), }; @@ -261,32 +253,30 @@ export function makeAgentLayer(mocks: HarnessMocks): Layer.Layer { flushSessionToMemory: () => Promise.resolve({ written: false, bytes: 0 }), }; + const mcpLayer = Layer.succeed(McpService, { + syncConnections: () => Effect.void, + listProjectMcpTools: () => Effect.succeed([]), + } as any); + const services = Layer.mergeAll( - Layer.succeed(SessionPort, session as any), - Layer.succeed(ToolExecutorPort, executor as any), - Layer.succeed(CheckpointPort, { + Layer.succeed(SessionService, session as any), + Layer.succeed(ToolExecutorService, executor as any), + Layer.succeed(CheckpointService, { snapshotBaseline: () => Effect.void, snapshotFinal: () => Effect.void, } as any), - Layer.succeed(HookPort, { - emit: hooks.emit, - emitDecision: hooks.emitDecision, - disposeSession: () => Effect.void, - } as any), - Layer.succeed(ApprovalPort, { + Layer.succeed(ApprovalService, { evaluate: () => Effect.succeed({ type: 'allow', source: 'test' }), } as any), - Layer.succeed(SkillPort, skills as any), - Layer.succeed(McpPort, mcpPort as any), - Layer.succeed(ContextPort, context as any), - Layer.succeed(MemoryPort, memory as any), - Layer.succeed(LlmPort, { getLLMClient: () => Effect.succeed(mocks.llm) } as any), - Layer.succeed(RulesPort, { + Layer.succeed(SkillService, skills as any), + Layer.succeed(ContextService, context as any), + Layer.succeed(MemoryService, memory as any), + Layer.succeed(LLMFactoryService, { getLLMClient: () => Effect.succeed(mocks.llm) } as any), + Layer.succeed(RulesService, { getAllRules: () => '', evictProjectRules: () => {}, } as any), - Layer.succeed(TodoPort, { read: (sid: string) => store.get(sid) ?? [] } as any), - // todo_write 工具 execute 执行时 yield* TodoService(完整 tag),窄端口 TodoPort 不可替代 + // agent 与 todo_write 工具消费同一个 TodoService Layer.succeed(TodoService, { read: (sid: string) => store.get(sid) ?? [], write: (sid: string, items: any[]) => { @@ -294,7 +284,7 @@ export function makeAgentLayer(mocks: HarnessMocks): Layer.Layer { }, reset: () => store.clear(), } as any), - // dispatch_agent 工具 execute 执行时 yield* 这三个完整服务 + // agent 与 dispatch_agent 工具消费同一个 HookService Layer.succeed(HookService, { register: () => Effect.succeed(() => {}), registerDecision: () => Effect.succeed(() => {}), @@ -303,15 +293,10 @@ export function makeAgentLayer(mocks: HarnessMocks): Layer.Layer { reloadUserHooks: () => Effect.void, disposeSession: () => Effect.void, } as any), - Layer.succeed(McpService, { - syncConnections: () => Effect.void, - listProjectMcpTools: () => [], - } as any), + mcpLayer, Layer.succeed(SubagentRunnerService, {} as any), // ToolEnvPort:把上面的具体服务适配成 agent 所需的工具执行期注入能力(同 layer.ts) ToolEnvLayer, - // ToolCatalogPort:静态内置工具 + profile 工具 + MCP 工具的装配(同 layer.ts) - ToolCatalogLayer, ); return services; } diff --git a/packages/codingcode/test/helpers/project-base.ts b/packages/codingcode/test/helpers/project-base.ts index 0e116aac..95a89322 100644 --- a/packages/codingcode/test/helpers/project-base.ts +++ b/packages/codingcode/test/helpers/project-base.ts @@ -1,40 +1,26 @@ -import { mkdtempSync, rmSync, mkdirSync } from 'fs'; +import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { afterEach, beforeEach } from 'vitest'; -import { - setProjectBaseDir, - setProjectPlansBaseDir, - getProjectBaseDir, - getProjectPlansBaseDir, -} from '../../src/core/path.js'; +import { setProjectBaseDir, getProjectBaseDir } from '../../src/core/path.js'; export interface TempProjectBase { readonly dir: string; - readonly plansDir: string; } export function useTempProjectBase(prefix = 'codingcode-test-project-base-'): TempProjectBase { let dir = ''; - let plansDir = ''; beforeEach(() => { dir = mkdtempSync(join(tmpdir(), prefix)); - plansDir = join(dir, 'plans'); - mkdirSync(plansDir, { recursive: true }); setProjectBaseDir(dir); - setProjectPlansBaseDir(plansDir); }); afterEach(() => { setProjectBaseDir(undefined); - setProjectPlansBaseDir(undefined); rmSync(dir, { recursive: true, force: true }); }); return { get dir() { return getProjectBaseDir(); }, - get plansDir() { - return getProjectPlansBaseDir(); - }, }; } diff --git a/packages/codingcode/test/llm/deepseek-provider.test.ts b/packages/codingcode/test/llm/deepseek-provider.test.ts index 3845cae2..4a3bbb4d 100644 --- a/packages/codingcode/test/llm/deepseek-provider.test.ts +++ b/packages/codingcode/test/llm/deepseek-provider.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { LLMStreamPart } from '../../src/llm/types.js'; +import type { LLMStreamPart } from '../../src/contracts/provider.js'; const streamText = vi.fn(); const stepCountIs = vi.fn((count: number) => ({ count })); diff --git a/packages/codingcode/test/llm/factory.test.ts b/packages/codingcode/test/llm/factory.test.ts index 6cac767b..1d279e51 100644 --- a/packages/codingcode/test/llm/factory.test.ts +++ b/packages/codingcode/test/llm/factory.test.ts @@ -59,7 +59,7 @@ describe('switchModel - persists to config', () => { mockFs(); const { LLMFactoryService } = await import('../../src/llm/port.js'); - const { WorkspaceService } = await import('../../src/core/workspace.js'); + const { WorkspaceService } = await import('../../src/workspace/workspace.js'); const workspaceLayer = makeWorkspaceLayer(WorkspaceService, { model: 'model-x', apiKeyEnv: 'API_KEY_A', @@ -89,7 +89,7 @@ describe('switchModel - persists to config', () => { mockFs(); const { LLMFactoryService } = await import('../../src/llm/port.js'); - const { WorkspaceService } = await import('../../src/core/workspace.js'); + const { WorkspaceService } = await import('../../src/workspace/workspace.js'); const workspaceLayer = makeWorkspaceLayer(WorkspaceService, { model: 'model-x', apiKeyEnv: 'API_KEY_A', @@ -120,7 +120,7 @@ describe('getActiveEntry - activeModel priority', () => { mockFs(); const { LLMFactoryService } = await import('../../src/llm/port.js'); - const { WorkspaceService } = await import('../../src/core/workspace.js'); + const { WorkspaceService } = await import('../../src/workspace/workspace.js'); const workspaceLayer = makeWorkspaceLayer(WorkspaceService, { model: 'model-y', apiKeyEnv: 'API_KEY_A', @@ -142,7 +142,7 @@ describe('getActiveEntry - activeModel priority', () => { it('returns error when activeModel is not set in config', async () => { const { LLMFactoryService } = await import('../../src/llm/port.js'); - const { WorkspaceService } = await import('../../src/core/workspace.js'); + const { WorkspaceService } = await import('../../src/workspace/workspace.js'); const workspaceLayer = makeWorkspaceLayer(WorkspaceService, undefined); const { LlmLayer } = await import('../../src/llm/llm.js'); const factoryLayer = LlmLayer.pipe(Layer.provide(workspaceLayer)); @@ -164,7 +164,7 @@ describe('getActiveEntry - activeModel priority', () => { mockFs(); const { LLMFactoryService } = await import('../../src/llm/port.js'); - const { WorkspaceService } = await import('../../src/core/workspace.js'); + const { WorkspaceService } = await import('../../src/workspace/workspace.js'); const workspaceLayer = makeWorkspaceLayer(WorkspaceService, { model: 'nonexistent', apiKeyEnv: 'UNKNOWN_KEY', @@ -195,7 +195,7 @@ describe('createClient - API key validation', () => { mockFs(); const { LLMFactoryService } = await import('../../src/llm/port.js'); - const { WorkspaceService } = await import('../../src/core/workspace.js'); + const { WorkspaceService } = await import('../../src/workspace/workspace.js'); const workspaceLayer = makeWorkspaceLayer(WorkspaceService, { model: 'model-x', apiKeyEnv: 'API_KEY_A', @@ -232,7 +232,7 @@ describe('createClient - API key validation', () => { mockFs(); const { LLMFactoryService } = await import('../../src/llm/port.js'); - const { WorkspaceService } = await import('../../src/core/workspace.js'); + const { WorkspaceService } = await import('../../src/workspace/workspace.js'); const workspaceLayer = makeWorkspaceLayer(WorkspaceService, { model: 'model-x', apiKeyEnv: 'API_KEY_A', diff --git a/packages/codingcode/test/llm/openai-provider.test.ts b/packages/codingcode/test/llm/openai-provider.test.ts index 7c750c17..71f46cec 100644 --- a/packages/codingcode/test/llm/openai-provider.test.ts +++ b/packages/codingcode/test/llm/openai-provider.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { LLMStreamPart } from '../../src/llm/types.js'; +import type { LLMStreamPart } from '../../src/contracts/provider.js'; const generateText = vi.fn(); const streamText = vi.fn(); diff --git a/packages/codingcode/test/mcp/service.test.ts b/packages/codingcode/test/mcp/service.test.ts index 93aa8095..aa12ff7d 100644 --- a/packages/codingcode/test/mcp/service.test.ts +++ b/packages/codingcode/test/mcp/service.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Effect, Layer } from 'effect'; -import { z } from 'zod'; import { McpService } from '../../src/mcp/port.js'; import { HookService } from '../../src/hooks/port.js'; import { McpLayer } from '../../src/mcp/mcp.js'; @@ -187,7 +186,7 @@ describe('McpService granular methods', () => { await run(program); }); - it('converts MCP inputSchema into the tool parameters schema', async () => { + it('exposes MCP tools as pure-data specs, keeping the JSON Schema raw', async () => { mockConfigs = [ { name: 'typed', @@ -209,16 +208,18 @@ describe('McpService granular methods', () => { const program = Effect.gen(function* () { const mcp = yield* McpService; yield* mcp.connectServers(TEST_PROJECT, TEST_SESSION, ['typed']); - const [tool] = mcp.listProjectMcpTools(TEST_PROJECT); + const [tool] = yield* mcp.listProjectMcpTools(TEST_PROJECT); expect(tool).toBeDefined(); - expect(z.toJSONSchema(tool!.parameters)).toMatchObject({ + expect(tool!.server).toBe('typed'); + expect(tool!.name).toBe('query'); + expect(tool!.inputSchema).toMatchObject({ type: 'object', properties: { text: { type: 'string' } }, required: ['text'], }); - expect(() => tool!.parameters.parse({})).toThrow(); - expect(tool!.parameters.parse({ text: 'hello' })).toEqual({ text: 'hello' }); + // execute 已绑定 client,zod 化留给 tools 层 + expect(yield* tool!.execute({ text: 'hello' })).toBe('mock-result'); }); await run(program); diff --git a/packages/codingcode/test/memory/llm-resolver.test.ts b/packages/codingcode/test/memory/llm-resolver.test.ts index cccb2b95..5cf0702a 100644 --- a/packages/codingcode/test/memory/llm-resolver.test.ts +++ b/packages/codingcode/test/memory/llm-resolver.test.ts @@ -3,8 +3,8 @@ import { Effect } from 'effect'; import { resolveLLM } from '../../src/llm/llm-resolver.js'; import { LLMFactoryService } from '../../src/llm/port.js'; import { AgentError } from '../../src/core/error.js'; -import type { LLMClient } from '../../src/llm/client.js'; -import type { SelectableModel } from '../../src/llm/port.js'; +import type { LLMClient } from '../../src/contracts/provider.js'; +import type { SelectableModel } from '../../src/contracts/provider.js'; const { mockFindModel, mockCreateClient } = vi.hoisted(() => ({ mockFindModel: vi.fn(), diff --git a/packages/codingcode/test/plan/gate-pipeline.test.ts b/packages/codingcode/test/plan/gate-pipeline.test.ts index 81bc3022..89d44f7d 100644 --- a/packages/codingcode/test/plan/gate-pipeline.test.ts +++ b/packages/codingcode/test/plan/gate-pipeline.test.ts @@ -7,7 +7,7 @@ import { runPipeline } from '../../src/approval/approval.js'; import { createRuleEngine } from '../../src/approval/rule-engine.js'; import { HookService } from '../../src/hooks/port.js'; import { ApprovalWaitService } from '../../src/approval/wait-port.js'; -import type { ProfileName } from '../../src/core/types.js'; +import type { ProfileName } from '../../src/contracts/types.js'; import { useTempProjectBase } from '../helpers/project-base.js'; useTempProjectBase(); diff --git a/packages/codingcode/test/plan/policy.test.ts b/packages/codingcode/test/plan/policy.test.ts index 22a5c858..2e09926b 100644 --- a/packages/codingcode/test/plan/policy.test.ts +++ b/packages/codingcode/test/plan/policy.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { PLAN_ALLOWED_TOOLS } from '../../src/approval/types.js'; +import { PLAN_ALLOWED_TOOLS } from '../../src/contracts/permission.js'; describe('PLAN_ALLOWED_TOOLS', () => { it('contains only read tools and submit_plan', () => { diff --git a/packages/codingcode/test/security/plan-profile-restart.test.ts b/packages/codingcode/test/security/plan-profile-restart.test.ts index 7942adeb..ee542781 100644 --- a/packages/codingcode/test/security/plan-profile-restart.test.ts +++ b/packages/codingcode/test/security/plan-profile-restart.test.ts @@ -9,7 +9,7 @@ import { computePaths } from '../../src/core/path.js'; import { HookService } from '../../src/hooks/port.js'; import { ApprovalService } from '../../src/approval/port.js'; import { ApprovalWaitService } from '../../src/approval/wait-port.js'; -import type { ProfileName } from '../../src/core/types.js'; +import type { ProfileName } from '../../src/contracts/types.js'; import { useTempProjectBase } from '../helpers/project-base.js'; import { ApprovalLayer } from '../../src/approval/approval.js'; diff --git a/packages/codingcode/test/self/todo/service.test.ts b/packages/codingcode/test/self/todo/service.test.ts index b8fb0e66..05837607 100644 --- a/packages/codingcode/test/self/todo/service.test.ts +++ b/packages/codingcode/test/self/todo/service.test.ts @@ -1,12 +1,12 @@ import { describe, it, expect } from 'vitest'; import { Effect } from 'effect'; import { TodoService, countByStatus } from '../../../src/todo/port.js'; -import type { Todo } from '../../../src/todo/port.js'; +import type { TodoItem } from '../../../src/contracts/types.js'; import { TodoLayer } from '../../../src/todo/todo.js'; describe('TodoService', () => { it('write then read returns full list', async () => { - const plan: Todo[] = [ + const plan: TodoItem[] = [ { step: 'step 1', status: 'pending' }, { step: 'step 2', status: 'in_progress' }, { step: 'step 3', status: 'completed' }, @@ -68,7 +68,7 @@ describe('TodoService', () => { }); it('countByStatus counts correctly', () => { - const plan: Todo[] = [ + const plan: TodoItem[] = [ { step: 'a', status: 'pending' }, { step: 'b', status: 'completed' }, { step: 'c', status: 'pending' }, diff --git a/packages/codingcode/test/server/compact-route.test.ts b/packages/codingcode/test/server/compact-route.test.ts index 4907c976..fbf6570e 100644 --- a/packages/codingcode/test/server/compact-route.test.ts +++ b/packages/codingcode/test/server/compact-route.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Effect, Layer, ManagedRuntime } from 'effect'; import { createServer } from '../../src/server/index.js'; -import { WorkspaceService } from '../../src/core/workspace.js'; +import { WorkspaceService } from '../../src/workspace/workspace.js'; import { SessionService } from '../../src/session/port.js'; import { LLMFactoryService } from '../../src/llm/port.js'; import { ApprovalService } from '../../src/approval/port.js'; diff --git a/packages/codingcode/test/server/create-session-active-profile.test.ts b/packages/codingcode/test/server/create-session-active-profile.test.ts index 332d5ff2..a6a8051d 100644 --- a/packages/codingcode/test/server/create-session-active-profile.test.ts +++ b/packages/codingcode/test/server/create-session-active-profile.test.ts @@ -6,7 +6,7 @@ import { join } from 'path'; import { SessionService } from '../../src/session/port.js'; import { SessionLayer } from '../../src/session/session.js'; import { computePaths } from '../../src/core/path.js'; -import { WorkspaceService } from '../../src/core/workspace.js'; +import { WorkspaceService } from '../../src/workspace/workspace.js'; import { registerSessionsRoutes } from '../../src/server/routes/sessions.js'; import { useTempProjectBase } from '../helpers/project-base.js'; diff --git a/packages/codingcode/test/server/index.test.ts b/packages/codingcode/test/server/index.test.ts index e9b24123..02aea953 100644 --- a/packages/codingcode/test/server/index.test.ts +++ b/packages/codingcode/test/server/index.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from 'vitest'; import { Effect, Layer, ManagedRuntime } from 'effect'; import { createServer } from '../../src/server/index.js'; -import { WorkspaceService } from '../../src/core/workspace.js'; +import { WorkspaceService } from '../../src/workspace/workspace.js'; import { SessionService } from '../../src/session/port.js'; import { LLMFactoryService } from '../../src/llm/port.js'; import { ApprovalService } from '../../src/approval/port.js'; diff --git a/packages/codingcode/test/server/messages-fork-permission-mode.test.ts b/packages/codingcode/test/server/messages-fork-permission-mode.test.ts index 55bddbd7..28b24d77 100644 --- a/packages/codingcode/test/server/messages-fork-permission-mode.test.ts +++ b/packages/codingcode/test/server/messages-fork-permission-mode.test.ts @@ -11,7 +11,7 @@ import { computePaths } from '../../src/core/path.js'; import { HookService } from '../../src/hooks/port.js'; import { ApprovalWaitService } from '../../src/approval/wait-port.js'; import { AgentService } from '../../src/agent/port.js'; -import { WorkspaceService } from '../../src/core/workspace.js'; +import { WorkspaceService } from '../../src/workspace/workspace.js'; import { useTempProjectBase } from '../helpers/project-base.js'; useTempProjectBase(); diff --git a/packages/codingcode/test/server/plan-file-route.test.ts b/packages/codingcode/test/server/plan-file-route.test.ts index 85448a22..cd0f0131 100644 --- a/packages/codingcode/test/server/plan-file-route.test.ts +++ b/packages/codingcode/test/server/plan-file-route.test.ts @@ -7,7 +7,7 @@ import { mkdirSync, writeFileSync, utimesSync } from 'fs'; import { join } from 'path'; import { Hono } from 'hono'; import { registerSessionsRoutes } from '../../src/server/routes/sessions.js'; -import { WorkspaceService } from '../../src/core/workspace.js'; +import { WorkspaceService } from '../../src/workspace/workspace.js'; import { SessionService } from '../../src/session/port.js'; import { LLMFactoryService } from '../../src/llm/port.js'; import { ApprovalService } from '../../src/approval/port.js'; diff --git a/packages/codingcode/test/session/compute-paths.test.ts b/packages/codingcode/test/session/compute-paths.test.ts index 3f40296c..f7109dd9 100644 --- a/packages/codingcode/test/session/compute-paths.test.ts +++ b/packages/codingcode/test/session/compute-paths.test.ts @@ -5,7 +5,8 @@ import { randomUUID } from 'crypto'; import { Effect } from 'effect'; import { SessionService } from '../../src/session/port.js'; import { SessionLayer } from '../../src/session/session.js'; -import { computePaths, sessionJsonlPathFromCwd, projectSessionsDir } from '../../src/core/path.js'; +import { computePaths, projectSessionsDir } from '../../src/core/path.js'; +import { sessionJsonlPathFromCwd } from '../../src/session/file-ops.js'; import { normalizePath, encodeProjectPath } from '../../src/core/path.js'; import { useTempProjectBase } from '../helpers/project-base.js'; diff --git a/packages/codingcode/test/session/filter-ui.test.ts b/packages/codingcode/test/session/filter-ui.test.ts index ed21a905..186cf448 100644 --- a/packages/codingcode/test/session/filter-ui.test.ts +++ b/packages/codingcode/test/session/filter-ui.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import type { SessionEvent } from '../../src/session/types.js'; +import type { SessionEvent } from '../../src/contracts/session.js'; import { filterForUI, sessionEventsToTurns } from '../../src/session/session.js'; function makeBaseEvents(extra: SessionEvent[] = []): SessionEvent[] { diff --git a/packages/codingcode/test/session/fork.test.ts b/packages/codingcode/test/session/fork.test.ts index 10cf121c..9865916f 100644 --- a/packages/codingcode/test/session/fork.test.ts +++ b/packages/codingcode/test/session/fork.test.ts @@ -7,7 +7,7 @@ import { SessionService } from '../../src/session/port.js'; import { SessionLayer } from '../../src/session/session.js'; import { filterForContext, buildContextMessages } from '../../src/context/context.js'; import { readHistory } from '../../src/session/file-ops.js'; -import type { SessionIndex, SessionEvent } from '../../src/session/types.js'; +import type { SessionIndex, SessionEvent } from '../../src/contracts/session.js'; import { useTempProjectBase } from '../helpers/project-base.js'; import { computePaths } from '../../src/core/path.js'; diff --git a/packages/codingcode/test/session/index-write-sync.test.ts b/packages/codingcode/test/session/index-write-sync.test.ts index d03b8582..cb3af999 100644 --- a/packages/codingcode/test/session/index-write-sync.test.ts +++ b/packages/codingcode/test/session/index-write-sync.test.ts @@ -7,7 +7,7 @@ import { SessionService } from '../../src/session/port.js'; import { SessionLayer } from '../../src/session/session.js'; import { encodeProjectPath, computePaths } from '../../src/core/path.js'; -import type { SessionIndex } from '../../src/session/types.js'; +import type { SessionIndex } from '../../src/contracts/session.js'; import { useTempProjectBase } from '../helpers/project-base.js'; const base = useTempProjectBase(); diff --git a/packages/codingcode/test/session/load-create.test.ts b/packages/codingcode/test/session/load-create.test.ts index 72f5f351..2bdd9833 100644 --- a/packages/codingcode/test/session/load-create.test.ts +++ b/packages/codingcode/test/session/load-create.test.ts @@ -7,7 +7,7 @@ import { SessionService } from '../../src/session/port.js'; import { SessionLayer } from '../../src/session/session.js'; import { AgentError } from '../../src/core/error.js'; import { encodeProjectPath, computePaths } from '../../src/core/path.js'; -import type { SessionIndex } from '../../src/session/types.js'; +import type { SessionIndex } from '../../src/contracts/session.js'; import { useTempProjectBase } from '../helpers/project-base.js'; const base = useTempProjectBase(); diff --git a/packages/codingcode/test/session/prompt-estimate.test.ts b/packages/codingcode/test/session/prompt-estimate.test.ts index 1d543a04..10d80b79 100644 --- a/packages/codingcode/test/session/prompt-estimate.test.ts +++ b/packages/codingcode/test/session/prompt-estimate.test.ts @@ -8,9 +8,9 @@ import { SessionLayer } from '../../src/session/session.js'; import { estimatePromptTokensFrom } from '../../src/context/context.js'; import { readHistory } from '../../src/session/file-ops.js'; -import { estimateTokensForContent } from '../../src/core/util.js'; +import { estimateTokensForContent } from '../../src/context/tokens.js'; import { encodeProjectPath, computePaths } from '../../src/core/path.js'; -import type { SessionIndex } from '../../src/session/types.js'; +import type { SessionIndex } from '../../src/contracts/session.js'; import { useTempProjectBase } from '../helpers/project-base.js'; const base = useTempProjectBase(); diff --git a/packages/codingcode/test/session/rollback.test.ts b/packages/codingcode/test/session/rollback.test.ts index 5295ace5..b8651691 100644 --- a/packages/codingcode/test/session/rollback.test.ts +++ b/packages/codingcode/test/session/rollback.test.ts @@ -4,7 +4,7 @@ import { join } from 'path'; import { randomUUID } from 'crypto'; import { filterForContext, buildContextMessages } from '../../src/context/context.js'; import { readHistory } from '../../src/session/file-ops.js'; -import type { SessionIndex } from '../../src/session/types.js'; +import type { SessionIndex } from '../../src/contracts/session.js'; import { useTempProjectBase } from '../helpers/project-base.js'; const base = useTempProjectBase(); diff --git a/packages/codingcode/test/session/session-jsonl-path.test.ts b/packages/codingcode/test/session/session-jsonl-path.test.ts index ee181602..fae54972 100644 --- a/packages/codingcode/test/session/session-jsonl-path.test.ts +++ b/packages/codingcode/test/session/session-jsonl-path.test.ts @@ -5,8 +5,8 @@ import { Effect } from 'effect'; import { SessionService } from '../../src/session/port.js'; import { SessionLayer } from '../../src/session/session.js'; -import { deleteSession } from '../../src/session/file-ops.js'; -import { sessionJsonlPathFromCwd, computePaths } from '../../src/core/path.js'; +import { deleteSession, sessionJsonlPathFromCwd } from '../../src/session/file-ops.js'; +import { computePaths } from '../../src/core/path.js'; import { useTempProjectBase } from '../helpers/project-base.js'; const base = useTempProjectBase(); diff --git a/packages/codingcode/test/session/store-compact-usage.test.ts b/packages/codingcode/test/session/store-compact-usage.test.ts index 86a0890c..77737b65 100644 --- a/packages/codingcode/test/session/store-compact-usage.test.ts +++ b/packages/codingcode/test/session/store-compact-usage.test.ts @@ -7,7 +7,7 @@ import { SessionService } from '../../src/session/port.js'; import { SessionLayer } from '../../src/session/session.js'; import { computePaths } from '../../src/core/path.js'; -import type { SessionIndex } from '../../src/session/types.js'; +import type { SessionIndex } from '../../src/contracts/session.js'; import { useTempProjectBase } from '../helpers/project-base.js'; const base = useTempProjectBase(); diff --git a/packages/codingcode/test/session/store-diff-rebuild.test.ts b/packages/codingcode/test/session/store-diff-rebuild.test.ts index 64f4a761..04a608e2 100644 --- a/packages/codingcode/test/session/store-diff-rebuild.test.ts +++ b/packages/codingcode/test/session/store-diff-rebuild.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import type { SessionEvent } from '../../src/session/types.js'; +import type { SessionEvent } from '../../src/contracts/session.js'; import { sessionEventsToTurns } from '../../src/session/session.js'; describe('sessionEventsToTurns', () => { diff --git a/packages/codingcode/test/session/store-rollback-usage.test.ts b/packages/codingcode/test/session/store-rollback-usage.test.ts index 9a1b9936..fa195c02 100644 --- a/packages/codingcode/test/session/store-rollback-usage.test.ts +++ b/packages/codingcode/test/session/store-rollback-usage.test.ts @@ -7,7 +7,7 @@ import { SessionService } from '../../src/session/port.js'; import { SessionLayer } from '../../src/session/session.js'; import { computePaths } from '../../src/core/path.js'; -import type { SessionIndex } from '../../src/session/types.js'; +import type { SessionIndex } from '../../src/contracts/session.js'; import { useTempProjectBase } from '../helpers/project-base.js'; const base = useTempProjectBase(); diff --git a/packages/codingcode/test/session/types-export.test.ts b/packages/codingcode/test/session/types-export.test.ts index e155e1ba..5068818e 100644 --- a/packages/codingcode/test/session/types-export.test.ts +++ b/packages/codingcode/test/session/types-export.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import type { SessionStoreState } from '../../src/session/types.js'; +import type { SessionStoreState } from '../../src/contracts/session.js'; describe('SessionStoreState export', () => { it('contains only cwd as its path source', () => { diff --git a/packages/codingcode/test/session/ui-history-rollback.test.ts b/packages/codingcode/test/session/ui-history-rollback.test.ts index 0d303d27..db73b85f 100644 --- a/packages/codingcode/test/session/ui-history-rollback.test.ts +++ b/packages/codingcode/test/session/ui-history-rollback.test.ts @@ -5,7 +5,7 @@ import { randomUUID } from 'crypto'; import { filterForContext, buildContextMessages } from '../../src/context/context.js'; import { readHistory } from '../../src/session/file-ops.js'; import { filterForUI } from '../../src/session/session.js'; -import type { SessionEvent, SessionIndex } from '../../src/session/types.js'; +import type { SessionEvent, SessionIndex } from '../../src/contracts/session.js'; import { useTempProjectBase } from '../helpers/project-base.js'; const base = useTempProjectBase(); diff --git a/packages/codingcode/test/session/view-assembly.test.ts b/packages/codingcode/test/session/view-assembly.test.ts index fdc4d9a2..252dbce2 100644 --- a/packages/codingcode/test/session/view-assembly.test.ts +++ b/packages/codingcode/test/session/view-assembly.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { filterForContext, buildContextMessages } from '../../src/context/context.js'; -import type { SessionEvent } from '../../src/session/types.js'; +import type { SessionEvent } from '../../src/contracts/session.js'; function toMessages(events: SessionEvent[]) { const { visible, compactedTurnIds } = filterForContext(events); diff --git a/packages/codingcode/test/subagent/dispatch-end-to-end.test.ts b/packages/codingcode/test/subagent/dispatch-end-to-end.test.ts index e1c52723..be4bf91a 100644 --- a/packages/codingcode/test/subagent/dispatch-end-to-end.test.ts +++ b/packages/codingcode/test/subagent/dispatch-end-to-end.test.ts @@ -5,22 +5,15 @@ import { join } from 'path'; import { tmpdir } from 'os'; import { AgentLayer } from '../../src/agent/agent.js'; import { ToolEnvLayer } from '../../src/agent/tool-env.js'; -import { ToolCatalogLayer } from '../../src/agent/tool-catalog.js'; import { AgentService } from '../../src/agent/port.js'; -import { - SessionPort, - ToolExecutorPort, - CheckpointPort, - HookPort, - ApprovalPort, - SkillPort, - McpPort, - ContextPort, - MemoryPort, - LlmPort, - RulesPort, - TodoPort, -} from '../../src/agent/deps.js'; +import { ApprovalService } from '../../src/approval/port.js'; +import { CheckpointService } from '../../src/checkpoint/port.js'; +import { ContextService } from '../../src/context/port.js'; +import { LLMFactoryService } from '../../src/llm/port.js'; +import { MemoryService } from '../../src/memory/port.js'; +import { RulesService } from '../../src/rules/port.js'; +import { SkillService } from '../../src/skills/port.js'; +import { ToolExecutorService } from '../../src/tools/port.js'; import { SessionLayer } from '../../src/session/session.js'; import { SessionService } from '../../src/session/port.js'; import { HookService } from '../../src/hooks/port.js'; @@ -29,9 +22,9 @@ import { SubagentRunnerService } from '../../src/subagent/port.js'; import { TodoService } from '../../src/todo/port.js'; import { readHistory } from '../../src/session/file-ops.js'; import { encodeProjectPath, normalizePath, setProjectBaseDir, computePaths } from '../../src/core/path.js'; -import type { Message } from '../../src/core/types.js'; -import type { LLMClient } from '../../src/llm/client.js'; -import type { FrameBody } from '../../src/core/frame.js'; +import type { Message } from '../../src/contracts/types.js'; +import type { LLMClient } from '../../src/contracts/provider.js'; +import type { FrameBody } from '../../src/contracts/frame.js'; function makeMockLLM(content: string): LLMClient { return { @@ -75,67 +68,65 @@ function readMessages(transcriptPath: string): Message[] { * real file-backed SessionLayer, everything else mocked. This mirrors how * the app is wired in layer.ts while keeping each dependency explicit. */ -// Real SessionService narrowed to the Agent's SessionPort (mirrors layer.ts's adapter). -const SessionPortLayer = Layer.effect(SessionPort, Effect.gen(function* () { - const svc = yield* SessionService; - return { - load: svc.load.bind(svc), - create: svc.create.bind(svc), - recordUser: svc.recordUser.bind(svc), - recordSystem: svc.recordSystem.bind(svc), - recordAssistant: svc.recordAssistant.bind(svc), - recordToolResult: svc.recordToolResult.bind(svc), - setPermissionMode: svc.setPermissionMode.bind(svc), - setActiveProfile: svc.setActiveProfile.bind(svc), - }; -})).pipe(Layer.provide(SessionLayer)); +// agent 依赖的全部宽服务 stub。 +const McpMock = Layer.succeed(McpService, { + syncConnections: () => Effect.void, + listProjectMcpTools: () => Effect.succeed([]), +} as any); + +const HookMock = Layer.succeed(HookService, { + register: () => Effect.succeed(() => {}), + registerDecision: () => Effect.succeed(() => {}), + emit: () => Effect.succeed(undefined), + emitDecision: () => Effect.succeed(null), + reloadUserHooks: () => Effect.succeed(undefined), + disposeSession: () => Effect.void, +} as any); + +const TodoMock = Layer.succeed(TodoService, { read: () => [], write: () => {}, reset: () => {} } as any); + +const SubagentMock = Layer.succeed(SubagentRunnerService, {} as any); -// Narrow agent ports + TodoService required to build the real AgentLayer. const AgentDeps = Layer.mergeAll( - SessionPortLayer, - Layer.succeed(ToolExecutorPort, { executeBatch: () => Effect.succeed([]) } as any), - Layer.succeed(CheckpointPort, { + SessionLayer, + Layer.succeed(ToolExecutorService, { + executeBatch: () => Effect.succeed([]), + prepare: () => Effect.succeed({ tools: [], lookup: () => undefined }), + } as any), + Layer.succeed(CheckpointService, { snapshotBaseline: () => Effect.void, snapshotFinal: () => Effect.void, } as any), - Layer.succeed(HookPort, { - emit: () => Effect.succeed(undefined), - emitDecision: () => Effect.succeed(null), - disposeSession: () => Effect.void, - } as any), - Layer.succeed(ApprovalPort, { + Layer.succeed(ApprovalService, { evaluate: () => Effect.succeed({ type: 'allow' }), } as any), - Layer.succeed(SkillPort, { + Layer.succeed(SkillService, { extractSkill: (_cwd: string, query: string) => Effect.succeed([undefined, query]), } as any), - Layer.succeed(McpPort, { - syncConnections: () => Effect.void, - listProjectMcpTools: () => [], - } as any), - Layer.succeed(ContextPort, { + Layer.succeed(ContextService, { willCompact: async () => false, assemblePayload: async (transcriptPath: string) => readMessages(transcriptPath), } as any), - Layer.succeed(MemoryPort, { + Layer.succeed(MemoryService, { loadMemoryForPrompt: () => '', flushSessionToMemory: () => Promise.resolve({ written: false, bytes: 0 }), } as any), - Layer.succeed(LlmPort, { + Layer.succeed(LLMFactoryService, { getLLMClient: () => Effect.succeed(makeMockLLM('subagent final answer') as LLMClient), } as any), - Layer.succeed(RulesPort, { + Layer.succeed(RulesService, { getAllRules: () => '', evictProjectRules: () => {}, } as any), - Layer.succeed(TodoPort, { read: () => [] } as any), + HookMock, + McpMock, + TodoMock, + SubagentMock, // ToolEnvPort 在 getToolEnv 运行时从外层 Runtime 解析具体服务(见 Runtime 定义) - ToolEnvLayer, - // ToolCatalogPort:静态内置 + profile 工具的装配(同 layer.ts) - ToolCatalogLayer + ToolEnvLayer ); -// Real AgentService built on the real SessionPort + stubbed narrow ports. +// Real AgentService built on the real SessionService + stubbed wide services. const AgentWired = AgentLayer.pipe(Layer.provide(AgentDeps as any)); // Runtime exposed to the tests: real AgentService + SessionService, plus the @@ -143,21 +134,10 @@ const AgentWired = AgentLayer.pipe(Layer.provide(AgentDeps as any)); const Runtime = Layer.mergeAll( AgentWired, SessionLayer, - Layer.succeed(HookService, { - register: () => Effect.succeed(() => {}), - registerDecision: () => Effect.succeed(() => {}), - emit: () => Effect.succeed(undefined), - emitDecision: () => Effect.succeed(null), - reloadUserHooks: () => Effect.succeed(undefined), - disposeSession: () => Effect.void, - } as any), - Layer.succeed(McpService, { - syncConnections: () => Effect.void, - listProjectMcpTools: () => [], - } as any), - Layer.succeed(SubagentRunnerService, {} as any), - // ToolEnvLayer.getToolEnv 运行时从外层解析 TodoService(工具执行期依赖) - Layer.succeed(TodoService, { read: () => [], write: () => {}, reset: () => {} } as any) + HookMock, + McpMock, + SubagentMock, + TodoMock ); function run(eff: Effect.Effect): Promise { diff --git a/packages/codingcode/test/subagent/dispatch.test.ts b/packages/codingcode/test/subagent/dispatch.test.ts index 6d802fab..f8f1a8f8 100644 --- a/packages/codingcode/test/subagent/dispatch.test.ts +++ b/packages/codingcode/test/subagent/dispatch.test.ts @@ -4,8 +4,8 @@ import { dispatchAgentTool } from '../../src/tools/domains/subagent/dispatch.js' import { HookService } from '../../src/hooks/port.js'; import { McpService } from '../../src/mcp/port.js'; import { SubagentRunnerService } from '../../src/subagent/port.js'; -import type { ToolExecCtx } from '../../src/tools/types.js'; -import type { FrameBody } from '../../src/core/frame.js'; +import type { ToolExecCtx } from '../../src/contracts/tool.js'; +import type { FrameBody } from '../../src/contracts/frame.js'; const mockHooks = { register: () => Effect.succeed(() => {}), diff --git a/packages/codingcode/test/tools/catalog.test.ts b/packages/codingcode/test/tools/catalog.test.ts index 2f46053f..dfd9e7ba 100644 --- a/packages/codingcode/test/tools/catalog.test.ts +++ b/packages/codingcode/test/tools/catalog.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; -import { z } from 'zod'; +import { Effect } from 'effect'; import { createToolCatalog } from '../../src/tools/catalog.js'; +import type { McpToolSpec } from '../../src/contracts/mcp.js'; const BUILD_NAMES = [ 'read_file', @@ -40,14 +41,47 @@ describe('createToolCatalog', () => { }); it('merges dynamic MCP tools into the catalog', () => { - const mcp = { - name: 'mcp_thing', + const spec: McpToolSpec = { + server: 'srv', + name: 'thing', description: 'a thing', - parameters: z.object({}), - execute: () => ({}) as any, + inputSchema: {}, + execute: () => Effect.succeed('ok'), }; - const { tools, lookup } = createToolCatalog(['read_file'], [mcp]); - expect(tools.map((t) => t.name)).toEqual(['read_file', 'mcp_thing']); - expect(lookup('mcp_thing')?.name).toBe('mcp_thing'); + const { tools, lookup } = createToolCatalog(['read_file'], [spec]); + expect(tools.map((t) => t.name)).toEqual(['read_file', 'srv:thing']); + expect(lookup('srv:thing')?.name).toBe('srv:thing'); + }); + + it('turns an MCP spec JSON Schema into the tool parameters schema', () => { + const spec: McpToolSpec = { + server: 'srv', + name: 'thing', + description: '', + inputSchema: { + type: 'object', + properties: { text: { type: 'string' } }, + required: ['text'], + }, + execute: () => Effect.succeed('ok'), + }; + const { lookup } = createToolCatalog([], [spec]); + const tool = lookup('srv:thing')!; + expect(() => tool.parse({})).toThrow(); + expect(tool.parse({ text: 'hello' })).toEqual({ text: 'hello' }); + }); + + it('forwards MCP spec execute into the tool runner', async () => { + const spec: McpToolSpec = { + server: 'srv', + name: 'thing', + description: '', + inputSchema: {}, + execute: (args) => Effect.succeed(`called:${String(args.n)}`), + }; + const { lookup } = createToolCatalog([], [spec]); + const tool = lookup('srv:thing')!; + const out = await Effect.runPromise(tool.execute({ n: 1 }) as Effect.Effect); + expect(out).toBe('called:1'); }); }); diff --git a/packages/codingcode/test/types/type-collapse.test.ts b/packages/codingcode/test/types/type-collapse.test.ts index 155b5f17..2c5145e1 100644 --- a/packages/codingcode/test/types/type-collapse.test.ts +++ b/packages/codingcode/test/types/type-collapse.test.ts @@ -1,26 +1,18 @@ import { describe, expect, it } from 'vitest'; -import type { UITurn, UITurnItem } from '../../src/session/port.js'; +import type { UITurn, UITurnItem } from '../../src/contracts/session.js'; import type { ForkResult, RollbackContextResult } from '../../src/client/contracts.js'; -import type { TodoItem as CoreTodoItem, TokenUsage as CoreTokenUsage } from '../../src/core/types.js'; -import type { TodoItem as PortTodoItem, Todo as PortTodo } from '../../src/todo/port.js'; -import type { TokenUsage as SessionTokenUsage } from '../../src/session/types.js'; +import type { TodoItem, TokenUsage } from '../../src/contracts/types.js'; type AssertNotAny = 0 extends 1 & T ? never : T; type _UITurnNotAny = AssertNotAny; -type _TodoNotAny = AssertNotAny; +type _TodoNotAny = AssertNotAny; +type _UsageNotAny = AssertNotAny; // 同构断言:两侧互相可赋值才通过 type _ForkTurnsIsUITurn = ForkResult['turns'] extends UITurn[] ? true : never; type _ContextTurnsIsUITurn = RollbackContextResult['turns'] extends UITurn[] ? true : never; -type _PortTodoIsCoreTodo = PortTodoItem extends CoreTodoItem ? true : never; -type _CoreTodoIsPortTodo = CoreTodoItem extends PortTodoItem ? true : never; -type _TodoAliasIsItem = PortTodo extends PortTodoItem ? true : never; - -type _SessionUsageIsCoreUsage = SessionTokenUsage extends CoreTokenUsage ? true : never; -type _CoreUsageIsSessionUsage = CoreTokenUsage extends SessionTokenUsage ? true : never; - describe('类型收口', () => { it('UITurn.status 不再退化为 string', () => { const turn: UITurn = { id: '1', items: [], status: 'completed' }; @@ -50,10 +42,10 @@ describe('类型收口', () => { }); it('TodoItem.status 是字面量联合而非 string', () => { - const todo: PortTodoItem = { step: 'do it', status: 'in_progress' }; + const todo: TodoItem = { step: 'do it', status: 'in_progress' }; expect(todo.status).toBe('in_progress'); // @ts-expect-error status 只能是 pending / in_progress / completed - const bad: PortTodoItem = { step: 'x', status: 'whatever' }; + const bad: TodoItem = { step: 'x', status: 'whatever' }; expect(bad).toBeDefined(); }); }); diff --git a/packages/desktop/src/agent/ProfileIndicator.tsx b/packages/desktop/src/agent/ProfileIndicator.tsx index d8829c9e..86024f49 100644 --- a/packages/desktop/src/agent/ProfileIndicator.tsx +++ b/packages/desktop/src/agent/ProfileIndicator.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { Eye, Hammer, Loader2 } from 'lucide-react'; import { useAgentProfile } from '../hooks/useAgent'; import { useAgentStore } from '../stores/agent.store'; -import type { ProfileName } from '@codingcode/core/core/types'; +import type { ProfileName } from '@codingcode/core/contracts/types'; interface ProfileIndicatorProps { sessionId: string | null; diff --git a/packages/desktop/src/hooks/useAgent.ts b/packages/desktop/src/hooks/useAgent.ts index 0453f182..921c37cc 100644 --- a/packages/desktop/src/hooks/useAgent.ts +++ b/packages/desktop/src/hooks/useAgent.ts @@ -4,8 +4,8 @@ import { useWorkspaceStore } from '../stores/workspace.store'; import { useRollbackStore } from '../stores/rollback.store'; import { agentClient } from '../lib/core-api'; import { createStreamState, reduceFrame, type StreamEffects } from '../lib/frame-reducer'; -import type { ProfileName } from '@codingcode/core/core/types'; -import type { PermissionMode } from '@codingcode/core/approval/types'; +import type { ProfileName } from '@codingcode/core/contracts/types'; +import type { PermissionMode } from '@codingcode/core/contracts/permission'; import { ApiError } from '../lib/api'; import { listModels, diff --git a/packages/desktop/src/lib/api.ts b/packages/desktop/src/lib/api.ts index b829fc52..9f20a012 100644 --- a/packages/desktop/src/lib/api.ts +++ b/packages/desktop/src/lib/api.ts @@ -1,4 +1,4 @@ -import { ApiError } from '@codingcode/core/core/error'; +import { ApiError } from '@codingcode/core/contracts/error'; export const API_BASE = `http://127.0.0.1:${new URLSearchParams(window.location.search).get('apiPort')}`; diff --git a/packages/desktop/src/lib/core-api.ts b/packages/desktop/src/lib/core-api.ts index 7fa1df3e..474cc1c5 100644 --- a/packages/desktop/src/lib/core-api.ts +++ b/packages/desktop/src/lib/core-api.ts @@ -1,15 +1,15 @@ import { API_BASE, api } from './api'; import { createHttpClients, type AgentRuntimeClient } from '@codingcode/core/client'; -import type { PermissionMode } from '@codingcode/core/approval/types'; -import type { ProfileName, TokenUsage } from '@codingcode/core/core/types'; +import type { PermissionMode } from '@codingcode/core/contracts/permission'; +import type { ProfileName, TokenUsage } from '@codingcode/core/contracts/types'; import type { CheckpointDiff, CodeRollbackResult, RollbackPreviewDiff, } from '@codingcode/core/checkpoint/types'; -import type { UITurn } from '@codingcode/core/session/port'; -import type { McpServerConfig } from '@codingcode/core/mcp/types'; -import type { UserHookConfig } from '@codingcode/core/hooks/types'; +import type { UITurn } from '@codingcode/core/contracts/session'; +import type { McpServerConfig } from '@codingcode/core/contracts/mcp'; +import type { UserHookConfig } from '@codingcode/core/contracts/hooks'; const clients = createHttpClients(API_BASE); diff --git a/packages/desktop/src/lib/frame-reducer.ts b/packages/desktop/src/lib/frame-reducer.ts index 1949c5b5..3e57e4cf 100644 --- a/packages/desktop/src/lib/frame-reducer.ts +++ b/packages/desktop/src/lib/frame-reducer.ts @@ -1,4 +1,4 @@ -import type { Frame, ToolOutcome } from '@codingcode/core/core/frame'; +import type { Frame, ToolOutcome } from '@codingcode/core/contracts/frame'; import type { Item, TodoItem } from '@shared/types'; export interface StreamState { diff --git a/packages/desktop/src/settings/HooksPanel.tsx b/packages/desktop/src/settings/HooksPanel.tsx index 35ee794e..b3ec55be 100644 --- a/packages/desktop/src/settings/HooksPanel.tsx +++ b/packages/desktop/src/settings/HooksPanel.tsx @@ -9,7 +9,7 @@ import { setHookDisabled, resetHookDisabled, } from '../lib/core-api'; -import type { UserHookConfig, HookPoint } from '@codingcode/core/hooks/types'; +import type { UserHookConfig, HookPoint } from '@codingcode/core/contracts/hooks'; interface HookEntry { name: string; diff --git a/packages/desktop/src/settings/McpPanel.tsx b/packages/desktop/src/settings/McpPanel.tsx index 9add777d..cca4650a 100644 --- a/packages/desktop/src/settings/McpPanel.tsx +++ b/packages/desktop/src/settings/McpPanel.tsx @@ -9,7 +9,7 @@ import { updateMcpServer, deleteMcpServer, } from '../lib/core-api'; -import type { McpServerConfig } from '@codingcode/core/mcp/types'; +import type { McpServerConfig } from '@codingcode/core/contracts/mcp'; interface McpEntry { name: string; diff --git a/packages/desktop/src/stores/agent.store.ts b/packages/desktop/src/stores/agent.store.ts index 9805a6e2..40e988bc 100644 --- a/packages/desktop/src/stores/agent.store.ts +++ b/packages/desktop/src/stores/agent.store.ts @@ -2,8 +2,8 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import type { Thread, Turn, Item, TodoItem } from '@shared/types'; -import type { ProfileName } from '@codingcode/core/core/types'; -import type { PermissionMode } from '@codingcode/core/approval/types'; +import type { ProfileName } from '@codingcode/core/contracts/types'; +import type { PermissionMode } from '@codingcode/core/contracts/permission'; import { buildToolDiff } from '../lib/diff-compute'; import { createDebouncedStorage, normalizeCwd } from './storage'; import { useRollbackStore } from './rollback.store'; diff --git a/packages/tui/src/index.tsx b/packages/tui/src/index.tsx index 9d3eb109..0df6e201 100644 --- a/packages/tui/src/index.tsx +++ b/packages/tui/src/index.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { render } from 'ink'; import { App } from './components/App.js'; -import type { Frame } from '@codingcode/core/core/frame'; +import type { Frame } from '@codingcode/core/contracts/frame'; import { createDirectAgentClient } from '@codingcode/core/direct/agent-runtime'; import { createDirectSessionClient } from '@codingcode/core/direct/sessions'; import { createDirectSettingsClient } from '@codingcode/core/direct/settings'; import { createDirectModelClient } from '@codingcode/core/direct/models'; -import type { LLMClient } from '@codingcode/core/llm/client'; +import type { LLMClient } from '@codingcode/core/contracts/provider'; import type { AppRuntime } from '@codingcode/core'; export type { Frame }; @@ -27,11 +27,11 @@ export interface TuiClient { getPermissionMode(input: { sessionId: string; cwd: string; - }): Promise; + }): Promise; setPermissionMode(input: { sessionId: string; cwd: string; - mode: import('@codingcode/core/approval/types').PermissionMode; + mode: import('@codingcode/core/contracts/permission').PermissionMode; }): Promise; resumeSession(sid: string): Promise; }