From d5db53858811bdbd00291b79f7a5ac76fc7bb5be Mon Sep 17 00:00:00 2001
From: phantom5099 <1011668688@qq.com>
Date: Sun, 6 Sep 2026 21:53:14 +0800
Subject: [PATCH 1/6] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E9=87=8D=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 5 +
.prettierignore | 2 +
.prettierrc | 3 +-
docs/configuration.md | 24 +-
docs/memory.md | 120 ++--
docs/tools.md | 19 +-
packages/codingcode/package.json | 40 +-
packages/codingcode/src/agent/agent.ts | 652 ++++++------------
packages/codingcode/src/agent/config.ts | 10 -
packages/codingcode/src/agent/deps.ts | 88 +++
packages/codingcode/src/agent/port.ts | 26 +
packages/codingcode/src/agent/profile.ts | 142 +++-
packages/codingcode/src/agent/prompt.ts | 104 +--
.../codingcode/src/agent/stream-adapter.ts | 14 +-
packages/codingcode/src/agent/todo.ts | 31 -
packages/codingcode/src/agent/types.ts | 59 +-
.../src/approval/{index.ts => approval.ts} | 31 +-
.../codingcode/src/approval/confirmation.ts | 2 +-
packages/codingcode/src/approval/pipeline.ts | 50 +-
packages/codingcode/src/approval/port.ts | 14 +
packages/codingcode/src/approval/presets.ts | 10 -
packages/codingcode/src/approval/wait-port.ts | 16 +
.../approval/{async-confirm.ts => wait.ts} | 9 +-
.../{checkpoint-service.ts => checkpoint.ts} | 9 +-
packages/codingcode/src/checkpoint/port.ts | 17 +
packages/codingcode/src/cli.ts | 4 +-
packages/codingcode/src/client/http.ts | 273 --------
.../src/client/http/agent-runtime.ts | 14 +-
packages/codingcode/src/client/http/models.ts | 2 +-
.../codingcode/src/client/http/sessions.ts | 2 +-
.../codingcode/src/client/http/settings.ts | 21 -
packages/codingcode/src/client/types.ts | 73 +-
.../src/context/{service.ts => context.ts} | 204 +++---
packages/codingcode/src/context/port.ts | 26 +
packages/codingcode/src/context/types.ts | 17 -
.../codingcode/src/direct/agent-runtime.ts | 57 +-
packages/codingcode/src/direct/models.ts | 4 +-
packages/codingcode/src/direct/sessions.ts | 18 +-
packages/codingcode/src/direct/settings.ts | 50 +-
.../src/hooks/{registry.ts => hooks.ts} | 76 +-
packages/codingcode/src/hooks/port.ts | 14 +
packages/codingcode/src/layer.ts | 231 ++++---
packages/codingcode/src/llm/llm-resolver.ts | 2 +-
.../codingcode/src/llm/{factory.ts => llm.ts} | 9 +-
packages/codingcode/src/llm/port.ts | 19 +
.../codingcode/src/llm/providers/deepseek.ts | 2 +-
.../codingcode/src/llm/providers/openai.ts | 2 +-
.../codingcode/src/mcp/{index.ts => mcp.ts} | 10 +-
packages/codingcode/src/mcp/port.ts | 19 +
packages/codingcode/src/memory/config.ts | 73 +-
packages/codingcode/src/memory/extractor.ts | 67 +-
.../src/memory/{index.ts => memory.ts} | 105 ++-
packages/codingcode/src/memory/port.ts | 11 +
packages/codingcode/src/memory/storage.ts | 97 +--
packages/codingcode/src/memory/types.ts | 12 -
packages/codingcode/src/rules/port.ts | 8 +
.../src/rules/{index.ts => rules.ts} | 71 +-
.../codingcode/src/runtime/project-runtime.ts | 116 ----
packages/codingcode/src/sandbox/index.ts | 26 -
packages/codingcode/src/scheduler/port.ts | 17 +
.../scheduler/{service.ts => scheduler.ts} | 70 +-
packages/codingcode/src/server/adapter.ts | 7 +-
packages/codingcode/src/server/handler.ts | 4 +-
.../codingcode/src/server/routes/approval.ts | 2 +-
.../src/server/routes/automations.ts | 2 +-
.../codingcode/src/server/routes/messages.ts | 60 +-
.../codingcode/src/server/routes/models.ts | 2 +-
.../codingcode/src/server/routes/sessions.ts | 79 ++-
.../codingcode/src/server/routes/settings.ts | 59 +-
packages/codingcode/src/session/file-ops.ts | 12 +-
packages/codingcode/src/session/index.ts | 4 +
packages/codingcode/src/session/port.ts | 51 ++
.../src/session/{store.ts => session.ts} | 143 ++--
packages/codingcode/src/session/types.ts | 13 +-
packages/codingcode/src/skills/port.ts | 14 +
.../src/skills/{service.ts => skills.ts} | 9 +-
packages/codingcode/src/skills/types.ts | 13 -
packages/codingcode/src/subagent/port.ts | 25 +
.../codingcode/src/subagent/runner-service.ts | 21 -
packages/codingcode/src/subagent/subagent.ts | 31 +
packages/codingcode/src/subagent/types.ts | 7 -
packages/codingcode/src/todo/port.ts | 29 +
packages/codingcode/src/todo/todo.ts | 11 +
.../codingcode/src/tools/builtin-tools.ts | 2 +-
.../src/tools/domains/self/todo-write.ts | 4 +-
.../src/tools/domains/subagent/dispatch.ts | 163 +----
packages/codingcode/src/tools/port.ts | 33 +
.../src/tools/{executor.ts => tools.ts} | 30 +-
packages/codingcode/src/tools/types.ts | 5 -
.../test/agent/agent-cache-stability.test.ts | 96 +--
.../test/agent/agent-concurrent.test.ts | 323 +++------
.../agent/agent-on-interrupt-emit.test.ts | 5 +-
.../test/agent/agent-todo-event.test.ts | 202 ++----
packages/codingcode/test/agent/agent.test.ts | 433 +++---------
.../test/agent/build-system-prompt.test.ts | 4 +-
packages/codingcode/test/agent/config.test.ts | 31 -
.../test/agent/context-compressed.test.ts | 85 +++
.../test/agent/hooks-deps-type.test.ts | 128 +---
.../test/agent/loop-options.test.ts | 329 ++-------
.../test/agent/memory-snapshot.test.ts | 170 +----
.../test/agent/reactive-compact.test.ts | 113 ---
.../send-message-optional-profile.test.ts | 38 -
.../test/agent/stop-decision-type.test.ts | 5 +-
.../codingcode/test/agent/stop-hook.test.ts | 329 +++------
.../test/agent/submit-plan-turn-end.test.ts | 336 +++------
.../test/agent/system-prompt-cwd.test.ts | 31 -
.../test/approval/async-confirm.test.ts | 5 +-
.../approval/fork-permission-mode.test.ts | 19 +-
.../codingcode/test/approval/pipeline.test.ts | 135 +---
.../codingcode/test/approval/presets.test.ts | 18 +-
.../test/checkpoint/checkpoint-diff.test.ts | 17 +-
.../test/checkpoint/checkpoint-undo.test.ts | 52 +-
.../checkpoint/turn-title-removal.test.ts | 9 +-
.../test/ci/tooling-scripts.test.ts | 80 ---
.../test/client/direct-types.test.ts | 7 +-
.../codingcode/test/client/direct.test.ts | 7 +-
.../test/client/get-session-plan.test.ts | 16 +-
.../test/client/http-direct-parity.test.ts | 19 -
.../test/client/missing-methods.test.ts | 29 +-
.../test/context/append-turn-end.test.ts | 2 -
.../test/context/budget-integration.test.ts | 29 +-
.../test/context/compressor/behavior.test.ts | 59 +-
.../compressor/compact-if-needed.test.ts | 149 ----
.../context/compressor/llm-resolver.test.ts | 4 +-
.../codingcode/test/context/organizer.test.ts | 11 +-
packages/codingcode/test/core/paths.test.ts | 25 -
.../codingcode/test/helpers/agent-harness.ts | 291 ++++++++
.../codingcode/test/hooks/decision.test.ts | 5 +-
.../codingcode/test/hooks/registry.test.ts | 5 +-
.../test/layer/system-hook-layer.test.ts | 72 --
.../test/llm/deepseek-provider.test.ts | 2 +-
packages/codingcode/test/llm/factory.test.ts | 35 +-
.../test/llm/openai-provider.test.ts | 4 +-
packages/codingcode/test/mcp/service.test.ts | 7 +-
.../codingcode/test/memory/config.test.ts | 279 +-------
.../codingcode/test/memory/extractor.test.ts | 172 +----
packages/codingcode/test/memory/index.test.ts | 349 ++++++----
.../test/memory/llm-resolver.test.ts | 4 +-
.../codingcode/test/memory/storage.test.ts | 142 ++--
packages/codingcode/test/orchestrate.test.ts | 386 ++---------
.../test/plan/allowed-tools.test.ts | 22 +
.../test/plan/gate-pipeline.test.ts | 12 +-
.../test/runtime/set-session-profile.test.ts | 114 ---
.../test/scheduler/approval-bypass.test.ts | 22 -
.../security/plan-profile-restart.test.ts | 86 +--
.../codingcode/test/self/todo/service.test.ts | 15 +-
.../codingcode/test/server/adapter.test.ts | 10 +-
.../test/server/compact-route.test.ts | 41 +-
.../create-session-active-profile.test.ts | 55 +-
packages/codingcode/test/server/index.test.ts | 35 +-
.../messages-fork-permission-mode.test.ts | 82 +--
.../test/server/plan-file-route.test.ts | 53 +-
.../server/routes-use-compute-paths.test.ts | 21 -
.../test/session/compute-paths.test.ts | 4 +-
.../session/create-active-profile.test.ts | 10 +-
.../session/create-session-profile.test.ts | 4 +-
.../test/session/disk-setters.test.ts | 16 +-
.../test/session/facade-surface.test.ts | 67 ++
packages/codingcode/test/session/fork.test.ts | 6 +-
.../test/session/index-write-error.test.ts | 6 +-
.../test/session/index-write-sync.test.ts | 4 +-
.../codingcode/test/session/io-error.test.ts | 8 +-
.../test/session/load-create.test.ts | 8 +-
.../test/session/load-restore-profile.test.ts | 92 +--
.../test/session/parent-session-id.test.ts | 4 +-
.../test/session/prompt-estimate.test.ts | 11 +-
.../record-tool-result-persist.test.ts | 4 +-
.../codingcode/test/session/rollback.test.ts | 2 +-
.../test/session/session-jsonl-path.test.ts | 4 +-
.../test/session/store-compact-usage.test.ts | 4 +-
.../test/session/store-rollback-usage.test.ts | 4 +-
.../test/session/ui-history-rollback.test.ts | 2 +-
.../test/session/update-index-dedup.test.ts | 4 +-
.../test/session/view-assembly.test.ts | 2 +-
packages/codingcode/test/skills/index.test.ts | 39 +-
.../codingcode/test/skills/layout.test.ts | 46 --
.../test/subagent/approval-fork.test.ts | 43 +-
.../test/subagent/dispatch-end-to-end.test.ts | 248 +++++--
.../codingcode/test/subagent/dispatch.test.ts | 287 +++-----
.../test/subagent/runner-service.test.ts | 35 +-
.../test/tools/builtin-tools.test.ts | 5 +-
.../test/tools/executor-context.test.ts | 9 +-
packages/codingcode/test/tools/todo.test.ts | 5 +-
.../desktop/src/agent/ProfileIndicator.tsx | 2 +-
packages/desktop/src/hooks/useAgent.ts | 28 +-
packages/desktop/src/lib/core-api.ts | 24 +-
packages/desktop/src/settings/MemoryPanel.tsx | 341 +--------
packages/desktop/src/stores/agent.store.ts | 2 +-
.../test/app-agent-layout-inline.test.ts | 17 -
.../desktop/test/compact-usage-reset.test.ts | 91 ++-
.../desktop/test/core-api-clients.test.ts | 33 -
packages/desktop/test/css-declaration.test.ts | 16 -
packages/desktop/test/menu-deletion.test.ts | 52 --
.../desktop/test/rollback-usage-reset.test.ts | 4 -
packages/desktop/test/scroll-layout.test.ts | 49 --
packages/desktop/test/selection.test.ts | 71 --
packages/desktop/test/thread-delete.test.ts | 4 -
.../test/useAgent-streamChunkToItem.test.ts | 8 +-
packages/infra/src/config.ts | 32 -
packages/tui/src/index.tsx | 2 +-
vitest.config.ts | 5 +
201 files changed, 3816 insertions(+), 7773 deletions(-)
delete mode 100644 packages/codingcode/src/agent/config.ts
create mode 100644 packages/codingcode/src/agent/deps.ts
create mode 100644 packages/codingcode/src/agent/port.ts
delete mode 100644 packages/codingcode/src/agent/todo.ts
rename packages/codingcode/src/approval/{index.ts => approval.ts} (87%)
create mode 100644 packages/codingcode/src/approval/port.ts
create mode 100644 packages/codingcode/src/approval/wait-port.ts
rename packages/codingcode/src/approval/{async-confirm.ts => wait.ts} (92%)
rename packages/codingcode/src/checkpoint/{checkpoint-service.ts => checkpoint.ts} (98%)
create mode 100644 packages/codingcode/src/checkpoint/port.ts
delete mode 100644 packages/codingcode/src/client/http.ts
rename packages/codingcode/src/context/{service.ts => context.ts} (76%)
create mode 100644 packages/codingcode/src/context/port.ts
delete mode 100644 packages/codingcode/src/context/types.ts
rename packages/codingcode/src/hooks/{registry.ts => hooks.ts} (76%)
create mode 100644 packages/codingcode/src/hooks/port.ts
rename packages/codingcode/src/llm/{factory.ts => llm.ts} (98%)
create mode 100644 packages/codingcode/src/llm/port.ts
rename packages/codingcode/src/mcp/{index.ts => mcp.ts} (98%)
create mode 100644 packages/codingcode/src/mcp/port.ts
rename packages/codingcode/src/memory/{index.ts => memory.ts} (55%)
create mode 100644 packages/codingcode/src/memory/port.ts
delete mode 100644 packages/codingcode/src/memory/types.ts
create mode 100644 packages/codingcode/src/rules/port.ts
rename packages/codingcode/src/rules/{index.ts => rules.ts} (57%)
delete mode 100644 packages/codingcode/src/runtime/project-runtime.ts
delete mode 100644 packages/codingcode/src/sandbox/index.ts
create mode 100644 packages/codingcode/src/scheduler/port.ts
rename packages/codingcode/src/scheduler/{service.ts => scheduler.ts} (78%)
create mode 100644 packages/codingcode/src/session/index.ts
create mode 100644 packages/codingcode/src/session/port.ts
rename packages/codingcode/src/session/{store.ts => session.ts} (85%)
create mode 100644 packages/codingcode/src/skills/port.ts
rename packages/codingcode/src/skills/{service.ts => skills.ts} (93%)
create mode 100644 packages/codingcode/src/subagent/port.ts
delete mode 100644 packages/codingcode/src/subagent/runner-service.ts
create mode 100644 packages/codingcode/src/subagent/subagent.ts
delete mode 100644 packages/codingcode/src/subagent/types.ts
create mode 100644 packages/codingcode/src/todo/port.ts
create mode 100644 packages/codingcode/src/todo/todo.ts
create mode 100644 packages/codingcode/src/tools/port.ts
rename packages/codingcode/src/tools/{executor.ts => tools.ts} (90%)
delete mode 100644 packages/codingcode/test/agent/config.test.ts
create mode 100644 packages/codingcode/test/agent/context-compressed.test.ts
delete mode 100644 packages/codingcode/test/agent/reactive-compact.test.ts
delete mode 100644 packages/codingcode/test/agent/send-message-optional-profile.test.ts
delete mode 100644 packages/codingcode/test/agent/system-prompt-cwd.test.ts
delete mode 100644 packages/codingcode/test/ci/tooling-scripts.test.ts
delete mode 100644 packages/codingcode/test/client/http-direct-parity.test.ts
delete mode 100644 packages/codingcode/test/context/compressor/compact-if-needed.test.ts
delete mode 100644 packages/codingcode/test/core/paths.test.ts
create mode 100644 packages/codingcode/test/helpers/agent-harness.ts
delete mode 100644 packages/codingcode/test/layer/system-hook-layer.test.ts
create mode 100644 packages/codingcode/test/plan/allowed-tools.test.ts
delete mode 100644 packages/codingcode/test/runtime/set-session-profile.test.ts
delete mode 100644 packages/codingcode/test/scheduler/approval-bypass.test.ts
delete mode 100644 packages/codingcode/test/server/routes-use-compute-paths.test.ts
create mode 100644 packages/codingcode/test/session/facade-surface.test.ts
delete mode 100644 packages/codingcode/test/skills/layout.test.ts
delete mode 100644 packages/desktop/test/app-agent-layout-inline.test.ts
delete mode 100644 packages/desktop/test/core-api-clients.test.ts
delete mode 100644 packages/desktop/test/css-declaration.test.ts
delete mode 100644 packages/desktop/test/menu-deletion.test.ts
delete mode 100644 packages/desktop/test/scroll-layout.test.ts
delete mode 100644 packages/desktop/test/selection.test.ts
diff --git a/.gitignore b/.gitignore
index 491a887c..3b632596 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,10 @@ client-tests.txt
desktop-test.txt
h-test.txt
test-fallback.txt
+test2.txt
+tsc-out.txt
+arch-out.txt
+vitest-result.json
# Compiled JS artifacts leaked into src directories
packages/infra/src/*.js
@@ -51,3 +55,4 @@ packages/desktop/pnpm-workspace.yaml
Thumbs.db
.idea/
.vscode/
+.workbuddy/memory/
diff --git a/.prettierignore b/.prettierignore
index b9d26309..330c03fc 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,3 +4,5 @@ node_modules/
package-lock.json
pnpm-lock.yaml
*.md
+vitest-result.json
+test-output*.txt
diff --git a/.prettierrc b/.prettierrc
index 1f4c4bbc..85a8e67e 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -3,5 +3,6 @@
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
- "printWidth": 100
+ "printWidth": 100,
+ "endOfLine": "auto"
}
diff --git a/docs/configuration.md b/docs/configuration.md
index f81dc40a..561d1faf 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -40,10 +40,7 @@ context:
memory:
enabled: false # 启用长期记忆
model: "" # 记忆提取模型,空字符串回退主模型
- maxBytes: 16384 # 记忆文件最大字节数
- promptMaxBytes: 8192 # 注入提示的最大字节数
- extraTypes: [] # 自定义记忆类型
- disabledTypes: [] # 禁用的记忆类型名
+ promptMaxBytes: 8192 # 注入提示的记忆内容最大字节数
```
### 字段详细说明
@@ -57,26 +54,7 @@ memory:
| `context.compactionModel` | `''` | 上下文压缩使用的模型,空字符串回退到主会话 LLM |
| `memory.enabled` | `false` | 是否启用长期记忆系统 |
| `memory.model` | `''` | 记忆提取使用的模型,空字符串回退到主模型 |
-| `memory.maxBytes` | `16384` | 单个记忆文件的最大字节数 |
| `memory.promptMaxBytes` | `8192` | 注入 system prompt 的记忆内容最大字节数 |
-| `memory.extraTypes` | `[]` | 自定义记忆类型列表 |
-| `memory.disabledTypes` | `[]` | 禁用的内置记忆类型名列表 |
-
-### 自定义记忆类型示例
-
-```yaml
-memory:
- enabled: true
- extraTypes:
- - name: feedback
- description: 工作流程中的教训和已验证的方法
- enabled: true
- - name: decision
- description: 重要的架构和设计决策
- enabled: true
- disabledTypes:
- - reference
-```
---
diff --git a/docs/memory.md b/docs/memory.md
index 15b72890..b8b91aad 100644
--- a/docs/memory.md
+++ b/docs/memory.md
@@ -1,116 +1,74 @@
# 长期记忆系统
-Coding Code 支持跨会话的长期记忆,自动从对话中提取和存储关键信息。本文档介绍记忆类型、内容分类、自动提取机制和手动编辑方法。
+Coding Code 支持跨会话的长期记忆:自动从对话中提取关键信息,并在下一次会话开始时重新注入。本文档介绍记忆文件、自动提取机制和手动编辑方法。
---
-## 内存类型
+## 记忆文件
-记忆文件存储在项目的 `.codingcode/memory.md` 中。
+记忆存储为单个 Markdown 文件:
----
-
-## 记忆内容
+```
+.codingcode/memory.md
+```
-内置三种记忆类型:
+**整个文件就是长期记忆**,没有分区、没有标记块。文件的全部内容会作为记忆注入,也会作为"已有记忆"参与下一次提取。
-| 类型 | 提取来源 | 内容 |
-|------|---------|------|
-| `user` | `[user]` 标签的消息 | 用户角色、技能栈、工作偏好及对 Agent 的纠正 |
-| `project` | `[user]` + `[assistant]` 消息 | 架构决策、技术选型、部署信息 |
-| `reference` | `[user]` + `[tool:*]` 消息 | 外部资源、文档、Dashboard 链接 |
+```markdown
+### 项目
+- 采用 monorepo 架构,使用 pnpm workspaces
+- 入口文件:packages/codingcode/src/cli.ts
-可通过 `memory.extraTypes` 添加自定义记忆类型,通过 `memory.disabledTypes` 禁用内置类型。
+### 用户偏好
+- 偏好结构化 Markdown 输出
+```
---
## 自动提取
-Agent 在每次会话后自动执行记忆提取:
+记忆模式开启后,Agent 在会话结束时自动执行记忆更新:
-1. 构建 system prompt,包含各记忆类型的提取指引
-2. 发送已有记忆 + 会话记录给 LLM
-3. LLM 输出 `...` 块
-4. 提取块内容,返回新记忆文本(null 表示无新内容)
-5. 矛盾时新信息替换旧条目,同一会话以最新为准
+1. 读取记忆文件全文作为"已有记忆"
+2. 将会话记录(按 `[user]` / `[assistant]` / `[tool:名称]` 标注)与已有记忆一起发送给 LLM
+3. LLM 输出整份**最新版记忆**,放在 `...` 块中
+4. 直接用输出内容整体替换记忆文件(受字节上限约束)
-提取使用的模型可通过 `memory.model` 配置,留空则回退到主会话模型。
-
----
+模型自行决定更新哪些内容:可以新增条目、修改过时信息、删除不再相关的内容,代码不做"模型只改动哪部分"的任何假设。若模型没有输出有效内容、或输出与当前文件一致,则不写入。
-## 记忆文件格式
+### 提取提示词
-记忆文件使用 Markdown 格式,自动提取内容包裹在标记块中:
+提取行为的规范全部写在提示词中,代码不感知记忆内容结构:
-```markdown
-
-### user
-- 偏好使用函数式编程风格
-- 常用技术栈:React + TypeScript
+- 只保留值得跨会话记住的信息:用户偏好与纠正、项目架构决策、技术选型、外部资源与链接等
+- 忽略一次性任务、调试过程、报错堆栈、闲聊
+- 输出必须是一份完整、自洽的最新记忆,而不是只输出变动部分
+- 新旧信息矛盾时以最新为准
+- 记忆用 `### 主题` 小节组织,小节下用 `- ` 列要点
-### project
-- 采用 monorepo 架构,使用 pnpm workspaces
-- 入口文件:packages/codingcode/src/cli.ts
+提取使用的模型可通过 `memory.model` 配置,留空则回退到主会话模型。
-### reference
-- [API 文档](https://example.com/api)
-
+---
-手动添加的内容可以写在标记块之外,不会被自动提取覆盖。
-```
+## 手动编辑
-### 标记块机制
+记忆文件就是普通 Markdown,用户可以直接编辑:
-- `replaceAutoBlock()`:原子替换 `` 和 `` 之间的内容
-- `stripMarkersForPrompt()`:去掉标记后注入系统提示
-- `enforceMaxBytes()`:按 `### ` 小节逐个裁剪到字节上限(默认 16384 字节)
-- `mergeAutoBlocks()`:以 `### ` 小节名为 key 合并,incoming 覆盖 base
+- 手动写下的内容会在下次会话时作为记忆注入 Agent
+- 手动编辑也会被下一次自动提取作为"已有记忆"读到;保留、修改还是删除由模型根据后续对话自行决定
+- 自动提取在写入前会重新检查文件:若提取期间文件被手动改动,则放弃本次写入,避免覆盖用户编辑
---
## 配置
-在 `codingcode.yaml` 中配置记忆系统:
-
-```yaml
-memory:
- enabled: true # 启用长期记忆(默认 false)
- model: "" # 记忆提取模型,空字符串回退到主模型
- maxBytes: 16384 # 记忆文件最大字节数
- promptMaxBytes: 8192 # 注入提示的最大字节数
- extraTypes: [] # 自定义记忆类型
- disabledTypes: [] # 禁用的记忆类型名
-```
-
-### 自定义记忆类型
+在 `~/.codingcode/config.yaml` 中配置记忆系统:
```yaml
memory:
- enabled: true
- extraTypes:
- - name: feedback
- description: 工作流程中的教训和已验证的方法
- enabled: true
- - name: decision
- description: 重要的架构和设计决策
- enabled: true
- disabledTypes:
- - reference # 禁用内置的 reference 类型
+ enabled: true # 启用长期记忆(默认 false)
+ model: "" # 记忆提取模型,空字符串回退到主模型
+ promptMaxBytes: 8192 # 注入提示词的记忆内容最大字节数
```
----
-
-## 手动编辑
-
-记忆文件采用 Markdown 格式,支持手动编辑。手动内容可写在 `` 标记之后,不会被自动提取覆盖:
-
-```markdown
-
-### user
-- 偏好使用函数式编程风格
-
-
-### 手动备注
-- 项目部署流程:npm run build -> scp dist/ -> pm2 restart
-- 数据库连接字符串在 Vault 中
-```
+记忆文件本身有 16KB 的硬上限,超限时按 `### ` 小节从后往前裁掉超出部分。
diff --git a/docs/tools.md b/docs/tools.md
index bfcce750..190c53c2 100644
--- a/docs/tools.md
+++ b/docs/tools.md
@@ -97,16 +97,15 @@ interface ToolVisibilityPolicy {
### 审批流水线(始终生效)
-六层决策链,按顺序执行,任一层返回 deny/allow 即终止:
+五层决策链,按顺序执行,任一层返回 deny/allow 即终止:
| 层级 | 名称 | 逻辑 |
|------|------|------|
| 1 | **RuleEngine** | 规则引擎匹配,支持 glob 模式匹配工具名和参数,按优先级排序 |
-| 2 | **ReadonlyWhitelist** | 只读工具自动放行(read_file, search_code, search_files, fetch_url, web_search, dispatch_agent, todo_write) |
-| 3 | **PermissionMode** | 权限模式判断:`bypass`(全部放行)、`acceptEdits`(非破坏性工具放行)、`default`(继续下一层)。`plan` Profile 由独立的 `agent/profile.ts` 中的 `planProfileGateHook` 在 Layer 4 强制,不在此层处理 |
-| 4 | **HookPreToolUse** | 钩子决策,可返回 allow/deny/ask/continue,支持 `modifiedInput` 修改参数 |
-| 5 | **UserConfirmation** | 异步用户确认,支持 allow/deny/always/never 四种响应,always/never 会持久化为规则 |
-| 6 | **AuditLog** | 每一层决策后记录审计日志,通过 `tool.approval.post` 钩子发出 |
+| 2 | **PermissionMode** | 权限模式驱动的自动放行:`bypass`(全部放行)、`acceptEdits`(非破坏性工具放行,涵盖只读与编辑工具)、`default`(不自动放行,继续下一层)。只读工具不再有无条件的独立白名单层;`plan` Profile 由 `agent/profile.ts` 中的 `planProfileGateHook` 在下一层强制,不在此层处理 |
+| 3 | **HookPreToolUse** | 钩子决策,可返回 allow/deny/ask/continue,支持 `modifiedInput` 修改参数 |
+| 4 | **UserConfirmation** | 异步用户确认,支持 allow/deny/always/never 四种响应,always/never 会持久化为规则 |
+| 5 | **AuditLog** | 每一层决策后记录审计日志,通过 `tool.approval.post` 钩子发出 |
### 预设安全规则
@@ -130,11 +129,13 @@ interface ToolVisibilityPolicy {
type PermissionMode = 'default' | 'acceptEdits' | 'bypass';
```
-- `default`:逐层审批,危险操作需用户确认
-- `acceptEdits`:非破坏性工具自动放行,减少确认弹窗
+- `default`:不自动放行任何工具(含只读工具),全部逐层审批
+- `acceptEdits`:非破坏性工具自动放行(涵盖只读工具与编辑类工具),破坏性工具仍需确认
- `bypass`:全部放行,跳过所有审批(慎用)
-> `plan` 不再是 `PermissionMode` 的成员。plan Profile 通过 `AgentProfile.name === 'plan'` 结构化识别,由 `agent/profile.ts` 的 `planProfileGateHook` 和 `PLAN_PROFILE_ALLOWED_TOOLS` 共同限制工具。
+> 原独立的 `ReadonlyWhitelist` 层(在 `default` 下也无条件放行只读工具)已废弃,其语义并入 `PermissionMode` 的自动放行判定:`acceptEdits` 视只读工具为非破坏性工具自动放行,`default` 不再自动放行。
+
+> `plan` 不再是 `PermissionMode` 的成员。plan Profile 通过 `AgentProfile.name === 'plan'` 结构化识别,由 `agent/profile.ts` 的 `planProfileGateHook` 和 `PLAN_PROFILE_ALLOWED_TOOLS` 共同限制工具。因只读白名单层已删除,`dispatch_agent` 等在 plan 下不再被流水线上层提前放行,统一由 plan gate 拦截。
### OS 级沙箱(预留)
diff --git a/packages/codingcode/package.json b/packages/codingcode/package.json
index 6ad3221d..a715ea62 100644
--- a/packages/codingcode/package.json
+++ b/packages/codingcode/package.json
@@ -8,44 +8,18 @@
},
"exports": {
".": "./src/layer.ts",
- "./agent/agent": "./src/agent/agent.ts",
- "./agent/todo": "./src/agent/todo.ts",
- "./agent/prompt": "./src/agent/prompt.ts",
- "./session/store": "./src/session/store.ts",
- "./session/io": "./src/session/io.ts",
- "./session/types": "./src/session/types.ts",
- "./session/messages": "./src/session/messages.ts",
- "./core/path": "./src/core/path.ts",
- "./core/workspace": "./src/core/workspace.ts",
- "./core/error": "./src/core/error.ts",
- "./core/result": "./src/core/result.ts",
- "./core/types": "./src/core/types.ts",
- "./context/context": "./src/context/context.ts",
- "./hooks/registry": "./src/hooks/registry.ts",
- "./tools/executor": "./src/tools/executor.ts",
- "./mcp/client": "./src/mcp/client.ts",
- "./mcp/types": "./src/mcp/types.ts",
- "./skills/types": "./src/skills/types.ts",
- "./approval/types": "./src/approval/types.ts",
- "./approval/async-confirm": "./src/approval/async-confirm.ts",
- "./server/create": "./src/server/index.ts",
- "./server/adapter": "./src/server/adapter.ts",
- "./server/port-discovery": "./src/server/port-discovery.ts",
+ "./client": "./src/client/http/index.ts",
"./client/types": "./src/client/types.ts",
- "./client/http": "./src/client/http.ts",
- "./client/http-clients": "./src/client/http/index.ts",
+ "./server": "./src/server/index.ts",
"./direct/agent-runtime": "./src/direct/agent-runtime.ts",
"./direct/sessions": "./src/direct/sessions.ts",
"./direct/settings": "./src/direct/settings.ts",
"./direct/models": "./src/direct/models.ts",
- "./agent/stream-adapter": "./src/agent/stream-adapter.ts",
- "./checkpoint/checkpoint-service": "./src/checkpoint/checkpoint-service.ts",
- "./checkpoint/shadow-git": "./src/checkpoint/shadow-git.ts",
- "./checkpoint/bootstrap": "./src/checkpoint/bootstrap.ts",
- "./llm/factory": "./src/llm/factory.ts",
- "./llm/client": "./src/llm/client.ts",
- "./layer": "./src/layer.ts",
- "./subagent/types": "./src/subagent/types.ts"
+ "./approval/types": "./src/approval/types.ts",
+ "./agent/profile": "./src/agent/profile.ts",
+ "./core/error": "./src/core/error.ts",
+ "./core/types": "./src/core/types.ts",
+ "./llm/client": "./src/llm/client.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 a5b2efb1..92842bb8 100644
--- a/packages/codingcode/src/agent/agent.ts
+++ b/packages/codingcode/src/agent/agent.ts
@@ -1,246 +1,176 @@
-import { Effect, Queue, Stream, Fiber } from 'effect';
+import { Effect, Queue, Stream, Fiber, Layer } from 'effect';
import type { Message } from '../core/types.js';
import { AgentError } from '../core/error.js';
import { Result } from '../core/result.js';
-import type { LLMClient } from '../llm/client.js';
-import { ToolExecutorService, type ToolLookup } from '../tools/executor.js';
-import { SessionService } from '../session/store.js';
-import { CheckpointService } from '../checkpoint/checkpoint-service.js';
-import { ApprovalService } from '../approval/index.js';
-import { ApprovalWaitService } from '../approval/async-confirm.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,
+} from './deps.js';
import { buildSystemPrompt } from './prompt.js';
-import type { AgentEvent, RunStreamOptions } from './types.js';
-import { resolveConfig } from './config.js';
-import { TodoService } from './todo.js';
-import { HookService } from '../hooks/registry.js';
-import { SkillService } from '../skills/service.js';
-import { McpService } from '../mcp/index.js';
-import { ContextService } from '../context/service.js';
-import { MemoryService } from '../memory/index.js';
+import type { AgentEvent } from './types.js';
+import { loadConfig } from '@codingcode/infra/config';
import { createLogger } from '@codingcode/infra/logger';
-import { ProjectRuntimeService } from '../runtime/project-runtime.js';
import { registerBuiltinTools } from '../tools/builtin-tools.js';
import { ToolRegistry } from '../tools/registry.js';
import { submitPlanTool } from '../tools/domains/subagent/submit-plan.js';
import { createDispatchAgentTool } from '../tools/domains/subagent/dispatch.js';
import { normalizePath } from '../core/path.js';
-import { isPlanProfile } from './profile.js';
-import type { AgentProfileName } from '../subagent/types.js';
-import type { PermissionMode } from '../approval/types.js';
-
-const REACTIVE_COMPACT_MAX_RETRIES = 3;
-import { RulesService } from '../rules/index.js';
+import { isPlanProfile, resolveProfile, getAllowedTools } from './profile.js';
+import type { AgentProfile } from './profile.js';
+import { TodoService } from '../todo/port.js';
const logger = createLogger();
-export class AgentService extends Effect.Service()('Agent', {
- effect: Effect.gen(function* () {
- const executor = yield* ToolExecutorService;
- const hooks = yield* HookService;
- const approval = yield* ApprovalService;
- const approvalWait = yield* ApprovalWaitService;
- const session = yield* SessionService;
- const checkpoint = yield* CheckpointService;
- const runtime = yield* ProjectRuntimeService;
- const todo = yield* TodoService;
- const context = yield* ContextService;
- const memory = yield* MemoryService;
- const { maxSteps, maxStopContinuations } = resolveConfig();
-
- const runStream = (
- opts: RunStreamOptions
- ): AsyncGenerator, unknown> => {
- const q = Effect.runSync(Queue.unbounded());
-
- const program = Effect.scoped(
- Effect.gen(function* () {
- yield* Effect.addFinalizer(() =>
- Effect.sync(() => {
- hooks.disposeSession(opts.state.sessionId);
- })
+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 todoService = yield* TodoService;
+ const cfg = loadConfig();
+ const maxSteps = cfg.maxSteps ?? 250;
+ const maxStopContinuations = cfg.maxStopContinuations ?? 3;
+
+ const runTurn = (input: string, opts: RunTurnOptions) =>
+ Effect.gen(function* () {
+ const normalizedCwd = normalizePath(opts.cwd);
+
+ // prepareProject: evict rules + reload hooks + sync mcp
+ rules.evictProjectRules(normalizedCwd);
+ yield* hooks.emit('agent.turn.start', { sessionId: '' }).pipe(Effect.catchAll(() => Effect.void));
+ yield* mcp.syncConnections(normalizedCwd).pipe(Effect.catchAll(() => Effect.void));
+
+ let sessionId = opts.sessionId;
+ const llm = yield* llmFactory.getLLMClient();
+ if (!sessionId) {
+ if (!opts.activeProfile || !opts.permissionMode) {
+ return yield* Effect.fail(
+ new AgentError('CONFIG_MISSING', 'new session requires activeProfile and permissionMode')
);
- return yield* agentLoop(executor, hooks, maxSteps, maxStopContinuations, opts, q);
- }).pipe(
- Effect.provideService(HookService, hooks),
- Effect.provideService(ToolExecutorService, executor),
- Effect.provideService(ApprovalService, approval),
- Effect.provideService(ApprovalWaitService, approvalWait),
- Effect.provideService(SessionService, session),
- Effect.provideService(CheckpointService, checkpoint),
- Effect.provideService(ProjectRuntimeService, runtime),
- Effect.provideService(TodoService, todo),
- Effect.provideService(ContextService, context),
- Effect.provideService(MemoryService, memory)
- )
- );
-
- return (async function* () {
- const fiber = Effect.runFork(program);
-
- if (opts.abortSignal) {
- opts.abortSignal.addEventListener(
- 'abort',
- () => {
- Effect.runFork(Fiber.interrupt(fiber));
- },
- { once: true }
- );
- if (opts.abortSignal.aborted) {
- Effect.runFork(Fiber.interrupt(fiber));
- }
}
+ const model = opts.model ?? llm.modelInfo.model;
+ const created = yield* session.create(normalizedCwd, {
+ model,
+ activeProfile: opts.activeProfile,
+ permissionMode: opts.permissionMode,
+ });
+ sessionId = created.sessionId;
+ }
- const stream = Stream.fromQueue(q).pipe(Stream.interruptWhen(Fiber.await(fiber)));
-
- for await (const event of Stream.toAsyncIterable(stream) as AsyncIterable) {
- yield event;
- }
+ const state = yield* session.load(normalizedCwd, sessionId);
+ const sid = state.sessionId;
- try {
- const result = await Effect.runPromise(Fiber.join(fiber));
- return result;
- } catch (e) {
- return Result.err(
- e instanceof AgentError ? e : new AgentError('AGENT_ABORTED' as any, String(e))
- );
- }
- })();
- };
-
- return { runStream };
- }),
-}) {}
-
-export const sendMessage = (
- sessionId: string | undefined,
- input: string,
- cwd: string,
- llm: LLMClient,
- options: {
- signal?: AbortSignal;
- approvalOverride?: import('../approval/index.js').ApprovalService;
- activeProfile?: AgentProfileName;
- permissionMode?: PermissionMode;
- model?: string;
- }
-) =>
- Effect.gen(function* () {
- const session = yield* SessionService;
- const agent = yield* AgentService;
- const hooks = yield* HookService;
- const mcp = yield* McpService;
- const checkpoint = yield* CheckpointService;
- const approval = yield* ApprovalService;
- const skills = yield* SkillService;
- const runtime = yield* ProjectRuntimeService;
- const todo = yield* TodoService;
- const rules = yield* RulesService;
- const context = yield* ContextService;
- const memory = yield* MemoryService;
-
- const normalizedCwd = normalizePath(cwd);
- yield* runtime.prepareProject(normalizedCwd);
- yield* skills.evictProject(normalizedCwd);
-
- if (!sessionId) {
- if (!options.activeProfile || !options.permissionMode || !options.model) {
- return yield* Effect.fail(
- new AgentError(
- 'CONFIG_MISSING',
- 'new session requires activeProfile, permissionMode, and model'
- )
- );
+ // restoreSessionProfile
+ const effectivePerm = opts.permissionMode ?? 'default';
+ yield* session.setPermissionMode(normalizedCwd, sid, effectivePerm);
+ if (opts.activeProfile) {
+ yield* session.setActiveProfile(normalizedCwd, sid, opts.activeProfile);
}
- const created = yield* session.create(normalizedCwd, {
- model: options.model,
- activeProfile: options.activeProfile,
- permissionMode: options.permissionMode,
- });
- sessionId = created.sessionId;
- }
- const state = yield* session.load(normalizedCwd, sessionId);
- yield* runtime.restoreSessionProfile(
- normalizedCwd,
- state.sessionId,
- state.activeProfile,
- state.permissionMode
- );
- state.memorySnapshot = memory.loadMemoryForPrompt(state.cwd);
- const sid = state.sessionId;
- const profile = runtime.resolveMainAgentProfile(normalizedCwd, state.sessionId);
- const policy = runtime.getToolPolicy(profile);
+ state.memorySnapshot = memory.loadMemoryForPrompt(state.cwd);
- const dispatchTool = yield* createDispatchAgentTool();
+ // resolveMainAgentProfile
+ const profileName = yield* session.getActiveProfile(normalizedCwd, sid);
+ const profile: AgentProfile | undefined = profileName ? resolveProfile(profileName) : undefined;
+ const allowedTools = getAllowedTools(profile);
- const activeLlm = llm;
- const effectiveMaxSteps = profile?.maxSteps;
- const effectiveApproval: any = options?.approvalOverride;
+ // create dispatch tool
+ const dispatchTool = yield* createDispatchAgentTool();
- const mcpTools = mcp.listProjectMcpTools(normalizedCwd);
+ // get MCP tools
+ const mcpTools = mcp.listProjectMcpTools(normalizedCwd);
- const turnId = session.incrementTurn(state);
- const [, actualInput] = yield* skills.extractSkill(state.cwd, input);
+ // increment turn + extract skill + record user
+ const turnId = session.incrementTurn(state);
+ const [, actualInput] = yield* skills.extractSkill(state.cwd, input);
+ yield* session.recordUser(state, actualInput);
- yield* session.recordUser(state, actualInput);
+ // checkpoint baseline
+ yield* checkpoint.snapshotBaseline(state.cwd, sid, turnId);
- yield* checkpoint.snapshotBaseline(state.cwd, sid, turnId);
+ // get rules text
+ const rulesText = rules.getAllRules(state.cwd);
- const rulesText = rules.getAllRules(state.cwd);
+ // run agent loop
+ const stream = runAgentLoop({
+ state, llm, profile, allowedTools, mcpTools,
+ abortSignal: opts.signal, rulesText, dispatchTool,
+ sid, projectPath: state.cwd,
+ });
- const stream = agent.runStream({
- state,
- llm: activeLlm,
- profile,
- toolPolicy: policy,
- maxStepsOverride: effectiveMaxSteps,
- approvalOverride: effectiveApproval,
- mcpTools,
- abortSignal: options?.signal,
- rulesText,
- dispatchTool,
+ return { stream, sessionId: sid };
});
- return { stream, sessionId: sid };
- });
-
-export function agentLoop(
- executor: ToolExecutorService,
- hooks: HookService,
- maxSteps: number,
- maxStopContinuations: number,
- opts: RunStreamOptions,
- q: Queue.Queue
-): Effect.Effect<
- Result,
- AgentError,
- | HookService
- | ToolExecutorService
- | CheckpointService
- | SessionService
- | ProjectRuntimeService
- | TodoService
- | ContextService
- | MemoryService
-> {
- const state = opts.state;
- const llm = opts.llm;
- const profile = opts.profile;
- const sessionId = state.sessionId;
- const projectPath = state.cwd;
-
- return Effect.gen(function* () {
- const checkpoint = yield* CheckpointService;
- const session = yield* SessionService;
- const runtime = yield* ProjectRuntimeService;
- const todo = yield* TodoService;
- const context = yield* ContextService;
- const memory = yield* MemoryService;
- const { rulesText } = opts;
-
- const basePrompt =
- opts.systemOverride ??
- buildSystemPrompt({
+ function runAgentLoop(opts: {
+ state: any; llm: any; profile: AgentProfile | undefined;
+ allowedTools: ReadonlySet | undefined;
+ mcpTools: any[]; abortSignal: AbortSignal | undefined;
+ rulesText: string; dispatchTool: any;
+ sid: string; projectPath: string;
+ }): AsyncGenerator {
+ const q = Effect.runSync(Queue.unbounded());
+
+ const program: any = 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),
+ // registerBuiltinTools 需要完整 TodoService 构建 todo_write 工具定义
+ Effect.provideService(TodoService, todoService),
+ )
+ );
+
+ return (async function* () {
+ const fiber = Effect.runFork(program);
+ if (opts.abortSignal) {
+ opts.abortSignal.addEventListener('abort', () => {
+ Effect.runFork(Fiber.interrupt(fiber));
+ }, { once: true });
+ if (opts.abortSignal.aborted) Effect.runFork(Fiber.interrupt(fiber));
+ }
+ const stream = Stream.fromQueue(q).pipe(Stream.interruptWhen(Fiber.await(fiber)));
+ for await (const event of Stream.toAsyncIterable(stream) as AsyncIterable) {
+ yield event;
+ }
+ })();
+ }
+
+ function agentLoopInternal(opts: {
+ state: any; llm: any; profile: AgentProfile | undefined;
+ allowedTools: ReadonlySet | undefined;
+ mcpTools: any[]; abortSignal: AbortSignal | undefined;
+ rulesText: string; dispatchTool: any;
+ sid: string; projectPath: string;
+ }, q: Queue.Queue): any {
+ const { state, llm, profile, allowedTools, mcpTools, abortSignal, rulesText, dispatchTool, sid, projectPath } = opts;
+
+ return Effect.gen(function* () {
+ const basePrompt = buildSystemPrompt({
cwd: projectPath,
platform: process.platform,
shell: process.env.SHELL || process.env.ComSpec || 'bash',
@@ -248,222 +178,114 @@ export function agentLoop(
profileSystemPrompt: profile?.systemPrompt,
});
- const memoryBlock = state.memorySnapshot;
- const memorySection = memoryBlock ? `## Session Memory\n\n${memoryBlock}` : '';
- const system = [basePrompt, memorySection].filter(Boolean).join('\n\n');
-
- const maxOverflowRetries = REACTIVE_COMPACT_MAX_RETRIES;
- const effectiveMaxSteps = opts.maxStepsOverride ?? maxSteps;
-
- let stopContinuations = 0;
- const effectiveMaxStopContinuations = opts.maxStopContinuations ?? maxStopContinuations;
-
- const registry = new ToolRegistry();
- yield* registerBuiltinTools(registry);
- registry.register(...(opts.mcpTools ?? []));
- if (opts.dispatchTool) registry.register(opts.dispatchTool);
- if (isPlanProfile(profile)) registry.register(submitPlanTool);
+ const memoryBlock = state.memorySnapshot;
+ const memorySection = memoryBlock ? `## Session Memory\n\n${memoryBlock}` : '';
+ const system = [basePrompt, memorySection].filter(Boolean).join('\n\n');
- let messages: Message[] = [];
- let submittedPlanTitle: string | null = null;
+ const effectiveMaxSteps = opts.profile?.maxSteps ?? maxSteps;
+ let stopContinuations = 0;
+ const effectiveMaxStopContinuations = maxStopContinuations;
- for (let attempt = 0; attempt <= maxOverflowRetries; attempt++) {
- const payload = yield* Effect.sync(() =>
- context.assemblePayload(session.getTranscriptPath(state), llm.modelInfo.maxTokens)
- );
- messages = payload.messages;
+ const registry = new ToolRegistry();
+ yield* registerBuiltinTools(registry);
+ registry.register(...(mcpTools ?? []));
+ if (dispatchTool) registry.register(dispatchTool);
+ if (isPlanProfile(profile)) registry.register(submitPlanTool);
+ let submittedPlanTitle: string | null = null;
let lastResult: Result | null = null;
- let overflow = false;
-
- yield* hooks.emit('agent.turn.start', { sessionId });
+ yield* hooks.emit('agent.turn.start', { sessionId: sid });
yield* q.offer({ _tag: 'TurnId', turnId: state.currentTurnId });
for (let step = 0; step < effectiveMaxSteps; step++) {
yield* q.offer({ _tag: 'Step', step: step + 1, max: effectiveMaxSteps });
- const allowedByPolicy = opts.toolPolicy?.allowedTools;
- const tools = registry.describe(allowedByPolicy);
- const toolLookup: ToolLookup = (name: string) => registry.get(name, allowedByPolicy);
- const systemWithCatalog = system;
-
- const stepBeforePayload = { sessionId, step: step + 1 };
- yield* hooks.emitDecision('agent.step.before', stepBeforePayload);
-
- const compressResult = yield* Effect.tryPromise({
- try: () =>
- context.compactIfNeeded(
- session.getTranscriptPath(state),
- messages,
- llm.modelInfo.maxTokens,
- llm
- ),
+ const tools = registry.describe(allowedTools);
+ const toolLookup = (name: string) => registry.get(name, allowedTools);
+
+ yield* hooks.emitDecision('agent.step.before', { sessionId: sid, step: step + 1 });
+
+ const payload = yield* Effect.tryPromise({
+ try: () => context.assemblePayload(session.getTranscriptPath(state), llm.modelInfo.maxTokens, llm),
catch: (e) => new AgentError('LLM_FAILED', String(e)),
});
- if (compressResult.didCompress && compressResult.messages) {
+ if (payload.compressed) {
yield* q.offer({
- _tag: 'ReactiveCompact',
- attempt: 1,
- released: compressResult.released,
- promptEstimate: compressResult.promptEstimate,
+ _tag: 'ContextCompressed',
+ released: payload.released,
+ promptEstimate: payload.promptEstimate,
});
-
- messages = compressResult.messages;
- state.usage = undefined;
}
-
- const llmMessages = [...messages];
-
+ const llmMessages = [...payload.messages];
const { stream: rawStream, response: respPromise } = llm.completeStream(
- {
- messages: llmMessages,
- system: systemWithCatalog,
- tools,
- maxSteps: 1,
- },
- opts.abortSignal
+ { messages: llmMessages, system, tools, maxSteps: 1 },
+ abortSignal
);
yield* Effect.tryPromise({
try: async () => {
for await (const chunk of rawStream) {
- if (opts.abortSignal?.aborted) break;
+ if (abortSignal?.aborted) break;
Effect.runSync(q.offer({ _tag: 'LlmChunk', text: chunk }));
}
},
catch: (e) => new AgentError('LLM_FAILED', String(e)),
});
- const llmResult = yield* Effect.tryPromise({
+ const llmResult: any = yield* Effect.tryPromise({
try: () => respPromise,
catch: (e) => new AgentError('LLM_FAILED', String(e)),
});
if (!llmResult.ok) {
- if (llmResult.error.code === 'CONTEXT_OVERFLOW' && attempt < maxOverflowRetries) {
- const compressResult = yield* Effect.tryPromise({
- try: () =>
- context.compactWithLLM(
- session.getTranscriptPath(state),
- llm.modelInfo.maxTokens,
- llm,
- undefined
- ),
- catch: (e) => new AgentError('LLM_FAILED', String(e)),
- });
- if (compressResult.didCompress && compressResult.messages) {
- messages = compressResult.messages;
- }
- yield* q.offer({
- _tag: 'ReactiveCompact',
- attempt: attempt + 1,
- released: compressResult.released,
- promptEstimate: compressResult.promptEstimate,
- });
- overflow = true;
- break;
- }
yield* q.offer({ _tag: 'Error', error: llmResult.error });
lastResult = Result.err(llmResult.error);
- yield* hooks.emit('agent.turn.end', {
- sessionId,
- turnId: state.currentTurnId,
- status: 'error',
- });
+ yield* hooks.emit('agent.turn.end', { sessionId: sid, turnId: state.currentTurnId, status: 'error' });
break;
}
const resp = llmResult.value;
const toolCalls = resp.toolCalls;
- const assistantMsg: Message = { role: 'assistant', content: resp.content };
- if (toolCalls && toolCalls.length > 0) {
- assistantMsg.tool_calls = toolCalls;
- }
- messages.push(assistantMsg);
yield* q.offer({ _tag: 'Assistant', content: resp.content, toolCalls });
if (resp.usage) {
- yield* q.offer({
- _tag: 'Usage',
- prompt: resp.usage.prompt,
- completion: resp.usage.completion,
- total: resp.usage.total,
- });
+ yield* q.offer({ _tag: 'Usage', prompt: resp.usage.prompt, completion: resp.usage.completion, total: resp.usage.total });
}
if (!toolCalls || toolCalls.length === 0) {
- if (session) {
- yield* session.recordAssistant(state, resp.content, toolCalls || [], resp.usage);
- }
- const stopDecision = yield* hooks.emitDecision('agent.turn.stop', {
- sessionId,
- content: resp.content,
- turnId: state.currentTurnId,
- });
+ yield* session.recordAssistant(state, resp.content, toolCalls || [], resp.usage);
+ const stopDecision = yield* hooks.emitDecision('agent.turn.stop', { sessionId: sid, content: resp.content, turnId: state.currentTurnId });
if (stopDecision && stopDecision.decision === 'continue') {
if (stopContinuations >= effectiveMaxStopContinuations) {
- yield* q.offer({
- _tag: 'Error',
- error: new AgentError('AGENT_LOOP_DETECTED', 'max stop continuations exceeded'),
- });
- yield* hooks.emit('agent.turn.end', {
- sessionId,
- turnId: state.currentTurnId,
- status: 'error',
- });
- memory
- .flushSessionToMemory(state.sessionId, llm, state.cwd)
- .catch((e) => logger.error('memory flush failed:', e));
- return Result.err(
- new AgentError('AGENT_LOOP_DETECTED', 'max stop continuations exceeded')
- );
+ yield* q.offer({ _tag: 'Error', error: new AgentError('AGENT_LOOP_DETECTED', 'max stop continuations exceeded') });
+ yield* hooks.emit('agent.turn.end', { sessionId: sid, turnId: state.currentTurnId, status: 'error' });
+ memory.flushSessionToMemory(state.sessionId, llm, state.cwd).catch((e) => logger.error('memory flush failed:', e));
+ return Result.err(new AgentError('AGENT_LOOP_DETECTED', 'max stop continuations exceeded'));
}
stopContinuations++;
const injection = stopDecision.injection ?? '(continue)';
- if (session) {
- yield* session.recordUser(state, injection);
- }
- messages.push({ role: 'user', content: injection });
+ yield* session.recordUser(state, injection);
continue;
}
if (submittedPlanTitle !== null) {
- yield* hooks.emit('plan.ready', {
- sessionId,
- projectPath,
- title: submittedPlanTitle,
- });
+ yield* hooks.emit('plan.ready', { sessionId: sid, projectPath, title: submittedPlanTitle });
submittedPlanTitle = null;
}
yield* q.offer({ _tag: 'Done', content: resp.content });
lastResult = Result.ok(resp.content);
- yield* hooks.emit('agent.turn.end', {
- sessionId,
- turnId: state.currentTurnId,
- status: 'done',
- });
+ yield* hooks.emit('agent.turn.end', { sessionId: sid, turnId: state.currentTurnId, status: 'done' });
break;
}
- if (toolCalls) {
- for (const tc of toolCalls) {
- yield* q.offer({
- _tag: 'ToolStart',
- id: tc.id,
- name: tc.name,
- args: tc.arguments ?? {},
- });
- }
+ for (const tc of toolCalls as any[]) {
+ yield* q.offer({ _tag: 'ToolStart', id: tc.id, name: tc.name, args: tc.arguments ?? {} });
}
- const record = yield* session.recordAssistant(state, resp.content, toolCalls!, resp.usage);
+ yield* session.recordAssistant(state, resp.content, toolCalls!, resp.usage);
const allResults = yield* executor.executeBatch(toolCalls, state.sessionId, {
- turnId: state.currentTurnId,
- projectPath,
- signal: opts.abortSignal,
- approval: opts.approvalOverride,
- toolLookup,
+ turnId: state.currentTurnId, projectPath, signal: abortSignal, toolLookup,
});
let todoPrinted = false;
@@ -473,93 +295,47 @@ export function agentLoop(
if (r.type === 'denied') {
yield* q.offer({ _tag: 'ToolDenied', id: r.id, name: r.name, reason: r.reason });
} else {
- const isOk = r.type === 'ok';
- yield* q.offer({
- _tag: 'ToolResult',
- id: r.id,
- name: r.name,
- output: resultOut,
- ok: isOk,
- });
- }
- if (!messages.find((m) => m.tool_call_id === r.id)) {
- const content =
- r.type === 'denied'
- ? `[Denied] Tool "${r.name}" was denied: ${r.reason}`
- : (r.output ?? '');
- messages.push({ role: 'tool', content, tool_call_id: r.id, tool_name: r.name });
+ yield* q.offer({ _tag: 'ToolResult', id: r.id, name: r.name, output: resultOut, ok: r.type === 'ok' });
}
if (!todoPrinted && r.name === 'todo_write') {
- yield* q.offer({ _tag: 'TodoUpdate', items: todo.read(sessionId) });
+ yield* q.offer({ _tag: 'TodoUpdate', items: todo.read(sid) as any });
todoPrinted = true;
}
}
- const submitPlanCall = toolCalls?.find((tc) => tc.name === 'submit_plan');
- const submitPlanResult = allResults.find(
- (r) => r.name === 'submit_plan' && r.type === 'ok'
- );
+ const submitPlanCall = toolCalls?.find((tc: any) => tc.name === 'submit_plan');
+ const submitPlanResult = allResults.find((r) => r.name === 'submit_plan' && r.type === 'ok');
if (submitPlanCall && submitPlanResult && submittedPlanTitle === null) {
submittedPlanTitle = String(submitPlanCall.arguments?.title ?? '');
}
}
- if (overflow) continue;
-
yield* checkpoint.snapshotFinal(projectPath, state.sessionId, state.currentTurnId);
-
- memory
- .flushSessionToMemory(state.sessionId, llm, state.cwd)
- .catch((e) => logger.error('memory flush failed:', e));
+ memory.flushSessionToMemory(state.sessionId, llm, state.cwd).catch((e) => logger.error('memory flush failed:', e));
if (lastResult) return lastResult;
yield* q.offer({ _tag: 'Error', error: AgentError.maxStepsReached(effectiveMaxSteps) });
- yield* hooks.emit('agent.turn.end', {
- sessionId,
- turnId: state.currentTurnId,
- status: 'maxSteps',
- });
+ yield* hooks.emit('agent.turn.end', { sessionId: sid, turnId: state.currentTurnId, status: 'maxSteps' });
return Result.err(AgentError.maxStepsReached(effectiveMaxSteps));
- }
+ }).pipe(
+ Effect.interruptible,
+ Effect.onInterrupt(() =>
+ Effect.gen(function* () {
+ yield* Effect.sync(() => {
+ Effect.runSync(q.offer({ _tag: 'Error', error: new AgentError('AGENT_ABORTED', 'cancelled') }));
+ });
+ yield* hooks.emit('agent.turn.end', { sessionId: opts.sid, turnId: opts.state.currentTurnId, status: 'aborted' }).pipe(Effect.ignore);
+ })
+ ),
+ Effect.ensuring(
+ Effect.gen(function* () {
+ yield* checkpoint.snapshotFinal(opts.projectPath, opts.sid, opts.state.currentTurnId).pipe(Effect.ignore);
+ memory.flushSessionToMemory(opts.state.sessionId, opts.llm, opts.projectPath).catch((e) => logger.error('memory flush failed:', e));
+ })
+ )
+ );
+ }
- yield* q.offer({ _tag: 'Error', error: AgentError.maxStepsReached(effectiveMaxSteps) });
- yield* hooks.emit('agent.turn.end', {
- sessionId,
- turnId: state.currentTurnId,
- status: 'maxSteps',
- });
- memory
- .flushSessionToMemory(state.sessionId, llm, state.cwd)
- .catch((e) => logger.error('memory flush failed:', e));
- return Result.err(AgentError.maxStepsReached(effectiveMaxSteps));
- }).pipe(
- Effect.interruptible,
- Effect.onInterrupt(() =>
- Effect.gen(function* () {
- yield* Effect.sync(() => {
- Effect.runSync(
- q.offer({ _tag: 'Error', error: new AgentError('AGENT_ABORTED', 'cancelled') })
- );
- });
- yield* hooks
- .emit('agent.turn.end', {
- sessionId,
- turnId: state.currentTurnId,
- status: 'aborted',
- })
- .pipe(Effect.ignore);
- })
- ),
- Effect.ensuring(
- Effect.gen(function* () {
- const cp = yield* CheckpointService;
- yield* cp.snapshotFinal(projectPath, sessionId, state.currentTurnId).pipe(Effect.ignore);
- const mem = yield* MemoryService;
- mem
- .flushSessionToMemory(state.sessionId, llm, state.cwd)
- .catch((e) => logger.error('memory flush failed:', e));
- })
- )
- );
-}
+ return { runTurn };
+} as any));
diff --git a/packages/codingcode/src/agent/config.ts b/packages/codingcode/src/agent/config.ts
deleted file mode 100644
index 7ac00240..00000000
--- a/packages/codingcode/src/agent/config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { loadConfig } from '@codingcode/infra/config';
-import type { ResolvedConfig } from './types.js';
-
-export function resolveConfig(): ResolvedConfig {
- const cfg = loadConfig();
- return {
- maxSteps: cfg.maxSteps ?? 250,
- maxStopContinuations: cfg.maxStopContinuations ?? 3,
- };
-}
diff --git a/packages/codingcode/src/agent/deps.ts b/packages/codingcode/src/agent/deps.ts
new file mode 100644
index 00000000..2f67eab8
--- /dev/null
+++ b/packages/codingcode/src/agent/deps.ts
@@ -0,0 +1,88 @@
+import { Context } from 'effect';
+import type { Effect } from 'effect';
+import type { AgentError } from '../core/error.js';
+import type { ToolCall, Message } from '../core/types.js';
+import type { SessionStoreState, SessionEvent, TokenUsage } from '../session/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 { AgentProfileName } from './profile.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: AgentProfileName; permissionMode: PermissionMode }, extra?: { parentSessionId?: string; agentName?: string }): Effect.Effect;
+ recordUser(state: SessionStoreState, content: string): Effect.Effect;
+ recordAssistant(state: SessionStoreState, content: string, toolCalls: any[], usage?: TokenUsage): Effect.Effect;
+ recordToolResult(state: SessionStoreState, name: string, id: string, output: string): Effect.Effect;
+ incrementTurn(state: SessionStoreState): number;
+ getTranscriptPath(state: SessionStoreState): string;
+ getActiveProfile(cwd: string, sid: string): Effect.Effect;
+ setPermissionMode(cwd: string, sid: string, mode: PermissionMode): Effect.Effect;
+ setActiveProfile(cwd: string, sid: string, profile: AgentProfileName): 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 }): Effect.Effect;
+ fork(opts?: { permissionMode?: PermissionMode }): Effect.Effect;
+}>() {}
+
+export class SkillPort extends Context.Tag('AgentSkillPort');
+ evictProject(cwd: string): Effect.Effect;
+}>() {}
+
+export class McpPort extends Context.Tag('AgentMcpPort');
+}>() {}
+
+export class ContextPort extends Context.Tag('AgentContextPort');
+}>() {}
+
+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');
+}>() {}
diff --git a/packages/codingcode/src/agent/port.ts b/packages/codingcode/src/agent/port.ts
new file mode 100644
index 00000000..189ad53a
--- /dev/null
+++ b/packages/codingcode/src/agent/port.ts
@@ -0,0 +1,26 @@
+import { Context } from 'effect';
+import type { Effect } from 'effect';
+import type { AgentEvent } from './types.js';
+import type { AgentProfileName } from './profile.js';
+import type { PermissionMode } from '../approval/types.js';
+
+export interface RunTurnOptions {
+ sessionId?: string;
+ cwd: string;
+ signal?: AbortSignal;
+ permissionMode?: PermissionMode;
+ model?: string;
+ activeProfile?: AgentProfileName;
+}
+
+export interface AgentShape {
+ runTurn(
+ input: string,
+ opts: RunTurnOptions
+ ): Effect.Effect<{
+ stream: AsyncGenerator;
+ sessionId: string;
+ }>;
+}
+
+export class AgentService extends Context.Tag('AgentService')() {}
diff --git a/packages/codingcode/src/agent/profile.ts b/packages/codingcode/src/agent/profile.ts
index d2f4291f..8d189fa9 100644
--- a/packages/codingcode/src/agent/profile.ts
+++ b/packages/codingcode/src/agent/profile.ts
@@ -1,12 +1,112 @@
-import { readFileSync } from 'fs';
+export type AgentProfileName = 'plan' | 'build';
+
+export interface AgentProfile {
+ name: AgentProfileName;
+ systemPrompt?: string;
+ maxSteps?: number;
+}
+
+import { readActiveProfileSync } from '../session/file-ops.js';
import type { DecisionHandler } from '../hooks/types.js';
-import { computePaths } from '../core/path.js';
-import type { AgentProfile } from '../subagent/types.js';
-import { BUILD_PROMPT, PLAN_PROMPT } from './prompt.js';
export const PLAN_PROFILE_NAME = 'plan' as const;
export const BUILD_PROFILE_NAME = 'build' as const;
+export const BUILD_PROMPT = `You are a coding assistant —an AI agent that helps users with software engineering tasks.
+
+## How you work
+- Your text output is displayed to the user as formatted text. Tool calls and their results are shown separately —the user can see what tools you used and their outcomes.
+- Tools run behind a permission system. If a tool call is denied, the user declined it —adjust your approach, do not retry the same call verbatim.
+- Messages may contain tags injected by the system, not by the user. They contain useful operational information —always read and follow them.
+
+## Rules
+1. Read files before modifying them —never guess file contents
+2. Use search_code or search_files to locate code before reading —this is faster than reading entire files blindly
+3. Prefer editing existing files over creating new ones
+4. Make small, focused changes —avoid large rewrites
+5. Run tests or type-check after changes when applicable
+6. If the user's request is ambiguous, ask for clarification
+7. For complex or broad tasks (understanding a whole module, cross-file analysis, comprehensive search):
+ a. Briefly assess the task scope using your own reasoning —do not use tools for exploration at this stage, as that would consume your limited context window.
+ b. If you can clearly handle it without extensive file reading or searching, proceed yourself.
+ c. Otherwise, delegate the discovery task with dispatch_agent when a runtime-configured subagent is available.
+
+## Using your tools
+- **Prefer dedicated tools over shell commands.** Use read_file instead of cat, edit_file instead of sed, search_code instead of grep. Dedicated tools give the user better visibility into your work.
+- **Call multiple tools in parallel** when they are independent —for example, reading several files at once, or searching with different patterns. Do NOT make sequential calls when the calls don't depend on each other.
+- After editing a file, do NOT re-read it to verify —the edit tool already confirms success or reports failure. Only re-read if you suspect the edit did not apply correctly.
+- Reserve execute_command for actual system commands and terminal operations (git, npm, build, test). Do not use it for file operations that dedicated tools can handle.
+
+## Executing actions with care
+Consider the reversibility and blast radius of actions before taking them:
+- **Freely take** local, reversible actions: editing files, running tests, reading code.
+- **Confirm with the user before** hard-to-reverse or outward-facing actions: pushing code, deleting files/branches, force-pushing, modifying CI/CD pipelines, sending messages to external services.
+- **Never** use destructive commands (rm -rf /, sudo, git reset --hard, git push --force, git clean -f) unless explicitly requested and approved by the user.
+- When you encounter unexpected state (unfamiliar files, branches, or configuration), investigate before deleting or overwriting —it may be the user's in-progress work. Never revert changes you did not make.
+
+## Git operations
+- Do NOT commit changes unless the user explicitly asks you to.
+- Do NOT push to remote unless the user explicitly asks you to.
+- Do NOT use destructive git commands (git reset --hard, git push --force, git clean -f, git checkout -- .) unless explicitly requested and approved.
+- If you notice unexpected changes in the working tree that you did not make, investigate before acting —they may be the user's in-progress work.
+
+## Professional objectivity
+Prioritize technical accuracy over validating the user's beliefs. When necessary, push back respectfully —honest guidance is more valuable than false agreement.
+- Do not begin responses with conversational interjections ("Got it", "Sure", "Great question")
+- Do not apologize unnecessarily when results are unexpected
+
+## Follow existing conventions
+When modifying code, first look at the surrounding code's style (naming, frameworks, imports) and match it:
+- **Never assume a library is available** —check imports in neighboring files, or check the dependency file (package.json, cargo.toml, requirements.txt, etc.) before using it.
+- **When creating a new component**, first look at existing components to understand naming conventions, typing patterns, and framework choices.
+- **When editing code**, look at the surrounding context (especially imports) to understand the code's choice of frameworks and libraries, then make your change in the most idiomatic way.
+- **Comments**: default to writing no comments. Only add one when the WHY is non-obvious —a hidden constraint, a subtle invariant, or a workaround for a specific bug. Do not explain WHAT the code does.
+
+## Code references
+When referencing code, use the format \`file_path:line_number\` for easy navigation.
+
+## Output efficiency
+- Be concise. Lead with the answer or action, not with reasoning or preamble.
+- Skip filler words and unnecessary transitions. Do not restate what the user said —just do it.
+- When working on a multi-step task, give brief updates at key moments (when you find something, change direction, or hit a blocker). One sentence per update is enough.
+- When the task is done, give a one-to-two sentence summary of what changed. Do not narrate your entire process.
+- Match the response to the question: a simple question gets a direct answer, not headers and sections.
+
+
+Respond in the user's language. Use code blocks for code.`;
+
+export const PLAN_PROMPT = `You are a planning agent. Your role is to analyze the codebase and produce an implementation plan that the user reviews and approves before any code is written.
+
+You can read files and search code. You can submit a plan via the \`submit_plan\` tool — each call overwrites the previous plan file; use it to revise your plan based on user feedback.
+
+In plan profile, write_file / edit_file / execute_command are denied. The only write operation allowed is \`submit_plan\`.
+
+## Research process
+1. Understand the project structure and conventions
+2. Identify relevant files and existing patterns
+3. Analyze dependencies and potential impacts
+4. Assess complexity and risks
+5. Check for existing implementations or similar patterns
+
+## Output format
+When ready, call \`submit_plan({ title, plan_content: "..." })\` with a Markdown plan:
+- **Current state**: What exists today
+- **Key files**: Files that need modification or creation, with line references
+- **Dependencies and risks**: Breaking changes, third-party concerns
+- **Recommended approach**: Step-by-step implementation strategy
+- **Phases**: If complex, break into ordered phases
+
+## After submit_plan
+submit_plan returns synchronously after writing the plan file. Once you have called it, stop and wait for the user's decision — do not call submit_plan again until the user responds, and do not attempt to use any other write tool.
+
+The user's decision arrives as the next user message. The system has already handled the agent-profile switch (plan → build on approval, plan → plan on revise, no change on cancel); the message body itself is your signal:
+
+- "Implement"/"proceed"/"go ahead" (or any explicit approval) — the plan is approved. Acknowledge briefly and stop. The build agent will pick up the plan from the persisted file.
+- The body contains a revised plan (a Markdown document, often with explicit section headers, or with a "Revise the plan with these changes:" wrapper) — treat the body as the new plan_content, call \`submit_plan\` again with the same title and the revised content, then stop.
+- "Cancel"/"do not implement" — the plan is rejected. Acknowledge briefly and stop.
+
+Never re-call submit_plan on your own initiative. Never treat an implement message as a request for further exploration.`;
+
export const PLAN_PROFILE: AgentProfile = {
name: PLAN_PROFILE_NAME,
systemPrompt: PLAN_PROMPT,
@@ -18,10 +118,6 @@ export const BUILD_PROFILE: AgentProfile = {
systemPrompt: BUILD_PROMPT,
};
-export function isPlanProfile(p: { name: string } | null | undefined): boolean {
- return p?.name === PLAN_PROFILE_NAME;
-}
-
export const PLAN_PROFILE_ALLOWED_TOOLS: ReadonlySet = new Set([
'read_file',
'search_files',
@@ -30,16 +126,28 @@ export const PLAN_PROFILE_ALLOWED_TOOLS: ReadonlySet = new Set([
'submit_plan',
]);
+export function isPlanProfile(p: { name: string } | null | undefined): boolean {
+ return p?.name === PLAN_PROFILE_NAME;
+}
+
+function isAgentProfileName(name: string): name is AgentProfileName {
+ return name === PLAN_PROFILE_NAME || name === BUILD_PROFILE_NAME;
+}
+
+export function resolveProfile(name: AgentProfileName): AgentProfile {
+ return name === PLAN_PROFILE_NAME ? PLAN_PROFILE : BUILD_PROFILE;
+}
+
+export function resolveSubagentProfile(name: string): AgentProfile | undefined {
+ return isAgentProfileName(name) ? resolveProfile(name) : undefined;
+}
+
+export function getAllowedTools(profile: AgentProfile | undefined): ReadonlySet | undefined {
+ return isPlanProfile(profile) ? PLAN_PROFILE_ALLOWED_TOOLS : undefined;
+}
+
export function isSessionUsingPlanProfile(sessionId: string, cwd: string): boolean {
- try {
- const paths = computePaths(cwd, sessionId);
- const idx = JSON.parse(readFileSync(paths.indexPath, 'utf8')) as {
- activeProfile?: string;
- };
- return idx?.activeProfile === PLAN_PROFILE_NAME;
- } catch {
- return false;
- }
+ return readActiveProfileSync(cwd, sessionId) === PLAN_PROFILE_NAME;
}
export const planProfileGateHook: DecisionHandler = (payload) => {
diff --git a/packages/codingcode/src/agent/prompt.ts b/packages/codingcode/src/agent/prompt.ts
index 6b675856..c9372c3d 100644
--- a/packages/codingcode/src/agent/prompt.ts
+++ b/packages/codingcode/src/agent/prompt.ts
@@ -1,99 +1,13 @@
-import type { SystemPromptOptions } from './types.js';
-
-export const BUILD_PROMPT = `You are a coding assistant —an AI agent that helps users with software engineering tasks.
-
-## How you work
-- Your text output is displayed to the user as formatted text. Tool calls and their results are shown separately —the user can see what tools you used and their outcomes.
-- Tools run behind a permission system. If a tool call is denied, the user declined it —adjust your approach, do not retry the same call verbatim.
-- Messages may contain tags injected by the system, not by the user. They contain useful operational information —always read and follow them.
-
-## Rules
-1. Read files before modifying them —never guess file contents
-2. Use search_code or search_files to locate code before reading —this is faster than reading entire files blindly
-3. Prefer editing existing files over creating new ones
-4. Make small, focused changes —avoid large rewrites
-5. Run tests or type-check after changes when applicable
-6. If the user's request is ambiguous, ask for clarification
-7. For complex or broad tasks (understanding a whole module, cross-file analysis, comprehensive search):
- a. Briefly assess the task scope using your own reasoning —do not use tools for exploration at this stage, as that would consume your limited context window.
- b. If you can clearly handle it without extensive file reading or searching, proceed yourself.
- c. Otherwise, delegate the discovery task with dispatch_agent when a runtime-configured subagent is available.
-
-## Using your tools
-- **Prefer dedicated tools over shell commands.** Use read_file instead of cat, edit_file instead of sed, search_code instead of grep. Dedicated tools give the user better visibility into your work.
-- **Call multiple tools in parallel** when they are independent —for example, reading several files at once, or searching with different patterns. Do NOT make sequential calls when the calls don't depend on each other.
-- After editing a file, do NOT re-read it to verify —the edit tool already confirms success or reports failure. Only re-read if you suspect the edit did not apply correctly.
-- Reserve execute_command for actual system commands and terminal operations (git, npm, build, test). Do not use it for file operations that dedicated tools can handle.
-
-## Executing actions with care
-Consider the reversibility and blast radius of actions before taking them:
-- **Freely take** local, reversible actions: editing files, running tests, reading code.
-- **Confirm with the user before** hard-to-reverse or outward-facing actions: pushing code, deleting files/branches, force-pushing, modifying CI/CD pipelines, sending messages to external services.
-- **Never** use destructive commands (rm -rf /, sudo, git reset --hard, git push --force, git clean -f) unless explicitly requested and approved by the user.
-- When you encounter unexpected state (unfamiliar files, branches, or configuration), investigate before deleting or overwriting —it may be the user's in-progress work. Never revert changes you did not make.
-
-## Git operations
-- Do NOT commit changes unless the user explicitly asks you to.
-- Do NOT push to remote unless the user explicitly asks you to.
-- Do NOT use destructive git commands (git reset --hard, git push --force, git clean -f, git checkout -- .) unless explicitly requested and approved.
-- If you notice unexpected changes in the working tree that you did not make, investigate before acting —they may be the user's in-progress work.
-
-## Professional objectivity
-Prioritize technical accuracy over validating the user's beliefs. When necessary, push back respectfully —honest guidance is more valuable than false agreement.
-- Do not begin responses with conversational interjections ("Got it", "Sure", "Great question")
-- Do not apologize unnecessarily when results are unexpected
-
-## Follow existing conventions
-When modifying code, first look at the surrounding code's style (naming, frameworks, imports) and match it:
-- **Never assume a library is available** —check imports in neighboring files, or check the dependency file (package.json, cargo.toml, requirements.txt, etc.) before using it.
-- **When creating a new component**, first look at existing components to understand naming conventions, typing patterns, and framework choices.
-- **When editing code**, look at the surrounding context (especially imports) to understand the code's choice of frameworks and libraries, then make your change in the most idiomatic way.
-- **Comments**: default to writing no comments. Only add one when the WHY is non-obvious —a hidden constraint, a subtle invariant, or a workaround for a specific bug. Do not explain WHAT the code does.
-
-## Code references
-When referencing code, use the format \`file_path:line_number\` for easy navigation.
-
-## Output efficiency
-- Be concise. Lead with the answer or action, not with reasoning or preamble.
-- Skip filler words and unnecessary transitions. Do not restate what the user said —just do it.
-- When working on a multi-step task, give brief updates at key moments (when you find something, change direction, or hit a blocker). One sentence per update is enough.
-- When the task is done, give a one-to-two sentence summary of what changed. Do not narrate your entire process.
-- Match the response to the question: a simple question gets a direct answer, not headers and sections.
-
-
-Respond in the user's language. Use code blocks for code.`;
-
-export const PLAN_PROMPT = `You are a planning agent. Your role is to analyze the codebase and produce an implementation plan that the user reviews and approves before any code is written.
-
-You can read files and search code. You can submit a plan via the \`submit_plan\` tool — each call overwrites the previous plan file; use it to revise your plan based on user feedback.
-
-In plan profile, write_file / edit_file / execute_command are denied. The only write operation allowed is \`submit_plan\`.
-
-## Research process
-1. Understand the project structure and conventions
-2. Identify relevant files and existing patterns
-3. Analyze dependencies and potential impacts
-4. Assess complexity and risks
-5. Check for existing implementations or similar patterns
-
-## Output format
-When ready, call \`submit_plan({ title, plan_content: "..." })\` with a Markdown plan:
-- **Current state**: What exists today
-- **Key files**: Files that need modification or creation, with line references
-- **Dependencies and risks**: Breaking changes, third-party concerns
-- **Recommended approach**: Step-by-step implementation strategy
-- **Phases**: If complex, break into ordered phases
-
-## After submit_plan
-submit_plan returns synchronously after writing the plan file. Once you have called it, stop and wait for the user's decision — do not call submit_plan again until the user responds, and do not attempt to use any other write tool.
-
-The user's decision arrives as the next user message. The system has already handled the agent-profile switch (plan → build on approval, plan → plan on revise, no change on cancel); the message body itself is your signal:
-
-- "Implement"/"proceed"/"go ahead" (or any explicit approval) — the plan is approved. Acknowledge briefly and stop. The build agent will pick up the plan from the persisted file.
-- The body contains a revised plan (a Markdown document, often with explicit section headers, or with a "Revise the plan with these changes:" wrapper) — treat the body as the new plan_content, call \`submit_plan\` again with the same title and the revised content, then stop.
-- "Cancel"/"do not implement" — the plan is rejected. Acknowledge briefly and stop.
+import { BUILD_PROMPT } from './profile.js';
+
+interface SystemPromptOptions {
+ cwd: string;
+ platform: string;
+ shell: string;
+ rules?: string;
+ profileSystemPrompt?: string;
+}
-Never re-call submit_plan on your own initiative. Never treat an implement message as a request for further exploration.`;
const DEFAULT_ENV_PROMPT = `## Environment
- Working directory: {{cwd}}
- Operating system: {{platform}}
diff --git a/packages/codingcode/src/agent/stream-adapter.ts b/packages/codingcode/src/agent/stream-adapter.ts
index c9fd1036..be4119f4 100644
--- a/packages/codingcode/src/agent/stream-adapter.ts
+++ b/packages/codingcode/src/agent/stream-adapter.ts
@@ -47,6 +47,13 @@ export async function* agentEventToStreamChunk(
case 'TodoUpdate':
yield { type: 'todo_update', items: event.items as any };
break;
+ case 'ContextCompressed':
+ yield {
+ type: 'context_compressed',
+ released: event.released,
+ promptEstimate: event.promptEstimate,
+ };
+ break;
case 'Usage':
yield {
type: 'usage',
@@ -55,13 +62,6 @@ export async function* agentEventToStreamChunk(
total: event.total,
};
break;
- case 'ReactiveCompact':
- yield {
- type: 'reactive_compact',
- released: event.released,
- promptEstimate: event.promptEstimate,
- };
- break;
}
}
}
diff --git a/packages/codingcode/src/agent/todo.ts b/packages/codingcode/src/agent/todo.ts
deleted file mode 100644
index c8d6ea90..00000000
--- a/packages/codingcode/src/agent/todo.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Effect } from 'effect';
-import type { Todo, TodoCounts } from './types.js';
-
-export const TODO_MAX_ITEMS = 20;
-export const TODO_MAX_STEP_LEN = 60;
-
-export function countByStatus(plan: Todo[]): TodoCounts {
- const c: TodoCounts = { pending: 0, in_progress: 0, completed: 0 };
- for (const t of plan) c[t.status]++;
- return c;
-}
-
-export class TodoService extends Effect.Service()('Todo', {
- sync: () => {
- const store = new Map();
-
- return {
- read(sessionId: string): Todo[] {
- return store.get(sessionId) ?? [];
- },
-
- write(sessionId: string, plan: Todo[]): void {
- store.set(sessionId, plan);
- },
-
- reset(): void {
- store.clear();
- },
- };
- },
-}) {}
diff --git a/packages/codingcode/src/agent/types.ts b/packages/codingcode/src/agent/types.ts
index 4fb3cfce..c18672a9 100644
--- a/packages/codingcode/src/agent/types.ts
+++ b/packages/codingcode/src/agent/types.ts
@@ -1,35 +1,5 @@
import type { ToolCall } from '../core/types.js';
import type { AgentError } from '../core/error.js';
-import type { SessionStoreState } from '../session/types.js';
-import type { LLMClient } from '../llm/client.js';
-import type { ToolDefinition, ToolVisibilityPolicy } from '../tools/types.js';
-import type { AgentProfile } from '../subagent/types.js';
-
-export type TodoStatus = 'pending' | 'in_progress' | 'completed';
-
-export interface Todo {
- step: string;
- status: TodoStatus;
-}
-
-export interface TodoCounts {
- pending: number;
- in_progress: number;
- completed: number;
-}
-
-export interface SystemPromptOptions {
- cwd: string;
- platform: string;
- shell: string;
- rules?: string;
- profileSystemPrompt?: string;
-}
-
-export interface ResolvedConfig {
- maxSteps: number;
- maxStopContinuations: number;
-}
export type AgentEvent =
| { readonly _tag: 'LlmChunk'; readonly text: string }
@@ -54,12 +24,6 @@ export type AgentEvent =
readonly ok: boolean;
}
| { readonly _tag: 'Step'; readonly step: number; readonly max: number }
- | {
- readonly _tag: 'ReactiveCompact';
- readonly attempt: number;
- readonly released: number;
- readonly promptEstimate: number;
- }
| { readonly _tag: 'Error'; readonly error: AgentError }
| { readonly _tag: 'Done'; readonly content: string }
| {
@@ -70,27 +34,14 @@ export type AgentEvent =
}>;
}
| { readonly _tag: 'TurnId'; readonly turnId: number }
+ | {
+ readonly _tag: 'ContextCompressed';
+ readonly released: number;
+ readonly promptEstimate: number;
+ }
| {
readonly _tag: 'Usage';
readonly prompt: number;
readonly completion: number;
readonly total: number;
};
-
-export interface RunStreamOptions {
- state: SessionStoreState;
- llm: LLMClient;
- profile?: AgentProfile;
- systemOverride?: string;
- coreAllowlist?: ReadonlySet;
- toolPolicy?: ToolVisibilityPolicy;
- dispatchTool?: ToolDefinition;
- mcpTools?: ToolDefinition[];
- abortSignal?: AbortSignal;
- parentSessionId?: string;
- agentName?: string;
- maxStepsOverride?: number;
- maxStopContinuations?: number;
- approvalOverride?: import('../approval/index.js').ApprovalService;
- rulesText?: string;
-}
diff --git a/packages/codingcode/src/approval/index.ts b/packages/codingcode/src/approval/approval.ts
similarity index 87%
rename from packages/codingcode/src/approval/index.ts
rename to packages/codingcode/src/approval/approval.ts
index 251c7597..1886787c 100644
--- a/packages/codingcode/src/approval/index.ts
+++ b/packages/codingcode/src/approval/approval.ts
@@ -1,27 +1,25 @@
-import { Effect } from 'effect';
-import { HookService } from '../hooks/registry.js';
+import { Layer, Effect } from 'effect';
+import { HookService } from '../hooks/port.js';
import type { PermissionMode, PermissionRule, ApprovalDecision } from './types.js';
import { createRuleEngine, type RuleEngine } from './rule-engine.js';
-import { DEFAULT_DENY_RULES, READONLY_TOOL_NAMES, DANGEROUS_TOOL_NAMES } from './presets.js';
+import { DEFAULT_DENY_RULES, DANGEROUS_TOOL_NAMES } from './presets.js';
import { runPipeline } from './pipeline.js';
-import { ApprovalWaitService } from './async-confirm.js';
+import { ApprovalWaitService } from './wait-port.js';
+import { ApprovalService } from './port.js';
-export class ApprovalService extends Effect.Service()('Approval', {
- effect: Effect.gen(function* () {
+export const ApprovalLayer = Layer.effect(ApprovalService, Effect.gen(function* () {
const hooks = yield* HookService;
const approvalWait = yield* ApprovalWaitService;
const ruleEngine: RuleEngine = createRuleEngine(DEFAULT_DENY_RULES);
const destructiveTools = new Set(DANGEROUS_TOOL_NAMES);
- const readonlyTools = new Set(READONLY_TOOL_NAMES);
function makeForkedService(
engine: RuleEngine,
permMode: PermissionMode,
- roTools: Set,
destTools: Set
- ): ApprovalService {
+ ): any {
let currentPermMode = permMode;
- return ApprovalService.make({
+ return {
evaluate: (request: {
tool: string;
input: Record;
@@ -29,7 +27,7 @@ export class ApprovalService extends Effect.Service()('Approval
callId?: string;
sessionId: string;
projectPath?: string;
- }): Effect.Effect =>
+ }): any =>
runPipeline(
{
tool: request.tool,
@@ -39,7 +37,6 @@ export class ApprovalService extends Effect.Service()('Approval
},
{
ruleEngine: engine,
- readonlyTools: roTools,
destructiveTools: destTools,
permissionMode: currentPermMode,
onAlways: (rule) => engine.addRule(rule),
@@ -85,11 +82,10 @@ export class ApprovalService extends Effect.Service()('Approval
return makeForkedService(
nextEngine,
opts?.permissionMode ?? currentPermMode,
- new Set(roTools),
new Set(destTools)
);
}),
- });
+ };
}
return {
@@ -100,7 +96,7 @@ export class ApprovalService extends Effect.Service()('Approval
callId?: string;
sessionId: string;
projectPath?: string;
- }): Effect.Effect =>
+ }): any =>
runPipeline(
{
tool: request.tool,
@@ -110,7 +106,6 @@ export class ApprovalService extends Effect.Service()('Approval
},
{
ruleEngine,
- readonlyTools,
destructiveTools,
permissionMode: 'default',
onAlways: (rule) => ruleEngine.addRule(rule),
@@ -163,10 +158,8 @@ export class ApprovalService extends Effect.Service()('Approval
return makeForkedService(
childEngine,
opts?.permissionMode ?? 'default',
- new Set(readonlyTools),
new Set(destructiveTools)
);
}),
};
- }),
-}) {}
+} as any));
diff --git a/packages/codingcode/src/approval/confirmation.ts b/packages/codingcode/src/approval/confirmation.ts
index a41bbac1..04b5182f 100644
--- a/packages/codingcode/src/approval/confirmation.ts
+++ b/packages/codingcode/src/approval/confirmation.ts
@@ -1,6 +1,6 @@
import { Effect } from 'effect';
import type { PermissionRule } from './types.js';
-import { ApprovalWaitService } from './async-confirm.js';
+import { ApprovalWaitService } from './wait-port.js';
export type ConfirmResult =
| { type: 'allow' }
diff --git a/packages/codingcode/src/approval/pipeline.ts b/packages/codingcode/src/approval/pipeline.ts
index d15a0921..3739c67e 100644
--- a/packages/codingcode/src/approval/pipeline.ts
+++ b/packages/codingcode/src/approval/pipeline.ts
@@ -2,12 +2,11 @@ import { Effect } from 'effect';
import type { ApprovalDecision, PermissionMode, PermissionRule, ToolCallRequest } from './types.js';
import type { RuleEngine } from './rule-engine.js';
import { userConfirmAsync } from './confirmation.js';
-import { ApprovalWaitService } from './async-confirm.js';
-import { HookService } from '../hooks/registry.js';
+import { ApprovalWaitService } from './wait-port.js';
+import { HookService } from '../hooks/port.js';
export interface PipelineOptions {
ruleEngine: RuleEngine;
- readonlyTools: Set;
destructiveTools: Set;
permissionMode: PermissionMode;
/** Called when user selects Always — allows caller to persist the rule. */
@@ -25,7 +24,6 @@ export interface PipelineOptions {
const LAYER_NAMES = [
'RuleEngine',
- 'ReadonlyWhitelist',
'PermissionMode',
'HookPreToolUse',
'UserConfirmation',
@@ -35,10 +33,10 @@ const LAYER_NAMES = [
export function runPipeline(
request: ToolCallRequest,
opts: PipelineOptions
-): Effect.Effect {
+): any {
return Effect.gen(function* () {
- const hooks = yield* HookService;
- const approvalWait = yield* ApprovalWaitService;
+ const hooks: any = yield* HookService;
+ const approvalWait: any = yield* ApprovalWaitService;
const asyncConfirm = yield* approvalWait.hasEmitter(opts.sessionId);
const layers: string[] = [];
@@ -52,35 +50,23 @@ export function runPipeline(
}
}
- // Layer 2: Read-only Whitelist
- {
- if (opts.readonlyTools.has(request.tool)) {
- const result: ApprovalDecision = {
- type: 'allow',
- source: 'readonly-whitelist',
- };
- layers.push(LAYER_NAMES[1]);
- const final = yield* recordAuditAndReturn(hooks, request, result, layers);
- return final;
- }
- }
-
- // Layer 3: Permission Mode
+ // Layer 2: Permission Mode — the single auto-allow gate. Read-only tools
+ // are NOT unconditionally whitelisted; acceptEdits covers them as
+ // non-destructive. In default mode nothing is auto-allowed here.
{
const modeResult = applyPermissionMode(
request.tool,
opts.permissionMode,
- opts.readonlyTools,
opts.destructiveTools
);
if (modeResult) {
- layers.push(LAYER_NAMES[2]);
+ layers.push(LAYER_NAMES[1]);
const final = yield* recordAuditAndReturn(hooks, request, modeResult, layers);
return final;
}
}
- // Layer 4: Hook PreToolUse
+ // Layer 3: Hook PreToolUse
{
const hookResult = yield* Effect.gen(function* () {
const result = yield* hooks.emitDecision('tool.approval.pre', {
@@ -95,7 +81,7 @@ export function runPipeline(
return result;
});
if (hookResult) {
- layers.push(LAYER_NAMES[3]);
+ layers.push(LAYER_NAMES[2]);
if (hookResult.decision === 'deny') {
const result: ApprovalDecision = {
type: 'deny',
@@ -119,9 +105,9 @@ export function runPipeline(
}
}
- // Layer 5: User Confirmation
+ // Layer 4: User Confirmation
{
- layers.push(LAYER_NAMES[4]);
+ layers.push(LAYER_NAMES[3]);
if (request.tool === 'submit_plan') {
const result: ApprovalDecision = {
@@ -176,7 +162,6 @@ export function runPipeline(
function applyPermissionMode(
tool: string,
mode: PermissionMode,
- readonlyTools: Set,
destructiveTools: Set
): ApprovalDecision | null {
switch (mode) {
@@ -185,7 +170,8 @@ function applyPermissionMode(
return { type: 'allow', source: 'permission-mode' };
case 'acceptEdits':
- // Accept edits: read-only + edit tools auto-allow, destructive tools need confirmation
+ // Accept edits: non-destructive tools (read-only + edit) auto-allow,
+ // destructive tools need confirmation
if (!destructiveTools.has(tool)) {
return { type: 'allow', source: 'permission-mode' };
}
@@ -198,13 +184,13 @@ function applyPermissionMode(
}
function recordAuditAndReturn(
- hooks: HookService,
+ hooks: any,
request: ToolCallRequest,
decision: ApprovalDecision,
passedLayers: string[]
-): Effect.Effect {
+): any {
return Effect.gen(function* () {
- passedLayers.push(LAYER_NAMES[5]);
+ passedLayers.push(LAYER_NAMES[4]);
yield* hooks.emit('tool.approval.post', {
tool: request.tool,
input: request.input,
diff --git a/packages/codingcode/src/approval/port.ts b/packages/codingcode/src/approval/port.ts
new file mode 100644
index 00000000..ed868a83
--- /dev/null
+++ b/packages/codingcode/src/approval/port.ts
@@ -0,0 +1,14 @@
+import { Context } from 'effect';
+import type { Effect } from 'effect';
+import type { PermissionMode, PermissionRule, ApprovalDecision } from './types.js';
+
+export interface ApprovalShape {
+ evaluate(request: { tool: string; input: Record; context?: Record; callId?: string; sessionId: string; projectPath?: string }): Effect.Effect;
+ addRule(rule: PermissionRule): Effect.Effect;
+ removeRule(id: string): Effect.Effect;
+ setPermissionMode(mode: PermissionMode): Effect.Effect;
+ getPermissionMode(): PermissionMode;
+ fork(opts?: { extraDenyRules?: PermissionRule[]; readonly?: boolean; permissionMode?: PermissionMode }): Effect.Effect;
+}
+
+export class ApprovalService extends Context.Tag('Approval')() {}
diff --git a/packages/codingcode/src/approval/presets.ts b/packages/codingcode/src/approval/presets.ts
index c611e4f4..40582368 100644
--- a/packages/codingcode/src/approval/presets.ts
+++ b/packages/codingcode/src/approval/presets.ts
@@ -84,14 +84,4 @@ export const DEFAULT_DENY_RULES: PermissionRule[] = [
},
];
-export const READONLY_TOOL_NAMES: string[] = [
- 'read_file',
- 'search_code',
- 'search_files',
- 'fetch_url',
- 'web_search',
- 'dispatch_agent',
- 'todo_write',
-];
-
export const DANGEROUS_TOOL_NAMES: string[] = ['execute_command'];
diff --git a/packages/codingcode/src/approval/wait-port.ts b/packages/codingcode/src/approval/wait-port.ts
new file mode 100644
index 00000000..3447c944
--- /dev/null
+++ b/packages/codingcode/src/approval/wait-port.ts
@@ -0,0 +1,16 @@
+import { Context } from 'effect';
+import type { Effect } from 'effect';
+import type { ConfirmResult } from './confirmation.js';
+
+export interface ApprovalWaitShape {
+ waitForConfirm(id: string, sessionId: string): Effect.Effect;
+ resolveConfirm(id: string, sessionId: string, result: ConfirmResult): Effect.Effect;
+ getPending(sessionId?: string): Effect.Effect;
+ emitApprovalRequest(sessionId: string, id: string, tool: string, args: Record): Effect.Effect;
+ registerEmitter(sessionId: string, fn: (id: string, tool: string, args: Record) => void): Effect.Effect;
+ delegateEmitter(childSessionId: string, parentSessionId: string): Effect.Effect;
+ unregisterEmitter(sessionId: string): Effect.Effect;
+ hasEmitter(sessionId: string): Effect.Effect;
+}
+
+export class ApprovalWaitService extends Context.Tag('ApprovalWait')() {}
diff --git a/packages/codingcode/src/approval/async-confirm.ts b/packages/codingcode/src/approval/wait.ts
similarity index 92%
rename from packages/codingcode/src/approval/async-confirm.ts
rename to packages/codingcode/src/approval/wait.ts
index e0228691..ca8796ca 100644
--- a/packages/codingcode/src/approval/async-confirm.ts
+++ b/packages/codingcode/src/approval/wait.ts
@@ -1,13 +1,13 @@
-import { Effect, Deferred } from 'effect';
+import { Layer, Effect, Deferred } from 'effect';
import type { ConfirmResult } from './confirmation.js';
+import { ApprovalWaitService } from './wait-port.js';
interface PendingEntry {
deferred: Deferred.Deferred;
sessionId: string;
}
-export class ApprovalWaitService extends Effect.Service()('ApprovalWait', {
- effect: Effect.gen(function* () {
+export const ApprovalWaitLayer = Layer.effect(ApprovalWaitService, Effect.gen(function* () {
const pendingConfirmations = new Map();
const approvalEmitters = new Map<
string,
@@ -79,5 +79,4 @@ export class ApprovalWaitService extends Effect.Service()('
hasEmitter: (sessionId: string): Effect.Effect =>
Effect.sync(() => approvalEmitters.has(sessionId)),
};
- }),
-}) {}
+}));
diff --git a/packages/codingcode/src/checkpoint/checkpoint-service.ts b/packages/codingcode/src/checkpoint/checkpoint.ts
similarity index 98%
rename from packages/codingcode/src/checkpoint/checkpoint-service.ts
rename to packages/codingcode/src/checkpoint/checkpoint.ts
index 165d4d88..7ba78fa3 100644
--- a/packages/codingcode/src/checkpoint/checkpoint-service.ts
+++ b/packages/codingcode/src/checkpoint/checkpoint.ts
@@ -1,4 +1,4 @@
-import { Effect } from 'effect';
+import { Layer, Effect } from 'effect';
import { createHash } from 'crypto';
import { resolve } from 'path';
import { ShadowGit } from './shadow-git.js';
@@ -8,11 +8,11 @@ import { shortSid, commitMsg, toGitPath, hashWorkspaceFile, ProjectCache } from
import { readRestoreEntry, writeRestoreEntry } from './undo-store.js';
import { getCompletedTurnsFor, getTurnRestorePlan, getRollbackToTurnPlan } from './turn-query.js';
import { emptyRollbackResult, executeRollback } from './rollback-engine.js';
+import { CheckpointService } from './port.js';
// ---- Effect Service ----
-export class CheckpointService extends Effect.Service()('Checkpoint', {
- effect: Effect.gen(function* () {
+export const CheckpointLayer = Layer.effect(CheckpointService, Effect.gen(function* () {
const shadowGitByProject = new ProjectCache(10);
const lockByProject = new ProjectCache(10);
@@ -330,5 +330,4 @@ export class CheckpointService extends Effect.Service()('Chec
return readRestoreEntry(sg.gitDir, sessionId);
}),
};
- }),
-}) {}
+} as any));
diff --git a/packages/codingcode/src/checkpoint/port.ts b/packages/codingcode/src/checkpoint/port.ts
new file mode 100644
index 00000000..6a7ef910
--- /dev/null
+++ b/packages/codingcode/src/checkpoint/port.ts
@@ -0,0 +1,17 @@
+import { Context } from 'effect';
+import type { Effect } from 'effect';
+
+export interface CheckpointShape {
+ snapshotBaseline(projectPath: string, sessionId: string, turnId: number): Effect.Effect;
+ snapshotFinal(projectPath: string, sessionId: string, turnId: number): Effect.Effect;
+ getCompletedTurns(projectPath: string, sessionId: string): Effect.Effect;
+ getCheckpoints(projectPath: string, sessionId: string): Effect.Effect>;
+ getCheckpointDiff(projectPath: string, sessionId: string, turnId?: number): Effect.Effect<{ turnId: number; files: Array<{ path: string; status: string; diff: string; insertions: number; deletions: number }> }>;
+ revertCheckpointFiles(projectPath: string, sessionId: string, turnId: number, files: string[]): Effect.Effect<{ restored: boolean; conflict: boolean; conflictFiles: string[]; restoredFiles: string[]; remainingRolledBack: string[] }>;
+ previewRollbackDiff(projectPath: string, sessionId: string, throughTurnId: number): Effect.Effect<{ throughTurnId: number; affectedTurns: number[]; diff: string }>;
+ rollbackCodeToTurn(projectPath: string, sessionId: string, throughTurnId: number): Effect.Effect<{ restored: boolean; conflict: boolean; conflictFiles: string[]; restoredFiles: string[]; remainingRolledBack: string[] }>;
+ undoLastCodeRollback(projectPath: string, sessionId: string, opts?: { force?: boolean; files?: string[] }): Effect.Effect<{ restored: boolean; conflict: boolean; conflictFiles: string[]; restoredFiles: string[]; remainingRolledBack: string[] }>;
+ getLatestRestoreEntry(projectPath: string, sessionId: string): Effect.Effect<{ id: string; turnId: number; selectedFiles: string[] } | null>;
+}
+
+export class CheckpointService extends Context.Tag('Checkpoint')() {}
diff --git a/packages/codingcode/src/cli.ts b/packages/codingcode/src/cli.ts
index b3b8e56a..f3fa0eff 100644
--- a/packages/codingcode/src/cli.ts
+++ b/packages/codingcode/src/cli.ts
@@ -1,13 +1,13 @@
import { Effect } from 'effect';
import { serve } from '@hono/node-server';
-import { LLMFactoryService } from './llm/factory.js';
+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 { findAvailablePort } from './server/port-discovery.js';
import { AgentError } from './core/error.js';
-import { SchedulerService } from './scheduler/service.js';
+import { SchedulerService } from './scheduler/port.js';
async function main() {
const installRoot = process.cwd();
diff --git a/packages/codingcode/src/client/http.ts b/packages/codingcode/src/client/http.ts
deleted file mode 100644
index 9f7629b5..00000000
--- a/packages/codingcode/src/client/http.ts
+++ /dev/null
@@ -1,273 +0,0 @@
-import type { AgentClient, StreamChunk } from './types.js';
-import type { McpServerConfig } from '../mcp/types.js';
-import type { UserHookConfig } from '../hooks/types.js';
-import type { PermissionMode } from '../approval/types.js';
-import { parseSseStream } from './sse.js';
-import { createHttpClients } from './http/index.js';
-
-export async function createHttpClient(serverUrl: string): Promise {
- let currentSessionId: string | undefined;
- const clients = createHttpClients(serverUrl);
-
- return {
- async *sendMessage(input: string, cwd?: string): AsyncGenerator {
- const response = await fetch(
- `${serverUrl}/api/sessions/${currentSessionId || '_'}/messages`,
- {
- method: 'POST',
- body: JSON.stringify({ input, cwd: cwd ?? '' }),
- headers: { 'Content-Type': 'application/json' },
- }
- );
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
-
- for await (const data of parseSseStream(response)) {
- switch (data.type) {
- case 'session_id':
- currentSessionId = data.sessionId as string;
- yield { type: 'session_id', sessionId: data.sessionId as string };
- break;
- case 'turn_id':
- yield { type: 'turn_id', turnId: data.turnId as number };
- break;
- case 'text':
- yield {
- type: 'text',
- text: data.text as string,
- messageId: data.messageId as number | undefined,
- };
- break;
- case 'message':
- yield {
- type: 'message',
- id: data.id as number,
- content: data.content as string,
- partial: false,
- };
- break;
- case 'approval_request':
- yield {
- type: 'approval_request',
- id: data.id as string,
- tool: data.tool as string,
- args: data.args as Record,
- };
- break;
- case 'tool_start':
- yield {
- type: 'tool_start',
- id: data.id as string,
- name: data.name as string,
- args: data.args as Record,
- };
- break;
- case 'tool_result':
- yield {
- type: 'tool_result',
- id: data.id as string,
- name: data.name as string,
- output: data.output as string,
- ok: data.ok as boolean,
- };
- break;
- case 'tool_denied':
- yield {
- type: 'tool_denied',
- id: data.id as string,
- name: data.name as string,
- reason: data.reason as string,
- };
- break;
- case 'todo_update':
- yield { type: 'todo_update', items: data.items as any };
- break;
- case 'usage':
- yield {
- type: 'usage',
- prompt: data.prompt as number,
- completion: data.completion as number,
- total: data.total as number,
- };
- break;
- case 'error':
- yield { type: 'error', message: data.message as string, code: data.code as string };
- return;
- case 'done':
- break;
- case 'complete':
- return;
- }
- }
- },
-
- async sendApprovalResponse(id: string, response: string) {
- if (!currentSessionId) return;
- await clients.agent.sendApprovalResponse({
- sessionId: currentSessionId,
- approvalId: id,
- response,
- });
- },
-
- async resumeSession(sid: string) {
- currentSessionId = sid;
- return clients.sessions.resumeSession({ sessionId: sid, cwd: '' });
- },
-
- async listSessions() {
- return clients.sessions.listSessions({ cwd: '' });
- },
-
- async listModels() {
- return clients.models.listModels();
- },
-
- async switchModel(id: string) {
- await clients.models.switchModel({ id });
- },
-
- getSessionId() {
- return currentSessionId ?? 'unknown';
- },
-
- async getCheckpoints() {
- return clients.agent.getCheckpoints();
- },
- async getCheckpointDiff(turnId?: number) {
- return clients.agent.getCheckpointDiff(turnId);
- },
- async revertCheckpointFiles(turnId: number, files: string[]) {
- return clients.agent.revertCheckpointFiles(turnId, files);
- },
- async previewRollbackDiff(throughTurnId: number) {
- return clients.agent.previewRollbackDiff(throughTurnId);
- },
- async rollbackCodeToTurn(throughTurnId: number) {
- return clients.agent.rollbackCodeToTurn(throughTurnId);
- },
- async rollbackContext(throughTurnId: number) {
- const res = await clients.agent.rollbackContext(throughTurnId);
- return {
- turns: (res as any).turns ?? [],
- rollbackState: (res as any).rollbackState ?? { active: false, currentThroughTurnId: null },
- };
- },
- async rollbackBothToTurn(throughTurnId: number) {
- const res = await clients.agent.rollbackBothToTurn(throughTurnId);
- return {
- turns: (res as any).turns ?? [],
- codeResult: (res as any).codeResult ?? {
- reverted: false,
- throughTurnId,
- affectedTurns: [],
- selectedFiles: [],
- restoreEntry: null,
- },
- rollbackState: (res as any).rollbackState ?? { active: false, currentThroughTurnId: null },
- };
- },
- async undoLastCodeRollback(force?: boolean, files?: string[]) {
- return clients.agent.undoLastCodeRollback(force, files);
- },
- async getRollbackState() {
- return clients.agent.getRollbackState();
- },
- async forkSession(atTurnId?: number) {
- return clients.agent.forkSession(atTurnId);
- },
-
- async compact() {
- if (!currentSessionId) return;
- await clients.agent.compact({ sessionId: currentSessionId, cwd: '' });
- },
-
- async getMemoryEnabled() {
- const data = await clients.settings.getMemoryConfig();
- return data.enabled;
- },
-
- async setMemoryEnabled(enabled: boolean) {
- await clients.settings.setMemoryEnabled(enabled);
- },
-
- async getMemoryConfig() {
- return clients.settings.getMemoryConfig();
- },
-
- async setTypeDisabled(name: string, disabled: boolean) {
- await clients.settings.setMemoryTypeDisabled(name, disabled);
- },
-
- async addExtraType(type: { name: string; description: string }) {
- await clients.settings.addMemoryExtraType(type);
- },
-
- async updateExtraType(name: string, type: { name: string; description: string }) {
- await clients.settings.updateMemoryExtraType(name, type);
- },
-
- async deleteExtraType(name: string) {
- await clients.settings.deleteMemoryExtraType(name);
- },
-
- async getMcpStatus({ cwd }: { cwd: string }) {
- return clients.settings.getMcpStatus({ cwd });
- },
-
- async setMcpDisabled(body: { name: string; disabled: boolean; cwd: string }) {
- await clients.settings.setMcpDisabled(body);
- },
-
- async resetMcpDisabled(body: { name: string; cwd: string }) {
- await clients.settings.resetMcpDisabled(body);
- },
-
- async listSkills() {
- return clients.settings.listSkills();
- },
-
- async createMcpServer(server: McpServerConfig, { cwd }: { cwd: string }) {
- await clients.settings.createMcpServer({ cwd, server });
- },
-
- async updateMcpServer(name: string, server: McpServerConfig, { cwd }: { cwd: string }) {
- await clients.settings.updateMcpServer({ cwd, name, server });
- },
-
- async deleteMcpServer(name: string, { cwd }: { cwd: string }) {
- await clients.settings.deleteMcpServer({ cwd, name });
- },
-
- async listHooks({ cwd }: { cwd: string }) {
- return clients.settings.listHooks({ cwd });
- },
-
- async setHookDisabled(body: { name: string; disabled: boolean; cwd: string }) {
- await clients.settings.setHookDisabled(body);
- },
-
- async resetHookDisabled(body: { name: string; cwd: string }) {
- await clients.settings.resetHookDisabled(body);
- },
-
- async createHook(hook: UserHookConfig, { cwd }: { cwd: string }) {
- await clients.settings.createHook({ cwd, hook });
- },
-
- async updateHook(name: string, hook: UserHookConfig, { cwd }: { cwd: string }) {
- await clients.settings.updateHook({ cwd, name, hook });
- },
-
- async deleteHook(name: string, { cwd }: { cwd: string }) {
- await clients.settings.deleteHook({ cwd, name });
- },
-
- async getPermissionMode(input: { sessionId: string; cwd: string }) {
- return clients.settings.getGlobalPermissionMode(input);
- },
-
- async setPermissionMode(input: { sessionId: string; cwd: string; mode: PermissionMode }) {
- await clients.settings.setGlobalPermissionMode(input);
- },
- };
-}
diff --git a/packages/codingcode/src/client/http/agent-runtime.ts b/packages/codingcode/src/client/http/agent-runtime.ts
index 3c8dfa0b..7ec5ca9e 100644
--- a/packages/codingcode/src/client/http/agent-runtime.ts
+++ b/packages/codingcode/src/client/http/agent-runtime.ts
@@ -129,6 +129,13 @@ export function createHttpAgentClient(
case 'todo_update':
yield { type: 'todo_update', items: data.items as any };
break;
+ case 'context_compressed':
+ yield {
+ type: 'context_compressed',
+ released: data.released as number,
+ promptEstimate: data.promptEstimate as number,
+ };
+ break;
case 'usage':
yield {
type: 'usage',
@@ -137,13 +144,6 @@ export function createHttpAgentClient(
total: data.total as number,
};
break;
- case 'reactive_compact':
- yield {
- type: 'reactive_compact',
- released: data.released as number,
- promptEstimate: data.promptEstimate as number,
- };
- break;
case 'error':
yield { type: 'error', message: data.message as string, code: data.code as string };
return;
diff --git a/packages/codingcode/src/client/http/models.ts b/packages/codingcode/src/client/http/models.ts
index 416f5df6..1f97d49e 100644
--- a/packages/codingcode/src/client/http/models.ts
+++ b/packages/codingcode/src/client/http/models.ts
@@ -1,4 +1,4 @@
-import type { SelectableModel } from '../../llm/factory.js';
+import type { SelectableModel } from '../../llm/port.js';
import type { createRequestHelpers } from './request.js';
export interface ModelClient {
diff --git a/packages/codingcode/src/client/http/sessions.ts b/packages/codingcode/src/client/http/sessions.ts
index 99551462..cbd9d7f8 100644
--- a/packages/codingcode/src/client/http/sessions.ts
+++ b/packages/codingcode/src/client/http/sessions.ts
@@ -7,7 +7,7 @@ import type {
RollbackState,
} from '../../checkpoint/types.js';
import type { SessionEvent, SessionIndex } from '../../session/types.js';
-import type { AgentProfileName } from '../../subagent/types.js';
+import type { AgentProfileName } from '../../agent/profile.js';
import type { createRequestHelpers } from './request.js';
export interface SessionClient {
diff --git a/packages/codingcode/src/client/http/settings.ts b/packages/codingcode/src/client/http/settings.ts
index 5768be68..18f512cb 100644
--- a/packages/codingcode/src/client/http/settings.ts
+++ b/packages/codingcode/src/client/http/settings.ts
@@ -7,14 +7,9 @@ export interface SettingsClient {
getMemoryEnabled(): Promise;
getMemoryConfig(): Promise<{
enabled: boolean;
- types: Array<{ name: string; description: string; isBuiltIn: boolean; disabled: boolean }>;
model: string;
}>;
setMemoryEnabled(enabled: boolean): Promise;
- setMemoryTypeDisabled(name: string, disabled: boolean): Promise;
- addMemoryExtraType(type: { name: string; description: string }): Promise;
- updateMemoryExtraType(name: string, type: { name: string; description: string }): Promise;
- deleteMemoryExtraType(name: string): Promise;
setMemoryModel(model: string): Promise<{ model: string }>;
getAgentConfig(): Promise<{ maxSteps: number; maxStopContinuations: number }>;
setCompactionModel(compactionModel: string): Promise<{ compactionModel: string }>;
@@ -74,22 +69,6 @@ export function createHttpSettingsClient(
await apiPost('/api/settings/memory/enabled', { enabled });
},
- async setMemoryTypeDisabled(name, disabled) {
- await apiPost('/api/settings/memory/type-disabled', { name, disabled });
- },
-
- async addMemoryExtraType(type) {
- await apiPost('/api/settings/memory/extra-type', type);
- },
-
- async updateMemoryExtraType(name, type) {
- await apiPut(`/api/settings/memory/extra-type/${encodeURIComponent(name)}`, type);
- },
-
- async deleteMemoryExtraType(name) {
- await apiDelete(`/api/settings/memory/extra-type/${encodeURIComponent(name)}`);
- },
-
async getMcpStatus({ cwd }) {
return apiGet(`/api/settings/mcp${qsCwd(cwd)}`);
},
diff --git a/packages/codingcode/src/client/types.ts b/packages/codingcode/src/client/types.ts
index 63756f83..153af13d 100644
--- a/packages/codingcode/src/client/types.ts
+++ b/packages/codingcode/src/client/types.ts
@@ -1,16 +1,3 @@
-import type { PermissionMode } from '../approval/types.js';
-import type { McpServerConfig, McpStatus } from '../mcp/types.js';
-import type { UserHookConfig } from '../hooks/types.js';
-import type { SessionEvent, SessionIndex } from '../session/types.js';
-import type { SelectableModel } from '../llm/factory.js';
-import type {
- CheckpointDiff,
- CodeRollbackResult,
- CodeRollbackUndoResult,
- RollbackPreviewDiff,
- RollbackState,
-} from '../checkpoint/types.js';
-
export type StreamChunk =
| { type: 'session_id'; sessionId: string }
| { type: 'turn_id'; turnId: number }
@@ -29,61 +16,5 @@ export type StreamChunk =
| { type: 'error'; message: string; code: string }
| { type: 'done' }
| { type: 'todo_update'; items: ReadonlyArray<{ step: string; status: string }> }
- | { type: 'usage'; prompt: number; completion: number; total: number }
- | { type: 'reactive_compact'; released: number; promptEstimate: number };
-
-export interface AgentClient {
- sendMessage(input: string, cwd?: string): AsyncGenerator;
- sendApprovalResponse(id: string, response: string): Promise;
- resumeSession(sid: string): Promise;
- listSessions(): Promise;
- listModels(): Promise<{ models: SelectableModel[]; activeId: string | null }>;
- switchModel(id: string): Promise;
- getSessionId(): string;
- getCheckpoints(): Promise>;
- getCheckpointDiff(turnId?: number): Promise;
- revertCheckpointFiles(turnId: number, files: string[]): Promise;
- previewRollbackDiff(throughTurnId: number): Promise;
- rollbackCodeToTurn(throughTurnId: number): Promise;
- rollbackContext(
- throughTurnId: number
- ): Promise<{ turns: SessionEvent[]; rollbackState: RollbackState }>;
- rollbackBothToTurn(throughTurnId: number): Promise<{
- turns: SessionEvent[];
- codeResult: CodeRollbackResult;
- rollbackState: RollbackState;
- }>;
- undoLastCodeRollback(force?: boolean, files?: string[]): Promise;
- getRollbackState(): Promise;
- forkSession(atTurnId?: number): Promise<{
- sessionId: string;
- turns: Array<{ id: string; items: object[]; status: string }>;
- }>;
- compact(): Promise;
- getMemoryEnabled(): Promise;
- setMemoryEnabled(enabled: boolean): Promise;
- getMemoryConfig(): Promise<{
- enabled: boolean;
- types: Array<{ name: string; description: string; isBuiltIn: boolean; disabled: boolean }>;
- model: string;
- }>;
- setTypeDisabled(name: string, disabled: boolean): Promise;
- addExtraType(type: { name: string; description: string }): Promise;
- updateExtraType(name: string, type: { name: string; description: string }): Promise;
- deleteExtraType(name: string): Promise;
- getMcpStatus(query: { cwd: string }): Promise;
- createMcpServer(server: McpServerConfig, query: { cwd: string }): Promise;
- updateMcpServer(name: string, server: McpServerConfig, query: { cwd: string }): Promise;
- deleteMcpServer(name: string, query: { cwd: string }): Promise;
- setMcpDisabled(body: { name: string; disabled: boolean; cwd: string }): Promise;
- resetMcpDisabled(body: { name: string; cwd: string }): Promise;
- listSkills(): Promise>;
- listHooks(query: { cwd: string }): Promise;
- setHookDisabled(body: { name: string; disabled: boolean; cwd: string }): Promise;
- resetHookDisabled(body: { name: string; cwd: string }): Promise;
- createHook(hook: UserHookConfig, query: { cwd: string }): Promise;
- updateHook(name: string, hook: UserHookConfig, query: { cwd: string }): Promise;
- deleteHook(name: string, query: { cwd: string }): Promise;
- getPermissionMode(input: { sessionId: string; cwd: string }): Promise;
- setPermissionMode(input: { sessionId: string; cwd: string; mode: PermissionMode }): Promise;
-}
+ | { type: 'context_compressed'; released: number; promptEstimate: number }
+ | { type: 'usage'; prompt: number; completion: number; total: number };
diff --git a/packages/codingcode/src/context/service.ts b/packages/codingcode/src/context/context.ts
similarity index 76%
rename from packages/codingcode/src/context/service.ts
rename to packages/codingcode/src/context/context.ts
index da9e6191..d3109637 100644
--- a/packages/codingcode/src/context/service.ts
+++ b/packages/codingcode/src/context/context.ts
@@ -1,13 +1,12 @@
-import { Effect } from 'effect';
+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 { SessionService } from '../session/store.js';
+import { SessionService } from '../session/port.js';
import { estimateTokens, estimateMessageTokens } from '../core/util.js';
-import { appendLine, readHistory } from '../session/file-ops.js';
import { resolveLLM } from '../llm/llm-resolver.js';
-import { LLMFactoryService } from '../llm/factory.js';
+import { LLMFactoryService } from '../llm/port.js';
import { COMPACTION_SYSTEM_PROMPT } from './compaction-prompt.js';
import type {
SessionEvent,
@@ -17,7 +16,8 @@ import type {
SummaryEvent,
} from '../session/types.js';
import type { LLMClient } from '../llm/client.js';
-import type { BuildResult, CompressResult } from './types.js';
+import { ContextService } from './port.js';
+import type { BuildResult, CompressResult } from './port.js';
const COMPACTABLE_TOOLS = new Set([
'read_file',
@@ -34,6 +34,7 @@ const MICRO_COMPACT_THRESHOLD = 0.25;
const MICRO_COMPACT_MIN_CHARS = 120;
const COMPACTION_THRESHOLD = 0.85;
const KEEP_RECENT_TURNS = 1;
+const MAX_AUTO_COMPACT_PASSES = 3;
// --- Internal: visibility computation for LLM context ---
@@ -179,21 +180,25 @@ export function buildContextMessages(
return filtered;
}
-/** Estimate prompt tokens for a session's jsonl file */
-export function estimatePromptTokens(jsonlPath: string): number {
- const events = readHistory(jsonlPath);
+/** Estimate prompt tokens for a filtered event stream */
+export function estimatePromptTokensFrom(events: SessionEvent[]): number {
const { visible, compactedTurnIds } = filterForContext(events);
return estimateTokens(buildContextMessages(visible, compactedTurnIds));
}
-export class ContextService extends Effect.Service()('Context', {
- effect: Effect.gen(function* () {
+interface PayloadState {
+ jsonlPath: string;
+ currentTurnId: number;
+ visible: SessionEvent[];
+ compactedTurnIds: Set;
+}
+
+export const ContextLayer = Layer.effect(ContextService, Effect.gen(function* () {
const session = yield* SessionService;
const factory = yield* LLMFactoryService;
- const assemblePayload = (transcriptPath: string, contextWindow: number): BuildResult => {
- const jsonlPath = transcriptPath;
- let events = session.readHistoryFile(jsonlPath);
+ const readState = (transcriptPath: string): PayloadState => {
+ const jsonlPath = transcriptPath;
let currentTurnId = 0;
const idxPath = transcriptPath.replace('.jsonl', '.index.json');
if (existsSync(idxPath)) {
@@ -202,43 +207,27 @@ export class ContextService extends Effect.Service()('Context',
currentTurnId = idx?.currentTurnId ?? 0;
} catch {}
}
+ const { visible, compactedTurnIds } = filterForContext(session.readEvents(jsonlPath));
+ return { jsonlPath, currentTurnId, visible, compactedTurnIds };
+ };
- let { visible, compactedTurnIds } = filterForContext(events);
-
- const preEstimate = estimateTokens(buildContextMessages(visible, compactedTurnIds));
-
- const didCompact = applyOldTurnCompaction(
- visible,
- currentTurnId,
- preEstimate,
- contextWindow,
- jsonlPath
- );
+ const estimateFor = (s: PayloadState): number =>
+ estimateTokens(buildContextMessages(s.visible, s.compactedTurnIds));
- if (didCompact) {
- events = session.readHistoryFile(jsonlPath);
- ({ visible, compactedTurnIds } = filterForContext(events));
+ // 微压缩:确定性截断旧 turn 的长工具输出;是否需要压缩由本模块内部判断
+ function runMicroCompact(s: PayloadState, contextWindow: number): PayloadState {
+ if (estimateFor(s) <= contextWindow * MICRO_COMPACT_THRESHOLD) return s;
+ if (applyOldTurnCompact(s.visible, s.currentTurnId, s.jsonlPath)) {
+ return readState(s.jsonlPath);
}
+ return s;
+ }
- const messages = buildContextMessages(visible, compactedTurnIds);
- return {
- messages,
- compactedEvents: visible,
- promptEstimate: estimateTokens(messages),
- currentTurnId,
- compactedTurnIds,
- };
- };
-
- function applyOldTurnCompaction(
+ function applyOldTurnCompact(
events: SessionEvent[],
currentTurnId: number,
- promptEstimate: number,
- contextWindow: number,
jsonlPath: string
): boolean {
- if (promptEstimate <= contextWindow * MICRO_COMPACT_THRESHOLD) return false;
-
const compactedTurnIds = new Set();
for (const ev of events) {
if (ev.type === 'compact') {
@@ -270,78 +259,16 @@ export class ContextService extends Effect.Service()('Context',
startTurnId,
endTurnId,
};
- appendLine(jsonlPath, compactEvent);
+ session.appendEvent(jsonlPath, compactEvent);
return true;
}
- const compactIfNeeded = async (
- transcriptPath: string,
- messages: Message[],
- modelMaxTokens: number,
- llm: LLMClient | null
- ): Promise => {
- const promptEstimate = estimateTokens(messages);
- const threshold = modelMaxTokens * COMPACTION_THRESHOLD;
- if (promptEstimate <= threshold) {
- return { didCompress: false, released: 0, promptEstimate };
- }
-
- const result = await compactWithLLM(transcriptPath, modelMaxTokens, llm, promptEstimate);
-
- return result;
- };
-
- const compactWithLLM = async (
- transcriptPath: string,
- modelMaxTokens: number,
- llm: LLMClient | null,
- usage?: number
- ): Promise => {
- let released = 0;
- let preEstimate = usage;
-
- const threshold = modelMaxTokens * COMPACTION_THRESHOLD;
- if (usage === undefined || usage - released > threshold) {
- const { compactedEvents, currentTurnId, compactedTurnIds, promptEstimate } =
- assemblePayload(transcriptPath, modelMaxTokens);
- preEstimate = promptEstimate;
- released += await tryCompaction(
- transcriptPath,
- llm,
- compactedEvents,
- currentTurnId,
- compactedTurnIds
- );
- }
-
- if (released <= 0) {
- return {
- didCompress: false,
- released: 0,
- promptEstimate: preEstimate ?? 0,
- };
- }
-
- const postPayload = assemblePayload(transcriptPath, modelMaxTokens);
- return {
- didCompress: true,
- released,
- promptEstimate: estimateTokens(postPayload.messages),
- messages: postPayload.messages,
- };
- };
-
- async function tryCompaction(
- transcriptPath: string,
- llm: LLMClient | null,
- compactedEvents: SessionEvent[],
- currentTurnId: number,
- compactedTurnIds: Set
- ): Promise {
- const endTurn = currentTurnId - KEEP_RECENT_TURNS - 1;
+ // LLM 摘要压缩(老 turn → summary 事件),失败返回 0 释放量
+ async function tryCompaction(s: PayloadState, llm: LLMClient | null): Promise {
+ const endTurn = s.currentTurnId - KEEP_RECENT_TURNS - 1;
if (endTurn < 1) return 0;
- const inRange = compactedEvents.filter((ev) => {
+ const inRange = s.visible.filter((ev) => {
if (ev.type === 'session_meta') return false;
if ('turnId' in ev && (ev as any).turnId >= 1 && (ev as any).turnId <= endTurn) return true;
return false;
@@ -351,7 +278,7 @@ export class ContextService extends Effect.Service()('Context',
const targetEvents = getIncrementalEvents(inRange);
if (targetEvents.length === 0) return 0;
- const msgs = buildContextMessages(targetEvents, compactedTurnIds);
+ const msgs = buildContextMessages(targetEvents, s.compactedTurnIds);
const totalTokens = estimateTokens(msgs);
let compactionLlm = await Effect.runPromise(
@@ -379,12 +306,63 @@ export class ContextService extends Effect.Service()('Context',
endTurnId,
summaryText: summary,
};
- appendLine(transcriptPath, summaryEvent);
+ session.appendEvent(s.jsonlPath, summaryEvent);
const summaryMsg: Message = { role: 'system', name: 'compacted_history', content: summary };
return Math.max(0, totalTokens - estimateMessageTokens(summaryMsg));
}
+ // 组装时内部决定是否需要 LLM 压缩;是否压缩、didCompress 均不对外暴露,
+ // 仅把释放量作为状态回报给组装结果
+ async function summarizeToFit(
+ s: PayloadState,
+ contextWindow: number,
+ llm: LLMClient | null
+ ): Promise<{ state: PayloadState; released: number }> {
+ let cur = s;
+ let releasedTotal = 0;
+ for (let i = 0; i < MAX_AUTO_COMPACT_PASSES; i++) {
+ if (estimateFor(cur) <= contextWindow * COMPACTION_THRESHOLD) break;
+ const released = await tryCompaction(cur, llm);
+ if (released <= 0) break;
+ releasedTotal += released;
+ cur = readState(cur.jsonlPath);
+ }
+ return { state: cur, released: releasedTotal };
+ }
+
+ const assemblePayload = async (
+ transcriptPath: string,
+ contextWindow: number,
+ llm: LLMClient | null
+ ): Promise => {
+ let s = readState(transcriptPath);
+ s = runMicroCompact(s, contextWindow);
+ const { state, released } = await summarizeToFit(s, contextWindow, llm);
+ return {
+ messages: buildContextMessages(state.visible, state.compactedTurnIds),
+ compressed: released > 0,
+ released,
+ promptEstimate: estimateFor(state),
+ };
+ };
+
+ const compactWithLLM = async (
+ transcriptPath: string,
+ modelMaxTokens: number,
+ llm: LLMClient | null,
+ usage?: number
+ ): Promise => {
+ let s = runMicroCompact(readState(transcriptPath), modelMaxTokens);
+ const preEstimate = usage ?? estimateFor(s);
+ const released = await tryCompaction(s, llm);
+ if (released <= 0) {
+ return { didCompress: false, released: 0, promptEstimate: preEstimate };
+ }
+ s = readState(transcriptPath);
+ return { didCompress: true, released, promptEstimate: estimateFor(s) };
+ };
+
function getIncrementalEvents(inRange: SessionEvent[]): SessionEvent[] {
const existingSummary = [...inRange]
.reverse()
@@ -439,8 +417,6 @@ export class ContextService extends Effect.Service()('Context',
return {
assemblePayload,
- compactIfNeeded,
compactWithLLM,
};
- }),
-}) {}
+}));
diff --git a/packages/codingcode/src/context/port.ts b/packages/codingcode/src/context/port.ts
new file mode 100644
index 00000000..b1efd27b
--- /dev/null
+++ b/packages/codingcode/src/context/port.ts
@@ -0,0 +1,26 @@
+import { Context } from 'effect';
+import type { Message } from '../core/types.js';
+import type { LLMClient } from '../llm/client.js';
+
+export interface BuildResult {
+ messages: Message[];
+ /** 本次组装是否发生过 LLM 摘要压缩(决策在 context 内部,仅上报结果) */
+ compressed: boolean;
+ /** 摘要压缩释放的 token 数(未压缩为 0) */
+ released: number;
+ /** 组装后上下文估算 token 数 */
+ promptEstimate: number;
+}
+
+export interface CompressResult {
+ didCompress: boolean;
+ released: number;
+ promptEstimate: number;
+}
+
+export interface ContextShape {
+ assemblePayload(transcriptPath: string, contextWindow: number, llm: LLMClient | null): Promise;
+ compactWithLLM(transcriptPath: string, modelMaxTokens: number, llm: LLMClient | null, usage?: number): Promise;
+}
+
+export class ContextService extends Context.Tag('Context')() {}
diff --git a/packages/codingcode/src/context/types.ts b/packages/codingcode/src/context/types.ts
deleted file mode 100644
index 66f0118a..00000000
--- a/packages/codingcode/src/context/types.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Message } from '../core/types.js';
-import type { SessionEvent } from '../session/types.js';
-
-export interface BuildResult {
- messages: Message[];
- compactedEvents: SessionEvent[];
- promptEstimate: number;
- currentTurnId: number;
- compactedTurnIds: Set;
-}
-
-export interface CompressResult {
- didCompress: boolean;
- released: number;
- promptEstimate: number;
- messages?: Message[];
-}
diff --git a/packages/codingcode/src/direct/agent-runtime.ts b/packages/codingcode/src/direct/agent-runtime.ts
index 578be493..928acbbc 100644
--- a/packages/codingcode/src/direct/agent-runtime.ts
+++ b/packages/codingcode/src/direct/agent-runtime.ts
@@ -1,13 +1,11 @@
import { Effect } from 'effect';
-import { sendMessage } from '../agent/agent.js';
-import { ApprovalWaitService } from '../approval/async-confirm.js';
+import { AgentService } from '../agent/port.js';
+import { ApprovalWaitService } from '../approval/wait-port.js';
import { parseApprovalResponse } from '../approval/response.js';
-import { ContextService } from '../context/service.js';
-import { HookService } from '../hooks/registry.js';
-import { SessionService } from '../session/store.js';
-import { CheckpointService } from '../checkpoint/checkpoint-service.js';
-import { readUIHistory } from '../session/ui-history.js';
-import { findUserMessageForTurn } from '../session/ui-history.js';
+import { ContextService } from '../context/port.js';
+import { HookService } from '../hooks/port.js';
+import { SessionService } from '../session/port.js';
+import { CheckpointService } from '../checkpoint/port.js';
import type { StreamChunk } from '../client/types.js';
import { agentEventToStreamChunk } from '../agent/stream-adapter.js';
import type { AppRuntime } from '../layer.js';
@@ -35,21 +33,21 @@ export interface AgentRuntimeClient {
cwd: string,
turnId: number,
files: string[]
- ): Promise;
+ ): Promise;
previewRollbackDiff(
cwd: string,
throughTurnId: number
- ): Promise;
+ ): Promise;
rollbackCodeToTurn(
cwd: string,
throughTurnId: number
- ): Promise;
+ ): Promise;
rollbackContext(
cwd: string,
throughTurnId: number
): Promise<{
turns: Array<{ id: string; items: object[]; status: string }>;
- rollbackState: import('../checkpoint/types.js').RollbackState;
+ rollbackState: any;
}>;
rollbackBothToTurn(
cwd: string,
@@ -57,14 +55,14 @@ export interface AgentRuntimeClient {
): Promise<{
turns: Array<{ id: string; items: object[]; status: string }>;
codeResult: import('../checkpoint/types.js').CodeRollbackResult;
- rollbackState: import('../checkpoint/types.js').RollbackState;
+ rollbackState: any;
}>;
undoLastCodeRollback(
cwd: string,
force?: boolean,
files?: string[]
- ): Promise;
- getRollbackState(cwd: string): Promise;
+ ): Promise;
+ getRollbackState(cwd: string): Promise;
forkSession(
cwd: string,
atTurnId?: number
@@ -79,16 +77,17 @@ export function createDirectAgentClient(llm: LLMClient, rt: AppRuntime): AgentRu
return {
async *sendMessage(input, { sessionId, cwd }) {
- const opts: Parameters[4] = {};
+ const runOpts: any = { cwd };
if (!sessionId) {
- opts.activeProfile = 'build';
- opts.permissionMode = 'default';
- opts.model = llm.modelInfo.model;
+ runOpts.activeProfile = 'build';
+ runOpts.permissionMode = 'default';
}
- const program = sendMessage(sessionId || undefined, input, cwd, llm, opts);
- const { stream: agentGen, sessionId: resolvedSessionId } = (await rt.runPromise(
- program
- )) as any;
+ const { stream: agentGen, sessionId: resolvedSessionId } = await rt.runPromise(
+ Effect.gen(function* () {
+ const agent = yield* AgentService;
+ return yield* agent.runTurn(input, { sessionId: sessionId || undefined, ...runOpts });
+ })
+ );
currentSessionId = resolvedSessionId;
yield { type: 'session_id', sessionId: resolvedSessionId };
@@ -258,8 +257,8 @@ export function createDirectAgentClient(llm: LLMClient, rt: AppRuntime): AgentRu
const session = yield* SessionService;
const state = yield* session.load(cwd, currentSessionId);
yield* session.rollbackToTurn(state, throughTurnId, 'user rollback');
- const turns = readUIHistory(currentSessionId, cwd);
- const rollbackState: import('../checkpoint/types.js').RollbackState = {
+ const turns = yield* session.readUITurns(currentSessionId, cwd);
+ const rollbackState: any = {
context: { active: true, currentThroughTurnId: throughTurnId },
code: {
canUndoLast: false,
@@ -273,7 +272,7 @@ export function createDirectAgentClient(llm: LLMClient, rt: AppRuntime): AgentRu
);
},
- async rollbackBothToTurn(cwd: string, throughTurnId: number) {
+ async rollbackBothToTurn(cwd: string, throughTurnId: number): Promise {
return rt.runPromise(
Effect.gen(function* () {
const session = yield* SessionService;
@@ -285,8 +284,8 @@ export function createDirectAgentClient(llm: LLMClient, rt: AppRuntime): AgentRu
throughTurnId
);
yield* session.rollbackToTurn(state, throughTurnId, 'user rollback');
- const turns = readUIHistory(currentSessionId, cwd);
- const rollbackState: import('../checkpoint/types.js').RollbackState = {
+ const turns = yield* session.readUITurns(currentSessionId, cwd);
+ const rollbackState: any = {
context: { active: true, currentThroughTurnId: throughTurnId },
code: {
canUndoLast: false,
@@ -336,7 +335,7 @@ export function createDirectAgentClient(llm: LLMClient, rt: AppRuntime): AgentRu
const session = yield* SessionService;
const state = yield* session.load(cwd, currentSessionId);
const newSessionId = yield* session.forkSession(state, atTurnId ?? 0);
- const turns = readUIHistory(newSessionId, cwd);
+ const turns = yield* session.readUITurns(newSessionId, cwd);
return { sessionId: newSessionId, turns };
})
);
diff --git a/packages/codingcode/src/direct/models.ts b/packages/codingcode/src/direct/models.ts
index 57942dc1..b3e30c1d 100644
--- a/packages/codingcode/src/direct/models.ts
+++ b/packages/codingcode/src/direct/models.ts
@@ -1,6 +1,6 @@
import { Effect } from 'effect';
-import { LLMFactoryService } from '../llm/factory.js';
-import type { SelectableModel } from '../llm/factory.js';
+import { LLMFactoryService } from '../llm/port.js';
+import type { SelectableModel } from '../llm/port.js';
import type { AppRuntime } from '../layer.js';
export interface ModelClient {
diff --git a/packages/codingcode/src/direct/sessions.ts b/packages/codingcode/src/direct/sessions.ts
index 6b3d191b..8d2d5f33 100644
--- a/packages/codingcode/src/direct/sessions.ts
+++ b/packages/codingcode/src/direct/sessions.ts
@@ -1,8 +1,7 @@
import { Effect } from 'effect';
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
import { join } from 'path';
-import { SessionService } from '../session/store.js';
-import { deleteSession } from '../session/file-ops.js';
+import { SessionService } from '../session/port.js';
import { encodeProjectPath, getProjectBaseDir } from '../core/path.js';
import type { PermissionMode } from '../approval/types.js';
import type {
@@ -13,7 +12,7 @@ import type {
RollbackState,
} from '../checkpoint/types.js';
import type { SessionEvent, SessionIndex } from '../session/types.js';
-import type { AgentProfileName } from '../subagent/types.js';
+import type { AgentProfileName } from '../agent/profile.js';
import type { AppRuntime } from '../layer.js';
export interface SessionClient {
@@ -140,7 +139,12 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient {
},
async deleteSession({ sessionId, cwd }) {
- deleteSession(sessionId, cwd);
+ await rt.runPromise(
+ Effect.gen(function* () {
+ const session = yield* SessionService;
+ yield* session.deleteSession(sessionId, cwd);
+ })
+ );
},
async getSessionProfile({ sessionId, cwd }) {
@@ -176,8 +180,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient {
const mode = await rt.runPromise(
Effect.gen(function* () {
const session = yield* SessionService;
- const state = yield* session.load(cwd, sessionId);
- return yield* session.getPermissionMode(state);
+ return yield* session.getPermissionMode(cwd, sessionId);
})
);
return mode as PermissionMode;
@@ -187,8 +190,7 @@ export function createDirectSessionClient(rt: AppRuntime): SessionClient {
return rt.runPromise(
Effect.gen(function* () {
const session = yield* SessionService;
- const state = yield* session.load(cwd, sessionId);
- yield* session.setPermissionMode(state, mode);
+ yield* session.setPermissionMode(cwd, sessionId, mode);
})
);
},
diff --git a/packages/codingcode/src/direct/settings.ts b/packages/codingcode/src/direct/settings.ts
index f9b282c3..77d393c0 100644
--- a/packages/codingcode/src/direct/settings.ts
+++ b/packages/codingcode/src/direct/settings.ts
@@ -1,7 +1,7 @@
import { Effect } from 'effect';
-import { McpService } from '../mcp/index.js';
+import { McpService } from '../mcp/port.js';
import type { McpServerConfig, McpStatus } from '../mcp/types.js';
-import { SkillService } from '../skills/service.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';
@@ -26,15 +26,8 @@ import {
resetProjectHookDisabledState,
} from '../hooks/config.js';
import { setHookRuntimeEnabled } from '../hooks/executor.js';
-import {
- getMemoryConfig,
- getAllTypesWithStatus,
- setMemoryTypeDisabled,
- addMemoryExtraType as _addMemoryExtraType,
- updateMemoryExtraType as _updateMemoryExtraType,
- deleteMemoryExtraType as _deleteMemoryExtraType,
-} from '../memory/config.js';
-import { MemoryService } from '../memory/index.js';
+import { getMemoryConfig } from '../memory/config.js';
+import { MemoryService } from '../memory/port.js';
import { AlreadyExistsError, NotFoundError } from '../core/error.js';
import {
loadConfig,
@@ -42,20 +35,15 @@ import {
updateContextCompactionModel,
} from '@codingcode/infra/config';
import type { AppRuntime } from '../layer.js';
-import { SessionService } from '../session/store.js';
+import { SessionService } from '../session/port.js';
export interface SettingsClient {
getMemoryEnabled(): Promise;
getMemoryConfig(): Promise<{
enabled: boolean;
- types: Array<{ name: string; description: string; isBuiltIn: boolean; disabled: boolean }>;
model: string;
}>;
setMemoryEnabled(enabled: boolean): Promise;
- setMemoryTypeDisabled(name: string, disabled: boolean): Promise;
- addMemoryExtraType(type: { name: string; description: string }): Promise;
- updateMemoryExtraType(name: string, type: { name: string; description: string }): Promise;
- deleteMemoryExtraType(name: string): Promise;
setMemoryModel(model: string): Promise<{ model: string }>;
getAgentConfig(): Promise<{ maxSteps: number; maxStopContinuations: number }>;
setCompactionModel(compactionModel: string): Promise<{ compactionModel: string }>;
@@ -238,7 +226,7 @@ export function createDirectSettingsClient(rt: AppRuntime): SettingsClient {
async getMemoryConfig() {
const cfg = getMemoryConfig();
- return { enabled: cfg.enabled, types: getAllTypesWithStatus(cfg), model: cfg.model };
+ return { enabled: cfg.enabled, model: cfg.model };
},
async setMemoryEnabled(enabled) {
@@ -265,26 +253,6 @@ export function createDirectSettingsClient(rt: AppRuntime): SettingsClient {
return { compactionModel };
},
- async setMemoryTypeDisabled(name, disabled) {
- setMemoryTypeDisabled(name, disabled);
- },
-
- async addMemoryExtraType(type) {
- _addMemoryExtraType({ name: type.name, description: type.description, enabled: true });
- },
-
- async updateMemoryExtraType(name, type) {
- _updateMemoryExtraType(name, {
- name: type.name,
- description: type.description,
- enabled: true,
- });
- },
-
- async deleteMemoryExtraType(name) {
- _deleteMemoryExtraType(name);
- },
-
async getMcpStatus({ cwd }) {
const projectCwd = isGlobalCwd(cwd) ? process.cwd() : cwd;
const runtime = await rt.runPromise(
@@ -425,8 +393,7 @@ export function createDirectSettingsClient(rt: AppRuntime): SettingsClient {
return rt.runPromise(
Effect.gen(function* () {
const session = yield* SessionService;
- const state = yield* session.load(input.cwd, input.sessionId);
- return yield* session.getPermissionMode(state);
+ return yield* session.getPermissionMode(input.cwd, input.sessionId);
})
);
},
@@ -439,8 +406,7 @@ export function createDirectSettingsClient(rt: AppRuntime): SettingsClient {
await rt.runPromise(
Effect.gen(function* () {
const session = yield* SessionService;
- const state = yield* session.load(input.cwd, input.sessionId);
- yield* session.setPermissionMode(state, input.mode);
+ yield* session.setPermissionMode(input.cwd, input.sessionId, input.mode);
})
);
},
diff --git a/packages/codingcode/src/hooks/registry.ts b/packages/codingcode/src/hooks/hooks.ts
similarity index 76%
rename from packages/codingcode/src/hooks/registry.ts
rename to packages/codingcode/src/hooks/hooks.ts
index 3e63e123..c4a1703a 100644
--- a/packages/codingcode/src/hooks/registry.ts
+++ b/packages/codingcode/src/hooks/hooks.ts
@@ -1,4 +1,4 @@
-import { Effect } from 'effect';
+import { Layer, Effect } from 'effect';
import { resolveHookConfigs, resolveHookDisabled } from './config.js';
import {
executeHookCommand,
@@ -6,6 +6,7 @@ import {
isHookRuntimeEnabled,
} from './executor.js';
import { createLogger } from '@codingcode/infra/logger';
+import { HookService } from './port.js';
import type {
HookPoint,
HookDecision,
@@ -19,8 +20,7 @@ import type {
const logger = createLogger();
-export class HookService extends Effect.Service()('HookService', {
- effect: Effect.gen(function* () {
+export const HookLayer = Layer.effect(HookService, Effect.gen(function* () {
let entryCounter = 0;
const globalHooks = new Map();
const hooksByProject = new Map>();
@@ -223,78 +223,10 @@ export class HookService extends Effect.Service()('HookService', {
hooksByProject.set(projectPath, projectMap);
}),
- attachSessionHooks: (
- sessionId: string,
- hooks: {
- name: string;
- point: HookPoint;
- type: 'observer' | 'decision';
- command: string;
- args?: string[];
- priority?: number;
- }[]
- ): Effect.Effect =>
- Effect.sync(() => {
- const sessionMap = new Map();
- for (const hc of hooks) {
- const observerHandler: ObserverHandler = (payload) =>
- Effect.tryPromise({
- try: () =>
- executeHookCommand({ command: hc.command, args: hc.args, env: {} }, payload),
- catch: (e) => logger.error(`session hook ${hc.name} error:`, e),
- }).pipe(Effect.ignore);
- const decisionHandler: DecisionHandler = (payload) =>
- Effect.tryPromise({
- try: () =>
- executeDecisionHookCommand(
- { command: hc.command, args: hc.args, env: {} },
- payload
- ),
- catch: (e) => {
- logger.error(`session decision hook ${hc.name} error:`, e);
- return null;
- },
- }) as unknown as Promise;
- const entry: HandlerEntry = {
- id: `session-${hc.name}-${++entryCounter}`,
- handler: hc.type === 'observer' ? observerHandler : decisionHandler,
- priority: hc.priority ?? 0,
- source: 'user',
- type: hc.type,
- };
- const set = sessionMap.get(hc.point) ?? [];
- set.push(entry);
- sessionMap.set(hc.point, set);
- }
- hooksBySession.set(sessionId, sessionMap);
- }),
-
- disableHook: (projectPath: string, name: string): Effect.Effect =>
- Effect.sync(() => {
- let set = disabledHooksByProject.get(projectPath);
- if (!set) {
- set = new Set();
- disabledHooksByProject.set(projectPath, set);
- }
- set.add(name);
- }),
-
- enableHook: (projectPath: string, name: string): Effect.Effect =>
- Effect.sync(() => {
- disabledHooksByProject.get(projectPath)?.delete(name);
- }),
-
disposeSession: (sessionId: string): Effect.Effect =>
Effect.sync(() => {
hooksBySession.delete(sessionId);
disabledHooksBySession.delete(sessionId);
}),
-
- disposeProject: (projectPath: string): Effect.Effect =>
- Effect.sync(() => {
- hooksByProject.delete(projectPath);
- disabledHooksByProject.delete(projectPath);
- }),
};
- }),
-}) {}
+}));
diff --git a/packages/codingcode/src/hooks/port.ts b/packages/codingcode/src/hooks/port.ts
new file mode 100644
index 00000000..f3efa197
--- /dev/null
+++ b/packages/codingcode/src/hooks/port.ts
@@ -0,0 +1,14 @@
+import { Context } from 'effect';
+import type { Effect } from 'effect';
+import type { HookPoint, HookDecision, ObserverHandler, DecisionHandler } from './types.js';
+
+export interface HookShape {
+ register(point: HookPoint, handler: ObserverHandler, opts?: { source?: 'system' | 'user' }): Effect.Effect<() => void>;
+ registerDecision(point: HookPoint, handler: DecisionHandler, opts?: { priority?: number; source?: 'system' | 'user' }): Effect.Effect<() => void>;
+ emit(point: HookPoint, payload: Record): Effect.Effect;
+ emitDecision(point: HookPoint, payload: Record): Effect.Effect;
+ reloadUserHooks(projectPath: string): Effect.Effect;
+ disposeSession(sessionId: string): Effect.Effect;
+}
+
+export class HookService extends Context.Tag('HookService')() {}
diff --git a/packages/codingcode/src/layer.ts b/packages/codingcode/src/layer.ts
index 818fa603..dad7689b 100644
--- a/packages/codingcode/src/layer.ts
+++ b/packages/codingcode/src/layer.ts
@@ -1,115 +1,166 @@
import { Context, Layer, Effect, ManagedRuntime } from 'effect';
-import { AgentService } from './agent/agent.js';
-import { SessionService } from './session/store.js';
-import { HookService } from './hooks/registry.js';
-import { McpService } from './mcp/index.js';
-import { SkillService } from './skills/service.js';
-import { ApprovalService } from './approval/index.js';
-import { ApprovalWaitService } from './approval/async-confirm.js';
-import { ToolExecutorService } from './tools/executor.js';
-import { CheckpointService } from './checkpoint/checkpoint-service.js';
-import { ProjectRuntimeService } from './runtime/project-runtime.js';
-import { LLMFactoryService } from './llm/factory.js';
+import { HookLayer } from './hooks/hooks.js';
+import { RulesLayer } from './rules/rules.js';
+import { SkillLayer } from './skills/skills.js';
+import { LlmLayer } from './llm/llm.js';
+import { McpLayer } from './mcp/mcp.js';
+import { CheckpointLayer } from './checkpoint/checkpoint.js';
+import { ApprovalLayer } from './approval/approval.js';
+import { ApprovalWaitLayer } from './approval/wait.js';
+import { TodoLayer } from './todo/todo.js';
+import { SessionLayer } from './session/session.js';
+import { ToolExecutorLayer } from './tools/tools.js';
+import { ContextLayer } from './context/context.js';
+import { MemoryLayer } from './memory/memory.js';
+import { AgentLayer } from './agent/agent.js';
+import { SubagentRunnerLayer } from './subagent/subagent.js';
+import { SchedulerLayer } from './scheduler/scheduler.js';
import { WorkspaceService } from './core/workspace.js';
-import { TodoService } from './agent/todo.js';
-import { SubagentRunnerService } from './subagent/runner-service.js';
-import { RulesService } from './rules/index.js';
-import { MemoryService } from './memory/index.js';
-import { ContextService } from './context/service.js';
-import { SchedulerService } from './scheduler/service.js';
import { planProfileGateHook } from './agent/profile.js';
-export const WorkspaceLayer = WorkspaceService.Default;
-export const TodoLayer = TodoService.Default;
-export const RulesLayer = RulesService.Default;
-export const SessionLayer = SessionService.Default;
-export const LLMFactoryLayer = LLMFactoryService.Default.pipe(Layer.provide(WorkspaceLayer));
-export const MemoryLayer = MemoryService.Default.pipe(Layer.provide(LLMFactoryLayer));
-export const ContextLayer = ContextService.Default.pipe(
- Layer.provide(Layer.mergeAll(SessionLayer, LLMFactoryLayer))
-);
-export const HookLayer = HookService.Default;
-export const SkillLayer = SkillService.Default;
-export const CheckpointLayer = CheckpointService.Default;
-export const ApprovalWaitLayer = ApprovalWaitService.Default;
-export const McpLayer = McpService.Default;
-export const SchedulerLayer = SchedulerService.Default;
-export const ProjectRuntimeLayer = ProjectRuntimeService.Default.pipe(
- Layer.provide(Layer.mergeAll(HookLayer, McpLayer, RulesLayer, SessionLayer))
+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), recordAssistant: s.recordAssistant.bind(s),
+ recordToolResult: s.recordToolResult.bind(s), incrementTurn: s.incrementTurn.bind(s),
+ getTranscriptPath: s.getTranscriptPath.bind(s),
+ getActiveProfile: s.getActiveProfile.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), fork: a.fork.bind(a) };
+}));
+
+const AgentSkillAdapter = Layer.effect(SkillPort, Effect.gen(function* () {
+ const s = yield* SkillService;
+ return { extractSkill: s.extractSkill.bind(s), evictProject: s.evictProject.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 {
+ 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,
);
-export const ApprovalLayer = ApprovalService.Default.pipe(
- Layer.provide(Layer.mergeAll(HookLayer, ApprovalWaitLayer))
+
+// base layers
+const InfraLayer = Layer.mergeAll(
+ WorkspaceService.Default, HookLayer, RulesLayer, SkillLayer, McpLayer, ApprovalWaitLayer, TodoLayer,
);
-export const SystemHookLayer = HookLayer.pipe(
+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, ApprovalLayer)));
+const ContextWithDeps = ContextLayer.pipe(Layer.provide(Layer.mergeAll(SessionLayer, LlmWithDeps)));
+const MemoryWithDeps = MemoryLayer.pipe(Layer.provide(LlmWithDeps));
+
+// system hook registration
+const SystemHookLayer = HookLayer.pipe(
Layer.tap((context) =>
Effect.gen(function* () {
const hooks = Context.get(context, HookService);
yield* hooks.registerDecision('tool.approval.pre', planProfileGateHook, {
- priority: -1000,
- source: 'system',
+ priority: -1000, source: 'system',
});
})
)
);
-/** ToolExecutor depends on HookLayer + ApprovalLayer. */
-const ExecutorDeps = Layer.mergeAll(HookLayer, ApprovalLayer);
-const ExecutorLayer = ToolExecutorService.Default.pipe(Layer.provide(ExecutorDeps));
-
-/** Agent depends on ToolExecutor + HookLayer + ApprovalLayer + ApprovalWaitLayer + Session + Checkpoint + ProjectRuntime + Skill + LLMFactory + Todo + Rules + Context + Memory. */
-const AgentDeps = Layer.mergeAll(
- ExecutorLayer,
- ApprovalLayer,
- ApprovalWaitLayer,
- SessionLayer,
- CheckpointLayer,
- McpLayer,
- SkillLayer,
- LLMFactoryLayer,
- HookLayer,
- ProjectRuntimeLayer,
- TodoLayer,
- RulesLayer,
- ContextLayer,
- MemoryLayer
+// agent with deps
+const AgentWithDeps = AgentLayer.pipe(
+ Layer.provide(Layer.mergeAll(AgentDepsAdapter, InfraLayer, SessionLayer, ToolExecutorWithDeps, ApprovalWithDeps, ContextWithDeps, MemoryWithDeps, CheckpointLayer))
);
-const AgentWithDeps = AgentService.Default.pipe(Layer.provide(AgentDeps));
-
-/** SubagentRunnerService delegates to AgentService.runStream. */
-const SubagentRunnerLayer = Layer.effect(
- SubagentRunnerService,
- Effect.gen(function* () {
- const agent = yield* AgentService;
- return SubagentRunnerService.make({ runStream: agent.runStream });
- })
-).pipe(Layer.provide(AgentWithDeps));
-
-/** Final application layer — all services merged. */
+
+// subagent runner (depends on agent)
+const SubagentWithDeps = SubagentRunnerLayer.pipe(Layer.provide(AgentWithDeps));
+
export const AppLayer = Layer.mergeAll(
- AgentWithDeps,
- SubagentRunnerLayer,
- ExecutorLayer,
+ InfraLayer,
+ LlmWithDeps,
+ ApprovalWithDeps,
SessionLayer,
- HookLayer,
- McpLayer,
- SkillLayer,
- ApprovalLayer,
- ApprovalWaitLayer,
+ ToolExecutorWithDeps,
+ ContextWithDeps,
+ MemoryWithDeps,
CheckpointLayer,
- ProjectRuntimeLayer,
- LLMFactoryLayer,
- WorkspaceLayer,
- TodoLayer,
- RulesLayer,
- MemoryLayer,
- ContextLayer,
+ AgentWithDeps,
+ SubagentWithDeps,
SchedulerLayer,
- SystemHookLayer
+ SystemHookLayer,
);
-/** Create the application ManagedRuntime from AppLayer. */
export const createAppRuntime = () => ManagedRuntime.make(AppLayer as any);
-
-/** Concrete runtime type for the application. */
export type AppRuntime = ManagedRuntime.ManagedRuntime;
diff --git a/packages/codingcode/src/llm/llm-resolver.ts b/packages/codingcode/src/llm/llm-resolver.ts
index 21a09d0a..35481ff7 100644
--- a/packages/codingcode/src/llm/llm-resolver.ts
+++ b/packages/codingcode/src/llm/llm-resolver.ts
@@ -1,6 +1,6 @@
import { Effect } from 'effect';
import { AgentError } from '../core/error.js';
-import { LLMFactoryService } from './factory.js';
+import { LLMFactoryService } from './port.js';
import type { LLMClient } from './client.js';
export function resolveLLM(
diff --git a/packages/codingcode/src/llm/factory.ts b/packages/codingcode/src/llm/llm.ts
similarity index 98%
rename from packages/codingcode/src/llm/factory.ts
rename to packages/codingcode/src/llm/llm.ts
index 2c94c899..abe8431e 100644
--- a/packages/codingcode/src/llm/factory.ts
+++ b/packages/codingcode/src/llm/llm.ts
@@ -1,12 +1,13 @@
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
-import { Effect } from 'effect';
+import { Layer, Effect } from 'effect';
import { AgentError } from '../core/error.js';
import { WorkspaceService } from '../core/workspace.js';
import type { LLMClient } from './client.js';
import { OpenAIProvider } from './providers/openai.js';
import { DeepSeekProvider } from './providers/deepseek.js';
import { updateActiveModel } from '@codingcode/infra/config';
+import { LLMFactoryService } from './port.js';
export interface ModelDescriptor {
id: string;
@@ -57,8 +58,7 @@ function flattenModels(cat: ProviderCatalog): SelectableModel[] {
return result;
}
-export class LLMFactoryService extends Effect.Service()('LLMFactory', {
- effect: Effect.gen(function* () {
+export const LlmLayer = Layer.effect(LLMFactoryService, Effect.gen(function* () {
const workspace = yield* WorkspaceService;
let catalog: ProviderCatalog | null = null;
let currentEntry: SelectableModel | null = null;
@@ -276,5 +276,4 @@ export class LLMFactoryService extends Effect.Service()('LLMF
return currentClient;
}),
};
- }),
-}) {}
+}));
diff --git a/packages/codingcode/src/llm/port.ts b/packages/codingcode/src/llm/port.ts
new file mode 100644
index 00000000..e17ce6ca
--- /dev/null
+++ b/packages/codingcode/src/llm/port.ts
@@ -0,0 +1,19 @@
+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 }
+
+export interface LLMFactoryShape {
+ listModels(): Effect.Effect;
+ findModel(target: string): Effect.Effect;
+ getActiveEntry(): Effect.Effect;
+ switchModel(id: string): Effect.Effect;
+ createClient(entry: SelectableModel): Effect.Effect;
+ getLLMClient(): Effect.Effect;
+}
+
+export class LLMFactoryService extends Context.Tag('LLMFactory')() {}
diff --git a/packages/codingcode/src/llm/providers/deepseek.ts b/packages/codingcode/src/llm/providers/deepseek.ts
index 97ec05c7..c16692c1 100644
--- a/packages/codingcode/src/llm/providers/deepseek.ts
+++ b/packages/codingcode/src/llm/providers/deepseek.ts
@@ -5,7 +5,7 @@ import { AgentError } from '../../core/error.js';
import { mapLlmError } from '../errors.js';
import type { LLMClient } from '../client.js';
import type { LLMRequest, LLMResponse } from '../types.js';
-import type { SelectableModel } from '../factory.js';
+import type { SelectableModel } from '../port.js';
import { convertMessages, convertTools, parseResponseMessages } 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 b3479a1c..0cdec077 100644
--- a/packages/codingcode/src/llm/providers/openai.ts
+++ b/packages/codingcode/src/llm/providers/openai.ts
@@ -5,7 +5,7 @@ import { AgentError } from '../../core/error.js';
import { mapLlmError } from '../errors.js';
import type { LLMClient } from '../client.js';
import type { LLMRequest, LLMResponse } from '../types.js';
-import type { SelectableModel } from '../factory.js';
+import type { SelectableModel } from '../port.js';
import { convertMessages, convertTools, parseResponseMessages } from './shared.js';
export class OpenAIProvider implements LLMClient {
diff --git a/packages/codingcode/src/mcp/index.ts b/packages/codingcode/src/mcp/mcp.ts
similarity index 98%
rename from packages/codingcode/src/mcp/index.ts
rename to packages/codingcode/src/mcp/mcp.ts
index 8ea59e54..4a74bffd 100644
--- a/packages/codingcode/src/mcp/index.ts
+++ b/packages/codingcode/src/mcp/mcp.ts
@@ -1,7 +1,8 @@
-import { Effect } from 'effect';
+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 { createLogger } from '@codingcode/infra/logger';
@@ -30,8 +31,7 @@ interface LeaseEntry {
type ProjectPath = string;
type ServerName = string;
-export class McpService extends Effect.Service()('Mcp', {
- effect: Effect.sync(() => {
+export const McpLayer = Layer.effect(McpService, Effect.sync(() => {
const clientsByProject = new Map>();
const leasesBySession = new Map>();
const disabledMcpByProject = new Map>();
@@ -318,8 +318,8 @@ export class McpService extends Effect.Service()('Mcp', {
configCache.delete(projectPath);
}),
};
- }),
-}) {}
+ }
+));
function namespacedName(serverName: string, toolName: string): string {
return `${serverName}:${toolName}`;
diff --git a/packages/codingcode/src/mcp/port.ts b/packages/codingcode/src/mcp/port.ts
new file mode 100644
index 00000000..6caac1b7
--- /dev/null
+++ b/packages/codingcode/src/mcp/port.ts
@@ -0,0 +1,19 @@
+import { Context } from 'effect';
+import type { Effect } from 'effect';
+import type { ToolDefinition } from '../tools/types.js';
+import type { McpStatus } from './types.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[];
+ status(projectPath: string): Effect.Effect;
+ disable(projectPath: string, name: string): Effect.Effect;
+ enable(projectPath: string, name: string): Effect.Effect;
+ disposeSession(sessionId: string): Effect.Effect;
+ disposeProject(projectPath: string): Effect.Effect;
+}
+
+export class McpService extends Context.Tag('Mcp')() {}
diff --git a/packages/codingcode/src/memory/config.ts b/packages/codingcode/src/memory/config.ts
index 6cf53de8..a11822c9 100644
--- a/packages/codingcode/src/memory/config.ts
+++ b/packages/codingcode/src/memory/config.ts
@@ -1,76 +1,5 @@
-import {
- DEFAULT_MEMORY_TYPES,
- loadConfig,
- type MemoryConfig,
- type MemoryTypeConfig,
- updateMemoryEnabled,
- updateMemoryDisabledTypes,
- updateMemoryExtraTypes,
-} from '@codingcode/infra/config';
-import type { MemoryTypeEntry } from './types.js';
+import { loadConfig, type MemoryConfig } from '@codingcode/infra/config';
export function getMemoryConfig(): MemoryConfig {
return loadConfig().memory;
}
-
-export function getEffectiveTypes(cfg: MemoryConfig): MemoryTypeConfig[] {
- return [...DEFAULT_MEMORY_TYPES, ...cfg.extraTypes].filter(
- (t) => t.enabled && !cfg.disabledTypes.includes(t.name)
- );
-}
-
-export function getAllTypesWithStatus(cfg?: MemoryConfig): MemoryTypeEntry[] {
- const config = cfg ?? getMemoryConfig();
- const builtIn: MemoryTypeEntry[] = DEFAULT_MEMORY_TYPES.map((t) => ({
- name: t.name,
- description: t.description,
- isBuiltIn: true,
- disabled: config.disabledTypes.includes(t.name),
- }));
- const custom: MemoryTypeEntry[] = config.extraTypes.map((t) => ({
- name: t.name,
- description: t.description,
- isBuiltIn: false,
- disabled: config.disabledTypes.includes(t.name),
- }));
- return [...builtIn, ...custom];
-}
-
-export function setMemoryTypeDisabled(name: string, disabled: boolean, cfg?: MemoryConfig): void {
- const config = cfg ?? getMemoryConfig();
- const disabledTypes = disabled
- ? [...new Set([...config.disabledTypes, name])]
- : config.disabledTypes.filter((n) => n !== name);
- updateMemoryDisabledTypes(disabledTypes);
-}
-
-export function addMemoryExtraType(type: MemoryTypeConfig, cfg?: MemoryConfig): void {
- const config = cfg ?? getMemoryConfig();
- if (config.extraTypes.some((t) => t.name === type.name)) {
- throw new Error(`Memory type '${type.name}' already exists`);
- }
- const updated = [...config.extraTypes, { ...type, enabled: true }];
- updateMemoryExtraTypes(updated);
-}
-
-export function updateMemoryExtraType(
- name: string,
- type: MemoryTypeConfig,
- cfg?: MemoryConfig
-): void {
- const config = cfg ?? getMemoryConfig();
- const idx = config.extraTypes.findIndex((t) => t.name === name);
- if (idx === -1) throw new Error(`Memory type '${name}' not found`);
- const updated = [...config.extraTypes];
- if (type.name !== name && config.extraTypes.some((t) => t.name === type.name)) {
- throw new Error(`Memory type '${type.name}' already exists`);
- }
- updated[idx] = { ...type, enabled: true };
- updateMemoryExtraTypes(updated);
-}
-
-export function deleteMemoryExtraType(name: string, cfg?: MemoryConfig): void {
- const config = cfg ?? getMemoryConfig();
- const updated = config.extraTypes.filter((t) => t.name !== name);
- updateMemoryExtraTypes(updated);
-}
diff --git a/packages/codingcode/src/memory/extractor.ts b/packages/codingcode/src/memory/extractor.ts
index 195e3a44..db82d360 100644
--- a/packages/codingcode/src/memory/extractor.ts
+++ b/packages/codingcode/src/memory/extractor.ts
@@ -1,67 +1,32 @@
import type { LLMClient } from '../llm/client.js';
-import type { MemoryTypeConfig } from '@codingcode/infra/config';
-import type { StructuredTranscript } from './types.js';
export async function extractMemory(opts: {
- currentAuto: string;
- transcript: StructuredTranscript;
- types: MemoryTypeConfig[];
+ currentMemory: string;
+ transcript: string;
llm: LLMClient;
}): Promise {
- const { currentAuto, transcript, types, llm } = opts;
+ const { currentMemory, transcript, llm } = opts;
- const typeDescriptions = types.map((t) => `- **${t.name}**: ${t.description}`).join('\n');
-
- const typeGuidelineMap: Record = {
- user: '- **user**: 从 [user] 标签提取用户角色、技能栈、对 Agent 的工作偏好及纠正',
- project: '- **project**: 从 [user] 和 [assistant] 标签提取架构决策、技术选型、部署信息',
- reference: '- **reference**: 从 [user] 和 [tool:*] 标签提取外部资源、文档、Dashboard 链接',
- };
-
- const typeGuidance = types
- .map((t) => typeGuidelineMap[t.name])
- .filter(Boolean)
- .join('\n');
-
- const formatExamples = types
- .map((t) => {
- switch (t.name) {
- case 'user':
- return '### user\n- 要点一\n- 要点二';
- case 'project':
- return '### project\n- 架构决策';
- case 'reference':
- return '### reference\n- [标题](URL)';
- default:
- return '';
- }
- })
- .filter(Boolean)
- .join('\n\n');
-
- const systemPrompt = `你是记忆提取器。从对话记录中提取值得长期记忆的内容,输出 ... 块。
-如果没有值得记忆的内容,输出 。
+ const systemPrompt = `你是记忆整理器。基于"已有记忆"和"会话记录",输出整份最新版长期记忆,放在 ... 块中,不要输出其它内容。
规则:
-- 新信息与已有记忆矛盾时,用新信息替换旧条目
-- 同一会话内前后不一致,以最新出现的为准
-- 只输出有内容的 ### 小节,忽略临时调试、一次性任务、报错堆栈
-
-记忆类型及信息来源:
-${typeGuidance}
+- 只保留值得跨会话记住的信息:用户角色、偏好与对 Agent 的纠正,项目架构决策、技术选型与部署信息,外部资源与链接等。
+- 忽略临时内容:一次性任务、调试过程、报错堆栈、闲聊。
+- 更新哪些内容由你决定:在已有记忆基础上自行增、删、改,输出必须是一份完整、自洽的最新记忆,而不是只输出变动部分。
+- 旧记忆与对话新信息矛盾时以最新为准;同一会话前后不一致时以最后出现为准。
+- 不要编造对话中未出现的信息。
+- 若没有值得记住的新信息且已有记忆为空,输出 。
格式:
-${formatExamples}`;
+- 纯 Markdown,用 "### 主题" 小节组织,小节下用 "- " 列要点。
+- 条目需具体、自包含,避免"上面提到的那个"这类指代。
+- 内不要带任何解释性文字。`;
const userMessage = `已有记忆:
-${currentAuto}
+${currentMemory || '(空)'}
-会话记录:
-[user] ${transcript.userOnly}
----
-[user+assistant] ${transcript.userAndAssistant}
----
-[user+tool] ${transcript.userAndTools}`;
+会话记录(按 [user]/[assistant]/[tool:名称] 标注):
+${transcript || '(空)'}`;
try {
const result = llm.completeStream({
diff --git a/packages/codingcode/src/memory/index.ts b/packages/codingcode/src/memory/memory.ts
similarity index 55%
rename from packages/codingcode/src/memory/index.ts
rename to packages/codingcode/src/memory/memory.ts
index 7a4cbee0..153f4927 100644
--- a/packages/codingcode/src/memory/index.ts
+++ b/packages/codingcode/src/memory/memory.ts
@@ -1,29 +1,23 @@
-import { Effect } from 'effect';
+import { Layer, Effect } from 'effect';
import type { LLMClient } from '../llm/client.js';
-import { sessionJsonlPathFromCwd } from '../core/path.js';
+import { readTranscript } from '../session/index.js';
import type { SessionEvent } from '../session/types.js';
import {
readMemoryFile,
resolveMemoryPath,
- extractAutoBlock,
- replaceAutoBlock,
- mergeAutoBlocks,
enforceMaxBytes,
writeMemoryFileAtomic,
- stripMarkersForPrompt,
} from './storage.js';
import { resolveLLM } from '../llm/llm-resolver.js';
-import { LLMFactoryService } from '../llm/factory.js';
-import { getMemoryConfig, getEffectiveTypes } from './config.js';
+import { LLMFactoryService } from '../llm/port.js';
+import { getMemoryConfig } from './config.js';
import { updateMemoryEnabled } from '@codingcode/infra/config';
import { extractMemory } from './extractor.js';
-import type { StructuredTranscript } from './types.js';
+import { MemoryService } from './port.js';
const MAX_BYTES = 16384;
-const PROMPT_MAX_BYTES = 8192;
-export class MemoryService extends Effect.Service()('Memory', {
- effect: Effect.gen(function* () {
+export const MemoryLayer = Layer.effect(MemoryService, Effect.gen(function* () {
const factory = yield* LLMFactoryService;
let _runtimeEnabled: boolean | null = null;
@@ -36,22 +30,6 @@ export class MemoryService extends Effect.Service()('Memory', {
updateMemoryEnabled(v);
}
- function loadMemoryForPrompt(cwd: string): string {
- if (!getMemoryEnabled()) return '';
- const cfg = getMemoryConfig();
-
- const projectPath = resolveMemoryPath(cwd);
- const projectContent = readMemoryFile(projectPath);
- const projectAuto = extractAutoBlock(projectContent);
-
- if (!projectAuto) return '';
-
- const stripped = stripMarkersForPrompt(projectAuto);
- const truncated = truncateForPrompt(stripped, cfg.promptMaxBytes);
-
- return truncated ? `## Long-term Memory\n\n${truncated}` : '';
- }
-
function truncateForPrompt(content: string, maxBytes: number): string {
const contentBytes = Buffer.byteLength(content, 'utf-8');
if (contentBytes <= maxBytes) {
@@ -71,20 +49,27 @@ export class MemoryService extends Effect.Service()('Memory', {
return result;
}
- function buildStructuredTranscript(events: SessionEvent[]): StructuredTranscript {
- const userOnly: string[] = [];
- const userAndAssistant: string[] = [];
- const userAndTools: string[] = [];
+ function loadMemoryForPrompt(cwd: string): string {
+ if (!getMemoryEnabled()) return '';
+ const cfg = getMemoryConfig();
+ const projectPath = resolveMemoryPath(cwd);
+ const content = readMemoryFile(projectPath);
+ if (!content) return '';
+
+ const truncated = truncateForPrompt(content, cfg.promptMaxBytes);
+ return truncated ? `## Long-term Memory\n\n${truncated}` : '';
+ }
+
+ function buildTranscript(events: SessionEvent[]): string {
+ const lines: string[] = [];
for (const event of events) {
switch (event.type) {
case 'user':
- userOnly.push(`[user] ${event.content}`);
- userAndAssistant.push(`[user] ${event.content}`);
- userAndTools.push(`[user] ${event.content}`);
+ lines.push(`[user] ${event.content}`);
break;
case 'assistant':
- userAndAssistant.push(`[assistant] ${event.content}`);
+ lines.push(`[assistant] ${event.content}`);
break;
case 'tool_result':
if (
@@ -92,17 +77,12 @@ export class MemoryService extends Effect.Service()('Memory', {
event.toolName === 'read_file' ||
event.toolName === 'Read'
) {
- userAndTools.push(`[tool:${event.toolName}] ${event.output}`);
+ lines.push(`[tool:${event.toolName}] ${event.output}`);
}
break;
}
}
-
- return {
- userOnly: userOnly.join('\n---\n'),
- userAndAssistant: userAndAssistant.join('\n---\n'),
- userAndTools: userAndTools.join('\n---\n'),
- };
+ return lines.join('\n');
}
async function flushSessionToMemory(
@@ -119,25 +99,20 @@ export class MemoryService extends Effect.Service()('Memory', {
let events: SessionEvent[];
try {
- const { readFileSync } = await import('node:fs');
- const jsonlPath = sessionJsonlPathFromCwd(sessionCwd, sessionId);
- const content = readFileSync(jsonlPath, 'utf-8');
- events = content
- .split('\n')
- .filter((l) => l.trim() && !l.includes('"type":"session_meta"'))
- .map((l) => JSON.parse(l) as SessionEvent);
+ events = readTranscript(sessionCwd, sessionId).filter((e) => e.type !== 'session_meta');
} catch {
return { written: false, bytes: 0 };
}
+ if (events.length === 0) {
+ return { written: false, bytes: 0 };
+ }
const cfg = getMemoryConfig();
const projectPath = resolveMemoryPath(sessionCwd);
- const projectContent = readMemoryFile(projectPath);
- const currentAuto = extractAutoBlock(projectContent);
+ const current = readMemoryFile(projectPath);
try {
- const transcript = buildStructuredTranscript(events);
- const types = getEffectiveTypes(cfg);
+ const transcript = buildTranscript(events);
const resolvedLlm = await Effect.runPromise(
resolveLLM(cfg.model, llm).pipe(Effect.provideService(LLMFactoryService, factory))
@@ -147,24 +122,25 @@ export class MemoryService extends Effect.Service()('Memory', {
}
const extracted = await extractMemory({
- currentAuto,
+ currentMemory: current,
transcript,
- types,
llm: resolvedLlm,
});
-
if (!extracted) {
return { written: false, bytes: 0 };
}
- const projectContentFresh = readMemoryFile(projectPath);
- const projectAutoFresh = extractAutoBlock(projectContentFresh);
- const merged = mergeAutoBlocks(projectAutoFresh, extracted);
- const truncated = enforceMaxBytes(merged, MAX_BYTES);
- const newProjectContent = replaceAutoBlock(projectContentFresh, truncated);
+ // 提取期间文件被手动改动则放弃本次写入
+ if (readMemoryFile(projectPath) !== current) {
+ return { written: false, bytes: 0 };
+ }
- writeMemoryFileAtomic(projectPath, newProjectContent);
+ const truncated = enforceMaxBytes(extracted, MAX_BYTES);
+ if (truncated === current) {
+ return { written: false, bytes: 0 };
+ }
+ writeMemoryFileAtomic(projectPath, truncated);
return { written: true, bytes: Buffer.byteLength(truncated, 'utf-8') };
} catch {
return { written: false, bytes: 0 };
@@ -177,5 +153,4 @@ export class MemoryService extends Effect.Service()('Memory', {
loadMemoryForPrompt,
flushSessionToMemory,
};
- }),
-}) {}
+}));
diff --git a/packages/codingcode/src/memory/port.ts b/packages/codingcode/src/memory/port.ts
new file mode 100644
index 00000000..e8bc034e
--- /dev/null
+++ b/packages/codingcode/src/memory/port.ts
@@ -0,0 +1,11 @@
+import { Context } from 'effect';
+import type { LLMClient } from '../llm/client.js';
+
+export interface MemoryShape {
+ getMemoryEnabled(): boolean;
+ setMemoryEnabled(v: boolean): void;
+ loadMemoryForPrompt(cwd: string): string;
+ flushSessionToMemory(sessionId: string, llm: LLMClient | null, sessionCwd: string): Promise<{ written: boolean; bytes: number }>;
+}
+
+export class MemoryService extends Context.Tag('Memory')() {}
diff --git a/packages/codingcode/src/memory/storage.ts b/packages/codingcode/src/memory/storage.ts
index a2fd7dd5..5a883920 100644
--- a/packages/codingcode/src/memory/storage.ts
+++ b/packages/codingcode/src/memory/storage.ts
@@ -5,8 +5,6 @@ export function resolveMemoryPath(cwd: string): string {
return path.join(cwd, '.codingcode', 'memory.md');
}
-// ── File Read/Write ──
-
export function readMemoryFile(absPath: string): string {
try {
return fs.readFileSync(absPath, 'utf-8').trim();
@@ -15,32 +13,13 @@ export function readMemoryFile(absPath: string): string {
}
}
-export function extractAutoBlock(content: string): string {
- const match = content.match(/([\s\S]*?)/);
- return match ? match[1]!.trim() : '';
-}
-
-export function replaceAutoBlock(content: string, newAutoInner: string): string {
- const marker = '';
- const endMarker = '';
-
- if (content.includes(marker) && content.includes(endMarker)) {
- return content.replace(
- new RegExp(
- `${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`
- ),
- `${marker}\n${newAutoInner}\n${endMarker}`
- );
- }
-
- return `${marker}\n${newAutoInner}\n${endMarker}`;
-}
+export function writeMemoryFileAtomic(absPath: string, content: string): void {
+ const dir = path.dirname(absPath);
+ fs.mkdirSync(dir, { recursive: true });
-export function stripMarkersForPrompt(content: string): string {
- return content
- .replace(/\n?/g, '')
- .replace(/\n?/g, '')
- .trim();
+ const tmpFile = absPath + '.tmp';
+ fs.writeFileSync(tmpFile, content, 'utf-8');
+ fs.renameSync(tmpFile, absPath);
}
export function enforceMaxBytes(content: string, maxBytes: number): string {
@@ -50,56 +29,34 @@ export function enforceMaxBytes(content: string, maxBytes: number): string {
}
const sections = content.split(/^### /m).filter(Boolean);
- const namedSections = sections.map((s) => {
- const lines = s.split('\n');
- const name = lines[0]!;
- const body = lines.slice(1).join('\n');
- return { name, body, full: `### ${s}` };
- });
+ if (sections.length === 0) {
+ return truncateByLines(content, maxBytes);
+ }
let result = '';
- for (const section of namedSections) {
- if (Buffer.byteLength(result + section.full + '\n', 'utf-8') <= maxBytes) {
- result += (result ? '\n' : '') + section.full;
+ for (const section of sections) {
+ const candidate = result ? `${result}\n### ${section}` : `### ${section}`;
+ if (Buffer.byteLength(candidate, 'utf-8') <= maxBytes) {
+ result = candidate;
+ } else {
+ break;
}
}
-
- return result;
+ // 首个小节即超限时退化为按行截断,避免整份清空
+ if (!result) {
+ return truncateByLines(content, maxBytes);
+ }
+ return result.trim();
}
-export function mergeAutoBlocks(base: string, incoming: string): string {
- const extractH3Sections = (content: string): Record => {
- const sections: Record = {};
- const parts = content.split(/^### /m).filter(Boolean);
- for (const part of parts) {
- const lines = part.split('\n');
- const name = lines[0]!;
- const body = lines.slice(1).join('\n').trim();
- sections[name] = body;
+function truncateByLines(content: string, maxBytes: number): string {
+ let result = '';
+ for (const line of content.split('\n')) {
+ const candidate = result ? `${result}\n${line}` : line;
+ if (Buffer.byteLength(candidate, 'utf-8') > maxBytes) {
+ break;
}
- return sections;
- };
-
- const baseSections = extractH3Sections(base);
- const incomingSections = extractH3Sections(incoming);
-
- const merged: Record = { ...baseSections };
- for (const [name, body] of Object.entries(incomingSections)) {
- merged[name] = body;
+ result = candidate;
}
-
- const result = Object.entries(merged)
- .map(([name, body]) => `### ${name}\n${body}`)
- .join('\n\n');
-
return result;
}
-
-export function writeMemoryFileAtomic(absPath: string, content: string): void {
- const dir = path.dirname(absPath);
- fs.mkdirSync(dir, { recursive: true });
-
- const tmpFile = absPath + '.tmp';
- fs.writeFileSync(tmpFile, content, 'utf-8');
- fs.renameSync(tmpFile, absPath);
-}
diff --git a/packages/codingcode/src/memory/types.ts b/packages/codingcode/src/memory/types.ts
deleted file mode 100644
index 15b6f7a7..00000000
--- a/packages/codingcode/src/memory/types.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export interface MemoryTypeEntry {
- name: string;
- description: string;
- isBuiltIn: boolean;
- disabled: boolean;
-}
-
-export interface StructuredTranscript {
- userOnly: string;
- userAndAssistant: string;
- userAndTools: string;
-}
diff --git a/packages/codingcode/src/rules/port.ts b/packages/codingcode/src/rules/port.ts
new file mode 100644
index 00000000..06d4e9b8
--- /dev/null
+++ b/packages/codingcode/src/rules/port.ts
@@ -0,0 +1,8 @@
+import { Context } from 'effect';
+
+export interface RulesShape {
+ getAllRules(projectPath?: string): string;
+ evictProjectRules(projectPath: string): void;
+}
+
+export class RulesService extends Context.Tag('Rules')() {}
diff --git a/packages/codingcode/src/rules/index.ts b/packages/codingcode/src/rules/rules.ts
similarity index 57%
rename from packages/codingcode/src/rules/index.ts
rename to packages/codingcode/src/rules/rules.ts
index e0570f95..4be63ee3 100644
--- a/packages/codingcode/src/rules/index.ts
+++ b/packages/codingcode/src/rules/rules.ts
@@ -1,8 +1,8 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
-import { spawn } from 'node:child_process';
-import { Effect } from 'effect';
+import { Layer, Effect } from 'effect';
+import { RulesService } from './port.js';
// ── Paths ──
@@ -14,8 +14,7 @@ function getProjectRulesPath(projectPath?: string): string {
return path.join(projectPath ?? process.cwd(), 'AGENTS.md');
}
-export class RulesService extends Effect.Service()('Rules', {
- sync: () => {
+export const RulesLayer = Layer.effect(RulesService, Effect.sync(() => {
let _globalRules: string | null = null;
const _projectRulesCache = new Map();
const _allRulesCache = new Map();
@@ -67,66 +66,4 @@ export class RulesService extends Effect.Service()('Rules', {
_allRulesCache.delete(projectPath);
},
};
- },
-}) {}
-
-// ── Clear ──
-
-export function clearGlobalRules(): void {
- try {
- fs.unlinkSync(getGlobalRulesPath());
- } catch {
- // file may not exist
- }
-}
-
-export function clearProjectRules(projectPath?: string): void {
- try {
- fs.unlinkSync(getProjectRulesPath(projectPath));
- } catch {
- // file may not exist
- }
-}
-
-// ── Edit ──
-
-export function editInEditor(filePath: string): boolean {
- const editor =
- process.env.EDITOR || process.env.VISUAL || (process.platform === 'win32' ? 'notepad' : 'vim');
-
- try {
- if (process.platform === 'win32') {
- spawn('cmd.exe', ['/c', 'start', '', editor, filePath], {
- detached: true,
- stdio: 'ignore',
- windowsHide: true,
- }).unref();
- } else {
- spawn(editor, [filePath], {
- detached: true,
- stdio: 'ignore',
- }).unref();
- }
- return true;
- } catch {
- return false;
- }
-}
-
-export function editGlobalRules(): boolean {
- const p = getGlobalRulesPath();
- const dir = path.dirname(p);
- fs.mkdirSync(dir, { recursive: true });
- if (!fs.existsSync(p)) {
- fs.writeFileSync(p, '', 'utf-8');
- }
- return editInEditor(p);
-}
-
-export function editProjectRules(projectPath?: string): boolean {
- const p = getProjectRulesPath(projectPath);
- if (!fs.existsSync(p)) {
- fs.writeFileSync(p, '', 'utf-8');
- }
- return editInEditor(p);
-}
+}));
diff --git a/packages/codingcode/src/runtime/project-runtime.ts b/packages/codingcode/src/runtime/project-runtime.ts
deleted file mode 100644
index dfaf174a..00000000
--- a/packages/codingcode/src/runtime/project-runtime.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { Effect } from 'effect';
-import type { AgentProfile, AgentProfileName } from '../subagent/types.js';
-import type { ToolVisibilityPolicy } from '../tools/types.js';
-import { HookService } from '../hooks/registry.js';
-import { McpService } from '../mcp/index.js';
-import { RulesService } from '../rules/index.js';
-import { SessionService } from '../session/store.js';
-import { normalizePath } from '../core/path.js';
-import type { PermissionMode } from '../approval/types.js';
-import { readCurrentIndex } from '../session/file-ops.js';
-import { computePaths } from '../core/path.js';
-import {
- BUILD_PROFILE,
- PLAN_PROFILE,
- isPlanProfile,
- PLAN_PROFILE_ALLOWED_TOOLS,
-} from '../agent/profile.js';
-
-function isAgentProfileName(name: string): name is AgentProfileName {
- return name === PLAN_PROFILE.name || name === BUILD_PROFILE.name;
-}
-
-function profileByName(name: AgentProfileName): AgentProfile {
- return name === PLAN_PROFILE.name ? PLAN_PROFILE : BUILD_PROFILE;
-}
-
-export class ProjectRuntimeService extends Effect.Service()(
- 'ProjectRuntime',
- {
- effect: Effect.gen(function* () {
- const hooks = yield* HookService;
- const mcp = yield* McpService;
- const rules = yield* RulesService;
- const session = yield* SessionService;
- const prepared = new Set();
-
- return {
- prepareProject: (projectPath: string): Effect.Effect =>
- Effect.gen(function* () {
- const norm = normalizePath(projectPath);
- if (prepared.has(norm)) return;
- prepared.add(norm);
- rules.evictProjectRules(norm);
- yield* hooks.reloadUserHooks(norm).pipe(Effect.catchAll(() => Effect.void));
- yield* mcp.syncConnections(norm).pipe(Effect.catchAll(() => Effect.void));
- }),
-
- resolveMainAgentProfile: (
- projectPath: string,
- sessionId: string
- ): AgentProfile | undefined => {
- const idx = readCurrentIndex(computePaths(projectPath, sessionId).indexPath);
- const name = idx?.activeProfile;
- return name ? profileByName(name) : undefined;
- },
-
- resolveSubagentProfile: (_projectPath: string, name: string): AgentProfile | undefined =>
- isAgentProfileName(name) ? profileByName(name) : undefined,
-
- getToolPolicy: (profile: AgentProfile | undefined): ToolVisibilityPolicy => ({
- allowedTools: isPlanProfile(profile) ? new Set(PLAN_PROFILE_ALLOWED_TOOLS) : undefined,
- allowedMcpServers: undefined,
- }),
-
- setSessionProfile: (
- projectPath: string,
- sessionId: string,
- profile: AgentProfile,
- permissionModeOverride?: PermissionMode
- ): Effect.Effect =>
- Effect.gen(function* () {
- const effectivePerm: PermissionMode = permissionModeOverride ?? 'default';
- yield* session.setPermissionModeOnDisk(projectPath, sessionId, effectivePerm);
- yield* session.setActiveProfile(projectPath, sessionId, profile.name);
- }),
-
- getSessionProfile: (
- sessionId: string,
- projectPath: string
- ): Effect.Effect =>
- Effect.gen(function* () {
- const name = yield* session.getActiveProfile(projectPath, sessionId);
- return profileByName(name);
- }),
-
- getSessionPermissionMode: (
- sessionId: string,
- projectPath: string
- ): Effect.Effect =>
- session.getPermissionModeFromDisk(projectPath, sessionId),
-
- restoreSessionProfile: (
- projectPath: string,
- sessionId: string,
- profileName: AgentProfileName,
- permissionModeOverride?: PermissionMode
- ): Effect.Effect =>
- Effect.gen(function* () {
- const profile = profileByName(profileName);
- const effectivePerm: PermissionMode = permissionModeOverride ?? 'default';
- yield* session.setPermissionModeOnDisk(projectPath, sessionId, effectivePerm);
- yield* session.setActiveProfile(projectPath, sessionId, profile.name);
- }),
-
- disposeSession: (_sessionId: string): Effect.Effect => Effect.void,
-
- disposeProject: (projectPath: string): Effect.Effect =>
- Effect.sync(() => {
- const norm = normalizePath(projectPath);
- prepared.delete(norm);
- rules.evictProjectRules(norm);
- }),
- };
- }),
- }
-) {}
diff --git a/packages/codingcode/src/sandbox/index.ts b/packages/codingcode/src/sandbox/index.ts
deleted file mode 100644
index d200b587..00000000
--- a/packages/codingcode/src/sandbox/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-// Sandbox module — reserved for future OS-level runtime isolation.
-
-export interface SandboxConfig {
- allowedDomains?: string[];
- deniedDomains?: string[];
- allowReadPaths?: string[];
- allowWritePaths?: string[];
- denyReadPaths?: string[];
- denyWritePaths?: string[];
- allowUnixSockets?: string[];
- defaultTimeoutMs?: number;
-}
-
-export interface ExecResult {
- stdout: string;
- stderr: string;
- exitCode: number;
-}
-
-export interface ExecuteOptions {
- command: string;
- timeoutMs?: number;
-}
-
-/** Stub service — re-implement here when a real sandbox runtime is integrated. */
-export class SandboxService {}
diff --git a/packages/codingcode/src/scheduler/port.ts b/packages/codingcode/src/scheduler/port.ts
new file mode 100644
index 00000000..68ab892e
--- /dev/null
+++ b/packages/codingcode/src/scheduler/port.ts
@@ -0,0 +1,17 @@
+import { Context } from 'effect';
+import type { Effect } from 'effect';
+import type { ManagedRuntime } from 'effect';
+import type { Automation, CreateAutomationInput, UpdateAutomationInput } from './types.js';
+
+export interface SchedulerShape {
+ setRuntime(rt: ManagedRuntime.ManagedRuntime): void;
+ initialize(): void;
+ list(): Automation[];
+ add(input: CreateAutomationInput): Automation;
+ update(id: string, patch: UpdateAutomationInput): Automation | null;
+ remove(id: string): boolean;
+ runOnce(id: string): Promise