Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ Coding Code 是 AI 编程助手。
- `scheduler`:定时调度
- `todo`:任务清单
- `rules`:全局 / 项目级规则装载
- `workspace`:工作区信息
- `server`:HTTP / SSE 入口
- `client`:HTTP 客户端(`AgentClient` 的实现)
- `direct`:进程内直连端口,免 HTTP 的 runtime / sessions / settings / models 接口
Expand All @@ -51,7 +50,7 @@ Coding Code 是 AI 编程助手。
| 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 |
| L2 实现 | `tools/`、`hooks/`、`session/`、`approval/`、`llm/`、`mcp/`、`context/` … | L0 + L1 |
| L3 组合根 | `layer.ts`、`agent/tool-env.ts` | 全部 |

**架构边界硬规则**(由 `packages/codingcode/test/architecture/boundaries.test.ts` 静态断言,共 29 项):
Expand Down
13 changes: 6 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ Coding Code 是 AI 编程助手。
- `scheduler`:定时调度
- `todo`:任务清单
- `rules`:全局 / 项目级规则装载
- `workspace`:工作区信息
- `server`:HTTP / SSE 入口
- `client`:HTTP 客户端(`AgentClient` 的实现)
- `direct`:进程内直连端口,免 HTTP 的 runtime / sessions / settings / models 接口
Expand All @@ -42,22 +41,22 @@ Coding Code 是 AI 编程助手。

## 架构要求

**依赖倒置**:所有非叶子模块利用 `deps.ts`(消费者侧窄端口)或 `port.ts`(叶子侧宽契约)声明自己需要的接口和类型定义,使调用者不需要依赖实现方;只允许依赖下层模块。
**依赖倒置**:所有非叶子模块利用 `port.ts`(宽契约;agent 自持的装配端口也在 `agent/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` | 全部 |
| L1' 端口契约 | 各 `xxx/port.ts`(含 `agent/port.ts` 的装配端口) | `core/` + `contracts/` |
| L2 实现 | `tools/`、`hooks/`、`session/`、`approval/`、`llm/`、`mcp/`、`context/` … | L0 + L1 |
| L3 组合根 | `layer.ts`、`agent/tool-env.ts` | 全部 |

**架构边界硬规则**(由 `packages/codingcode/test/architecture/boundaries.test.ts` 静态断言,共 28 项):
**架构边界硬规则**(由 `packages/codingcode/test/architecture/boundaries.test.ts` 静态断言,共 29 项):

- **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` 可见
- **R2** 实现不得依赖消费者模块:agent 自持的装配端口 `ToolEnvPort` 只在 `agent/` 内部出现
- **R3** `core/` 零内部依赖:不引用 `core/` 之外的任何 src 模块
- **R4** 一个概念只允许一处类型定义,canonical 落点为 `contracts/`
- **准入**:`core/` 的 import 只能是 node 内置与同目录;`contracts/` 只引用 `core/`、同目录与第三方
Expand Down
8 changes: 4 additions & 4 deletions docs/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ Agent 判断技能相关后,使用 `read_file` 读取 `skillPath`,再按需
通过 `AgentClient` SDK 读取技能元数据:

```typescript
const client = await createHttpClient('http://localhost:8080');
const clients = createHttpClients('http://localhost:8080');

// 列出所有技能
const skills = await client.listSkills();
// 列出技能:cwd 为项目路径,空串走全局分支
const skills = await clients.settings.listSkills({ cwd: '/path/to/project' });
// 返回:Array<{ name: string, description: string, skillPath: string }>
```

也可通过 HTTP API:

| 路由 | 方法 | 说明 |
|------|------|------|
| `/api/settings/skills` | GET | 列出所有技能元数据 |
| `/api/settings/skills` | GET | 列出技能元数据;`?cwd=` 指定项目,缺省只返回全局技能 |
6 changes: 3 additions & 3 deletions packages/codingcode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ 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));
yield* hooks.reloadUserHooks(normalizedCwd);
yield* hooks.emit('agent.turn.start', { sessionId: '' });
yield* mcp.syncConnections(normalizedCwd);

let sessionId = opts.sessionId;
const llm = yield* llmFactory.getLLMClient();
Expand Down
12 changes: 3 additions & 9 deletions packages/codingcode/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ 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 './workspace/workspace.js';
import { ensureTempCwd } from './core/path.js';
import { findAvailablePort } from './server/port-discovery.js';
import { AgentError } from './core/error.js';
import { SchedulerService } from './scheduler/port.js';

async function main() {
const installRoot = process.cwd();
const { workspaceCwd, args } = parseWorkspaceArgs(process.argv.slice(2));
const args = process.argv.slice(2);
ensureUserConfig();
ensureTempCwd();
const config = loadConfig();

const serveOnly = args.includes('serve');
Expand All @@ -22,12 +22,6 @@ async function main() {
const rt = createAppRuntime();

const program = Effect.gen(function* () {
const ws = yield* WorkspaceService;
ws.init({ processRoot: installRoot, workspaceCwd });
if (workspaceCwd) {
console.log(`Workspace: ${ws.getWorkspaceCwd()}`);
}

const port = yield* Effect.tryPromise(() => findAvailablePort(basePort));
const llmFactory = yield* LLMFactoryService;

Expand Down
2 changes: 1 addition & 1 deletion packages/codingcode/src/client/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export interface SettingsClient {
createMcpServer(input: { cwd: string; server: McpServerConfig }): Promise<void>;
updateMcpServer(input: { cwd: string; name: string; server: McpServerConfig }): Promise<void>;
deleteMcpServer(input: { cwd: string; name: string }): Promise<void>;
listSkills(): Promise<Array<{ name: string; description: string; skillPath: string }>>;
listSkills(input: { cwd: string }): Promise<Array<{ name: string; description: string; skillPath: string }>>;
listHooks(input: { cwd: string }): Promise<UserHookConfig[]>;
createHook(input: { cwd: string; hook: UserHookConfig }): Promise<void>;
updateHook(input: { cwd: string; name: string; hook: UserHookConfig }): Promise<void>;
Expand Down
4 changes: 2 additions & 2 deletions packages/codingcode/src/client/http/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ export function createHttpSettingsClient(
await apiDelete(`/api/settings/mcp/${encodeURIComponent(name)}${qsCwd(cwd)}`);
},

async listSkills() {
return apiGet('/api/settings/skills');
async listSkills({ cwd }) {
return apiGet(`/api/settings/skills${qsCwd(cwd)}`);
},

async listHooks({ cwd }) {
Expand Down
22 changes: 21 additions & 1 deletion packages/codingcode/src/core/path.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { mkdirSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { join, resolve } from 'path';

/** 空值 / `'global'` 表示操作全局(home)配置,而非某个项目。 */
export function isGlobalCwd(cwd: string | undefined): boolean {
return !cwd || cwd === '' || cwd === 'global';
}

export function normalizePath(p: string): string {
let s = p.replaceAll('\\', '/');
Expand All @@ -26,6 +32,20 @@ export function getProjectBaseDir(): string {
return _projectBaseOverride ?? join(homedir(), '.codingcode', 'project');
}

/** 无 cwd 的请求共用的工作目录。 */
export function getTempCwd(): string {
return join(homedir(), '.codingcode', 'temp');
}

export function ensureTempCwd(): void {
mkdirSync(getTempCwd(), { recursive: true });
}

/** 请求级 cwd:请求没带 cwd 时落到共用的临时工作目录。 */
export function resolveCwd(cwd?: string): string {
return cwd ? resolve(cwd) : getTempCwd();
}

export interface SessionPaths {
sessionId: string;
cwd: string;
Expand Down
15 changes: 8 additions & 7 deletions packages/codingcode/src/direct/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { McpServerConfig, McpStatus } from '../contracts/mcp.js';
import { SkillService } from '../skills/port.js';
import type { PermissionMode } from '../contracts/permission.js';
import type { UserHookConfig } from '../contracts/hooks.js';
import { isGlobalCwd } from '../workspace/workspace.js';
import { isGlobalCwd, resolveCwd } from '../core/path.js';
import {
loadMcpConfig,
writeMcpConfig,
Expand Down Expand Up @@ -224,7 +224,7 @@ export function createDirectSettingsClient(rt: AppRuntime): SettingsClient {
},

async getMcpStatus({ cwd }) {
const projectCwd = isGlobalCwd(cwd) ? process.cwd() : cwd;
const projectCwd = resolveCwd(cwd);
const runtime = await rt.runPromise(
Effect.gen(function* () {
const mcp = yield* McpService;
Expand Down Expand Up @@ -287,17 +287,18 @@ export function createDirectSettingsClient(rt: AppRuntime): SettingsClient {
},

async setMcpDisabled({ name, disabled, cwd }) {
const projectCwd = resolveCwd(cwd);
if (isGlobalCwd(cwd)) {
setGlobalMcpDisabledState(name, disabled);
} else {
setProjectMcpDisabledState(cwd, name, disabled);
setProjectMcpDisabledState(projectCwd, name, disabled);
}
await rt.runPromise(
Effect.gen(function* () {
const mcp = yield* McpService;
return yield* disabled
? mcp.disable(isGlobalCwd(cwd) ? process.cwd() : cwd, name)
: mcp.enable(isGlobalCwd(cwd) ? process.cwd() : cwd, name);
? mcp.disable(projectCwd, name)
: mcp.enable(projectCwd, name);
})
);
},
Expand All @@ -318,11 +319,11 @@ export function createDirectSettingsClient(rt: AppRuntime): SettingsClient {
mcpDeleteServer(cwd, name);
},

async listSkills() {
async listSkills({ cwd }) {
return rt.runPromise(
Effect.gen(function* () {
const skill = yield* SkillService;
return yield* skill.getAll(process.cwd());
return yield* skill.getAll(resolveCwd(cwd));
})
);
},
Expand Down
12 changes: 5 additions & 7 deletions packages/codingcode/src/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,23 @@ import { AgentLayer } from './agent/agent.js';
import { ToolEnvLayer } from './agent/tool-env.js';
import { SubagentRunnerLayer } from './subagent/subagent.js';
import { SchedulerLayer } from './scheduler/scheduler.js';
import { WorkspaceService } from './workspace/workspace.js';

// base layers
const InfraLayer = Layer.mergeAll(
WorkspaceService.Default, HookLayer, RulesLayer, SkillLayer, McpLayer, ApprovalWaitLayer, TodoLayer,
HookLayer, RulesLayer, SkillLayer, McpLayer, ApprovalWaitLayer, TodoLayer,
);

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 ContextWithDeps = ContextLayer.pipe(Layer.provide(Layer.mergeAll(SessionLayer, LlmWithDeps)));
const MemoryWithDeps = MemoryLayer.pipe(Layer.provide(LlmWithDeps));
const ContextWithDeps = ContextLayer.pipe(Layer.provide(Layer.mergeAll(SessionLayer, LlmLayer)));
const MemoryWithDeps = MemoryLayer.pipe(Layer.provide(LlmLayer));

// agent 直接消费的宽服务集合
const AgentServiceLayers = Layer.mergeAll(
InfraLayer, SessionLayer, ToolExecutorWithDeps, ApprovalWithDeps,
ContextWithDeps, MemoryWithDeps, CheckpointLayer, LlmWithDeps,
ContextWithDeps, MemoryWithDeps, CheckpointLayer, LlmLayer,
);

// agent with deps
Expand All @@ -47,7 +45,7 @@ const SubagentWithDeps = SubagentRunnerLayer.pipe(Layer.provide(AgentWithDeps));

export const AppLayer = Layer.mergeAll(
InfraLayer,
LlmWithDeps,
LlmLayer,
ApprovalWithDeps,
SessionLayer,
ToolExecutorWithDeps,
Expand Down
10 changes: 4 additions & 6 deletions packages/codingcode/src/llm/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
import { resolve } from 'path';
import { Layer, Effect } from 'effect';
import { AgentError } from '../core/error.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';
import { loadConfig, updateActiveModel } from '@codingcode/infra/config';
import { LLMFactoryService } from './port.js';

export interface ModelDescriptor {
Expand Down Expand Up @@ -51,14 +50,13 @@
return result;
}

export const LlmLayer = Layer.effect(LLMFactoryService, Effect.gen(function* () {

Check warning on line 53 in packages/codingcode/src/llm/llm.ts

View workflow job for this annotation

GitHub Actions / lint

This generator function does not have 'yield'
const workspace = yield* WorkspaceService;
let catalog: ProviderCatalog | null = null;
let currentEntry: SelectableModel | null = null;
let currentClient: LLMClient | null = null;

function modelsFile(): string {
return resolve(workspace.getProcessRoot(), 'config/models.json');
return resolve(process.cwd(), 'config/models.json');
}

const loadCatalog = (): Effect.Effect<ProviderCatalog, AgentError> =>
Expand Down Expand Up @@ -105,7 +103,7 @@
getActiveEntry: (): Effect.Effect<SelectableModel, AgentError> =>
Effect.gen(function* () {
if (currentEntry) return currentEntry;
const cfg = workspace.getConfig().activeModel;
const cfg = loadConfig().activeModel;
if (!cfg) {
return yield* Effect.fail(
new AgentError(
Expand Down Expand Up @@ -198,7 +196,7 @@
getLLMClient: (): Effect.Effect<LLMClient, AgentError> =>
Effect.gen(function* () {
if (currentClient) return currentClient;
const cfg = workspace.getConfig().activeModel;
const cfg = loadConfig().activeModel;
if (!cfg) {
return yield* Effect.fail(
new AgentError(
Expand Down
9 changes: 2 additions & 7 deletions packages/codingcode/src/server/routes/messages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Hono } from 'hono';
import { Effect, ManagedRuntime } from 'effect';
import { AgentService } from '../../agent/port.js';
import { WorkspaceService } from '../../workspace/workspace.js';
import { resolveCwd } from '../../core/path.js';
import { errorResponse } from '../util.js';
import { createSseHandler } from '../handler.js';

Expand All @@ -13,12 +13,7 @@ export function registerMessagesRoutes(router: Hono, rt: ManagedRt): void {
router.post('/api/sessions/:id/messages', async (c) => {
let sessionId = c.req.param('id');
const { input, cwd } = await c.req.json<{ input: string; cwd: string }>();
const normalizedCwd = await rt.runPromise(
Effect.gen(function* () {
const ws = yield* WorkspaceService;
return ws.resolveWorkspaceCwd(cwd);
})
);
const normalizedCwd = resolveCwd(cwd);

const isNew = sessionId === '_' || !sessionId;
const runOpts: any = {
Expand Down
Loading
Loading