feat: 拆分输入解析功能;拓展输入命令;更新文档规划
This commit is contained in:
parent
8c7a9098ef
commit
8fdf29b4e9
@ -15,10 +15,12 @@ pnpm dev
|
|||||||
其中设置 `DEEPSEEK_API_KEY`;
|
其中设置 `DEEPSEEK_API_KEY`;
|
||||||
也可用 `LLM_TO_AGENT_HOME` 修改数据根目录,用 `DEEPSEEK_MODEL` 修改模型。
|
也可用 `LLM_TO_AGENT_HOME` 修改数据根目录,用 `DEEPSEEK_MODEL` 修改模型。
|
||||||
|
|
||||||
|
CLI 支持 `/new` 新建对话、`/switch <id>` 切换对话、`/history` 查看当前对话、`/exit` 退出;输入 `//` 可以发送以 `/` 开头的普通消息。
|
||||||
|
|
||||||
文档:
|
文档:
|
||||||
|
|
||||||
- [架构总览](docs/architecture.md)
|
- [架构总览](docs/architecture.md)
|
||||||
- [源码目录](docs/source-layout.md)
|
- [源码目录](docs/source-layout.md)
|
||||||
- [数据目录](docs/data-layout.md)
|
- [数据目录](docs/data-layout.md)
|
||||||
- [Extension 目录](docs/extensions.md)
|
- [Extension 规划](docs/extensions.md)
|
||||||
- [路线图](docs/roadmap.md)
|
- [路线图](docs/roadmap.md)
|
||||||
|
|||||||
@ -2,53 +2,81 @@
|
|||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
llm-to-agent 是个人项目,架构优先级是:
|
llm-to-agent 是个人使用、本地优先的 Agent。架构优先级是:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
简单易懂 > 容易修改 > 容易扩展 > 通用性
|
简单易懂 > 容易扩展和修改 > 架构完整 > 通用性
|
||||||
```
|
```
|
||||||
|
|
||||||
Runtime 内部长期只有:
|
Runtime 长期保持:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Kernel + Extensions
|
Kernel + Extensions
|
||||||
```
|
```
|
||||||
|
|
||||||
Kernel 管“Extension 怎样连接和运行”,Extension 管“系统具体能做什么”。
|
Kernel 管 Extension 怎样连接和运行,Extension 管系统具体能做什么。
|
||||||
|
|
||||||
## Kernel
|
## Kernel
|
||||||
|
|
||||||
第一版 Kernel 只保留几件直观的事:
|
Kernel 只提供所有 Extension 共用的运行机制:
|
||||||
|
|
||||||
- 按顺序装入 Extension;
|
- 按顺序装入 Extension;
|
||||||
- 依次执行 `setup` 和 `start`,停止时倒序执行 `stop`;
|
- 依次执行 `setup` 和 `start`,停止时倒序执行 `stop`;
|
||||||
- 通过命名抓手提供和取得能力;
|
- 通过命名抓手提供和取得能力;
|
||||||
- 通过命名事件发布和监听事实。
|
- 通过命名事件发布和监听事实。
|
||||||
|
|
||||||
抓手和事件暂时使用带命名空间的字符串,不为它们建立复杂的 TypeScript 类型系统。Setup Context 负责注册,Runtime Context 负责使用;Kernel 信任 Extension 遵守生命周期,不增加 Context 失效检查和不可变包装。
|
Kernel 不认识 Workspace、Agent、模型、Tool 或其他业务概念。
|
||||||
|
|
||||||
Kernel 不认识 Workspace、Conversation、Run、Agent、模型或 Tool。只有所有 Extension 都必须遵守的运行规则才进入 Kernel。
|
|
||||||
|
|
||||||
## Extensions
|
## Extensions
|
||||||
|
|
||||||
Extension 可以提供能力、调用其他能力、添加同类实现、发布或监听事实,并管理自己的数据与资源。
|
Extension 是 Runtime 的独立装配单位,分为三类:
|
||||||
|
|
||||||
控制流程必须使用明确调用;事件只表示已经发生的事实,不能承担顺序、返回值、审批或回滚。
|
- 共享能力:拥有明确状态、资源或业务流程;
|
||||||
|
- Provider:接入可替换的模型、协议或外部系统;
|
||||||
|
- 产品入口:处理 CLI、Web、Desktop 的输入、输出和平台资源。
|
||||||
|
|
||||||
Extension 之间只依赖公开契约,不引用彼此的实现。
|
规划可以列出尚未实现的 Extension,源码、Catalog 和产品装配只包含已经开始实现的部分。Extension 内部文件和目录不设统一结构。
|
||||||
|
|
||||||
具体能力归属见 [Extension 目录](extensions.md)。
|
最终能力归属见 [Extension 规划](extensions.md)。
|
||||||
|
|
||||||
|
## 扩展点
|
||||||
|
|
||||||
|
多个实现向能力所有者登记,不再为扩展点增加新的架构层:
|
||||||
|
|
||||||
|
```text
|
||||||
|
models.providers
|
||||||
|
tools.providers
|
||||||
|
agent.contextContributors
|
||||||
|
automation.triggers
|
||||||
|
security.approvalChannels
|
||||||
|
health.checks
|
||||||
|
```
|
||||||
|
|
||||||
|
模型 Provider 由 `models` 选择,具体 Tool 由 `tools` 统一执行,上下文来源由 `agent` 组合。
|
||||||
|
|
||||||
|
## 协作
|
||||||
|
|
||||||
|
- 控制流程使用明确调用;
|
||||||
|
- 事件只表示已经发生的事实;
|
||||||
|
- Extension 只依赖其他 Extension 的公开契约;
|
||||||
|
- Provider 向扩展点注册实现,能力所有者不引用 Provider 实现;
|
||||||
|
- Tool 是能力 Extension 暴露给 Agent 的操作,不是新的 Extension;
|
||||||
|
- 高风险操作统一经过 `security`,凭据统一通过 `secrets` 的受控引用取得。
|
||||||
|
|
||||||
|
事件不能承担顺序、返回值、审批、事务或回滚。
|
||||||
|
|
||||||
## Runtime 与产品
|
## Runtime 与产品
|
||||||
|
|
||||||
Runtime 持有唯一一套 Kernel、Extension 实例和本地数据。CLI、Web、Desktop 是不同产品入口,使用同一套能力和状态。
|
Runtime 持有一套 Kernel、Extension 实例和本地数据。CLI、Web、Desktop 是不同产品入口,共用共享能力和数据。
|
||||||
|
|
||||||
自进化阶段再增加 Runtime 外的 Supervisor,用于版本切换、健康检查和失败回退。
|
版本切换、进程守护、离线恢复和失败回退由 Runtime 外的 Supervisor 负责。
|
||||||
|
|
||||||
## 长期约束
|
## 长期约束
|
||||||
|
|
||||||
- 默认在现有 Extension 内增加普通代码,出现真实独立边界后再拆。
|
- 保持单用户、本地优先和单 Runtime;
|
||||||
- 第一版静态装配,不建设插件平台、热加载或依赖图。
|
- 保持一个 TypeScript package,直到出现独立构建、发布或进程边界;
|
||||||
- 保持单进程、单用户和本地优先,直到真实需求要求改变。
|
- 使用静态装配,不提前建设插件平台、热加载或依赖图;
|
||||||
|
- 规划高风险边界,但不创建占位目录、文件、ID 或 Hook;
|
||||||
|
- 普通函数、页面、命令、Parser、Prompt 和单个 Tool 不拆成 Extension;
|
||||||
|
- 新增 Kernel 机制必须有多个具体使用者;
|
||||||
- 不提前建设 Command Bus、Middleware、事件溯源或分布式状态。
|
- 不提前建设 Command Bus、Middleware、事件溯源或分布式状态。
|
||||||
- 新增 Kernel 机制必须有多个具体使用者。
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
~/.llm-to-agent/
|
~/.llm-to-agent/
|
||||||
```
|
```
|
||||||
|
|
||||||
开发环境可以指向另一个用户目录,测试使用临时目录。
|
开发环境可以指定其他数据根,测试使用临时目录。
|
||||||
|
|
||||||
## 物理目录
|
## 物理目录
|
||||||
|
|
||||||
@ -17,40 +17,51 @@
|
|||||||
├── config/
|
├── config/
|
||||||
├── workspaces/
|
├── workspaces/
|
||||||
│ └── <workspace-id>/
|
│ └── <workspace-id>/
|
||||||
|
│ ├── workspace.json
|
||||||
│ ├── files/
|
│ ├── files/
|
||||||
│ ├── conversations/
|
│ ├── conversations/
|
||||||
│ ├── runs/
|
│ ├── artifacts/
|
||||||
│ └── extensions/
|
│ └── extensions/
|
||||||
|
│ └── <extension-id>/
|
||||||
├── extensions/
|
├── extensions/
|
||||||
|
│ └── <extension-id>/
|
||||||
├── releases/
|
├── releases/
|
||||||
├── runtime/
|
├── runtime/
|
||||||
└── trash/
|
└── trash/
|
||||||
```
|
```
|
||||||
|
|
||||||
目录按需要创建,不规定尚未实现的数据文件和格式。
|
目录按需要创建,不预建尚未使用的层级。
|
||||||
|
|
||||||
当前对话链路只会创建 Workspace 绑定信息和
|
## 数据归属
|
||||||
`conversations/default/messages.jsonl`,后续能力再增加自己的数据。
|
|
||||||
|
- `workspace` 管理 Workspace、Conversation、Message 和 Artifact;
|
||||||
|
- `agent` 管理 Agent Run;
|
||||||
|
- `tasks` 管理 Goal、Plan 和任务执行状态;
|
||||||
|
- `memory` 与 `knowledge` 分别管理自己的长期数据和索引;
|
||||||
|
- `automation` 管理 Workflow、Schedule 和执行历史;
|
||||||
|
- `security` 管理审批和安全记录;
|
||||||
|
- 每个 Extension 只能通过公开契约访问其他 Extension 的数据;
|
||||||
|
- 每个数据所有者负责自己的迁移、校验、导出和清理。
|
||||||
|
|
||||||
|
Workspace 范围的私有数据写入 `workspaces/<workspace-id>/extensions/<extension-id>/`,全局私有数据写入 `extensions/<extension-id>/`。
|
||||||
|
|
||||||
|
凭据明文不写入数据根,由 `secrets` 使用系统 Keychain 或受控加密存储管理。
|
||||||
|
|
||||||
## Workspace
|
## Workspace
|
||||||
|
|
||||||
- `managed` Workspace 的工作文件位于自己的 `files/`。
|
- `managed` Workspace 的工作文件位于自己的 `files/`;
|
||||||
- `linked` Workspace 只记录用户已有目录的绑定关系。
|
- `linked` Workspace 只记录用户已有目录的绑定关系;
|
||||||
- 两种 Workspace 的 Conversation、Run、记忆和 Extension 数据都位于数据根目录。
|
- 产品入口各自管理当前选择的 Workspace 和 Conversation;
|
||||||
|
- 删除 linked Workspace 绝不能删除外部项目源码。
|
||||||
|
|
||||||
## 项目目录零落地
|
## 项目目录零落地
|
||||||
|
|
||||||
绑定外部项目时,项目源码保留在原位置,所有 llm-to-agent 专属数据仍保存在:
|
绑定外部项目时,项目源码保留在原位置,所有 llm-to-agent 专属数据仍保存在数据根中。项目中不创建 `.llm-to-agent/` 或其他专属元数据。
|
||||||
|
|
||||||
```text
|
|
||||||
<LLM_TO_AGENT_HOME>/workspaces/<workspace-id>/
|
|
||||||
```
|
|
||||||
|
|
||||||
项目中不创建 `.llm-to-agent/` 或其他专属元数据。项目移动时更新绑定关系;删除 linked Workspace 绝不能删除外部项目源码。
|
|
||||||
|
|
||||||
## 数据规则
|
## 数据规则
|
||||||
|
|
||||||
- Kernel 只提供安全路径、隔离和基础写入机制,不理解业务数据。
|
- Runtime 公共库只提供安全路径、原子写入和基础存储工具,不理解业务数据;
|
||||||
- 每个 Extension 管理自己的数据,不能直接修改其他 Extension 的私有内容。
|
- 缓存、索引和临时数据必须可以清理重建;
|
||||||
- 缓存和临时数据必须可以清理重建。
|
- 删除重要数据默认先进入 `trash/`;
|
||||||
- 删除重要数据默认先进入 `trash/`,迁移和修复前先保留可恢复副本。
|
- 迁移、修复和恢复前保留可恢复副本;
|
||||||
|
- 全局备份、离线恢复和版本回退由 Supervisor 协调。
|
||||||
|
|||||||
@ -1,66 +1,78 @@
|
|||||||
# Extension 目录
|
# Extension 规划
|
||||||
|
|
||||||
具体能力全部由 Extension 实现。Tool、页面、命令和事件监听器只是 Extension 内部组成,不是新的架构类型。
|
## 共享能力
|
||||||
|
|
||||||
## 第一版
|
| Extension | 职责 | 公开能力与 Tool |
|
||||||
|
|---|---|---|
|
||||||
|
| `workspace` | Workspace、Conversation、Message、Artifact | `WorkspaceService`;默认不提供 Tool |
|
||||||
|
| `models` | 模型注册、选择、流式调用、Embedding 和 Fallback | `ModelService`;接收 `models.providers` |
|
||||||
|
| `tools` | Tool 注册、Schema 校验、执行、取消和安全拦截 | `ToolService`;可提供 `tools.search`、`tools.describe` |
|
||||||
|
| `agent` | 一次 Agent Run、上下文构建、模型与 Tool 循环 | `AgentService`;接收 `agent.contextContributors` |
|
||||||
|
| `tasks` | Goal、Plan、子 Agent、任务依赖、等待和恢复 | `TaskService`;`tasks.update_plan`、`tasks.delegate`、`tasks.wait` |
|
||||||
|
| `memory` | 用户偏好、事实、经验和长期记忆 | `MemoryService`;`memory.recall`、`memory.propose`、`memory.forget` |
|
||||||
|
| `knowledge` | 知识来源、索引、检索和引用 | `KnowledgeService`;`knowledge.search`、`knowledge.open_source` |
|
||||||
|
| `project` | 项目路径、文件、搜索、Patch、项目识别和验证 | `ProjectService`;`project.read`、`project.search`、`project.apply_patch`、`project.run_checks` |
|
||||||
|
| `shell` | 命令、PTY、输出流、超时和取消 | `ShellService`;`shell.run`、`shell.read`、`shell.cancel` |
|
||||||
|
| `git` | Repository、Diff、分支、Commit 和 Worktree | `GitService`;`git.status`、`git.diff`、`git.log`、`git.create_worktree` |
|
||||||
|
| `automation` | Workflow、Scheduler、提醒、重试和执行历史 | `AutomationService`;`automation.create`、`automation.schedule`、`automation.pause`、`automation.run_now` |
|
||||||
|
| `security` | 风险规则、授权、审批和安全记录 | `SecurityService`;接收 `security.approvalChannels`,不提供 Tool |
|
||||||
|
| `secrets` | Keychain、凭据引用、授权范围和轮换 | `SecretsService`;不向模型提供明文凭据 |
|
||||||
|
| `browser` | 浏览器 Session 和页面操作 | `BrowserService`;`browser.open`、`browser.read`、`browser.click`、`browser.type` |
|
||||||
|
| `computer` | 屏幕、窗口、应用、键鼠和剪贴板 | `ComputerService`;`computer.screenshot`、`computer.open_app`、`computer.click`、`computer.type` |
|
||||||
|
| `evolution` | 自身修改候选、验证和发布提案 | `EvolutionService`;`evolution.prepare`、`evolution.validate` |
|
||||||
|
|
||||||
|
`browser`、`computer` 和 `evolution` 在实现对应能力时创建。
|
||||||
|
|
||||||
|
## Provider
|
||||||
|
|
||||||
|
- 模型 Provider:`deepseek` 以及实际接入的其他模型;
|
||||||
|
- Tool Provider:GitHub、MCP 和其他实际接入的外部系统;
|
||||||
|
- Provider 管理自己的协议、配置、资源和故障,向所属扩展点注册实现;
|
||||||
|
- 一个 Provider 可以提供多个 Tool,一个 Tool 不对应一个 Extension。
|
||||||
|
|
||||||
|
## 产品入口
|
||||||
|
|
||||||
|
- `cli`:终端输入、命令、展示和审批通道;
|
||||||
|
- `web`:HTTP、WebSocket、页面和 Web 会话;
|
||||||
|
- `desktop`:窗口、快捷键、通知和原生平台集成。
|
||||||
|
|
||||||
|
产品入口不拥有共享业务能力。
|
||||||
|
|
||||||
|
## 协作关系
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/extensions/
|
模型 Provider ────────────────> models
|
||||||
├── catalog.ts
|
|
||||||
├── shared/
|
workspace / memory / knowledge ─> agent.contextContributors
|
||||||
│ ├── workspace/
|
|
||||||
│ ├── agent/
|
project / shell / git
|
||||||
│ └── deepseek/
|
browser / computer / 外部 Provider ─> tools.providers
|
||||||
└── cli/
|
|
||||||
|
tasks ────────────────> agent
|
||||||
|
automation ───────────> tasks / agent / tools
|
||||||
|
agent ────────────────> workspace / models / tools / memory / knowledge
|
||||||
|
tools ────────────────> security
|
||||||
|
外部 Provider ────────> secrets
|
||||||
|
evolution ────────────> project / shell / git
|
||||||
|
|
||||||
|
cli / web / desktop ──> workspace / agent / tasks / automation / security
|
||||||
```
|
```
|
||||||
|
|
||||||
- `workspace`:Workspace、Conversation 和消息。
|
## 不单独拆分
|
||||||
- `agent`:Run、上下文和 Agent 执行。
|
|
||||||
- `deepseek`:DeepSeek 模型接入。
|
|
||||||
- `cli`:终端输入、命令和展示。
|
|
||||||
|
|
||||||
第一版只静态装配这四个 Extension。
|
- Conversation、Message、Artifact 归 `workspace`;
|
||||||
|
- Context、Agent Profile 和声明式 Skill 归 `agent`;
|
||||||
|
- Workflow、Scheduler 和 Notification 请求归 `automation`;
|
||||||
|
- Policy、Approval 和 Audit 归 `security`;
|
||||||
|
- 项目文件归 `project`,进程归 `shell`,仓库归 `git`;
|
||||||
|
- Run 由实际执行者拥有,不创建通用 Run Extension;
|
||||||
|
- 页面、命令、Parser、Prompt、模板和单个 Tool 不是 Extension;
|
||||||
|
- 数据迁移、校验和清理由数据所有者实现。
|
||||||
|
|
||||||
`catalog.ts` 集中列出所有跨 Extension 使用的 Extension ID、能力抓手和事实事件;它只保存字符串,不保存类型或运行逻辑。
|
## 创建规则
|
||||||
|
|
||||||
当前协作链路:
|
- 规划中的 Extension 只有开始实现时才创建源码;
|
||||||
|
- `catalog.ts` 只登记已实现并发生跨 Extension 协作的 ID、Hook 和 Event;
|
||||||
```text
|
- 不创建占位目录、文件、Factory、ID 或 Hook;
|
||||||
workspace -> 提供 workspace
|
- Extension 内部文件划分不作统一限制;
|
||||||
deepseek -> 添加 model.providers
|
- 当前保持单包,只有独立构建、发布或进程隔离时才拆包。
|
||||||
agent -> 使用二者并提供 agent
|
|
||||||
cli -> 调用 agent
|
|
||||||
```
|
|
||||||
|
|
||||||
## 协作
|
|
||||||
|
|
||||||
- 一个明确能力由 Extension 提供,调用方取得后直接调用。
|
|
||||||
- 模型、Tool 等多个实现通过同一扩展点汇集,由业务拥有者选择。
|
|
||||||
- 状态真正发生后再发布事件,监听器只做展示、日志或派生处理。
|
|
||||||
- Extension 只引用其他 Extension 的公开契约。
|
|
||||||
|
|
||||||
## 后续方向
|
|
||||||
|
|
||||||
后续公共能力大致包括:
|
|
||||||
|
|
||||||
```text
|
|
||||||
项目操作与 Git
|
|
||||||
自身进化
|
|
||||||
计划与多 Agent
|
|
||||||
记忆与知识
|
|
||||||
浏览器与本机控制
|
|
||||||
自动化
|
|
||||||
```
|
|
||||||
|
|
||||||
CLI、Web、Desktop 各自先保持一个产品 Extension。远程仓库、外部 Tool 协议、Keychain 等在实际接入时再决定是否独立。
|
|
||||||
|
|
||||||
## 何时拆分
|
|
||||||
|
|
||||||
只有出现以下真实边界时才创建新 Extension:
|
|
||||||
|
|
||||||
- 需要独立启停;
|
|
||||||
- 拥有独立的长期资源或数据;
|
|
||||||
- 存在可替换实现;
|
|
||||||
- 有明确产品或平台边界。
|
|
||||||
|
|
||||||
否则继续留在现有 Extension 内。目录只在能力开始实现时创建,不预建占位代码。
|
|
||||||
|
|||||||
@ -1,46 +1,45 @@
|
|||||||
# 路线图
|
# 路线图
|
||||||
|
|
||||||
路线图只描述大的实现顺序。安全、日志、取消和数据保护从每个阶段开始就随能力一起实现。
|
## P1:持续对话
|
||||||
|
|
||||||
## P1:能持续聊天
|
建立 Kernel、`workspace`、`models`、`secrets`、`agent`、模型 Provider 和 `cli`。
|
||||||
|
|
||||||
建立最小 Kernel、第一版 Extension 和集中式数据目录。
|
|
||||||
|
|
||||||
完成标志:CLI 可以管理 Workspace 与 Conversation,流式聊天,并在重启后继续历史。
|
完成标志:CLI 可以管理 Workspace 与 Conversation,流式聊天,并在重启后继续历史。
|
||||||
|
|
||||||
## P2:能操作项目
|
## P2:项目闭环
|
||||||
|
|
||||||
加入文件、Shell、Tool Calling、Git、Worktree、验证和最小权限确认。
|
实现 `tools`、`security`、`project`、`shell` 和 `git`。
|
||||||
|
|
||||||
完成标志:Agent 可以在普通代码项目中完成一次可审核的修改与测试闭环。
|
完成标志:Agent 可以在受控范围内读取和修改项目、运行验证、检查 Git 变更,并处理审批。
|
||||||
|
|
||||||
## P3:能修改自己
|
## P3:自身演进
|
||||||
|
|
||||||
加入隔离候选、构建验证、发布确认、版本切换和失败回退。
|
实现 `evolution` 和 Runtime 外的 Supervisor。
|
||||||
|
|
||||||
完成标志:Agent 可以安全完成一次自身小能力的修改与发布。
|
完成标志:Agent 可以生成隔离候选、完成验证和发布提案,Supervisor 可以安全切换版本并失败回退。
|
||||||
|
|
||||||
## P4:能处理复杂工作
|
## P4:复杂任务
|
||||||
|
|
||||||
加入多模型、计划、多 Agent、工作流、长期记忆和知识检索。
|
实现 `tasks`、`memory`、`knowledge` 和更多需要的模型 Provider。
|
||||||
|
|
||||||
完成标志:复杂目标可以持续执行,跨 Conversation 找到相关上下文,并给出可验证结果。
|
完成标志:复杂目标可以持续执行、使用子 Agent、恢复任务并检索长期上下文。
|
||||||
|
|
||||||
## P5:能管理电脑
|
## P5:电脑与自动化
|
||||||
|
|
||||||
加入浏览器、本机控制、多模态、提醒、定时任务和自动化。
|
实现 `browser`、`computer` 和 `automation`,完善 `secrets` 的 Keychain、轮换和恢复能力。
|
||||||
|
|
||||||
完成标志:Agent 可以完成开发之外的高频电脑事务,并可靠重复执行。
|
完成标志:Agent 可以操作浏览器和本机应用,管理提醒与自动化,并安全使用外部凭据。
|
||||||
|
|
||||||
## P6:适合长期使用
|
## P6:长期使用
|
||||||
|
|
||||||
实现 Web 和 Desktop,完善远程访问、诊断、备份迁移、资源清理、凭据保护和版本维护。
|
实现 `web` 和 `desktop`,完善远程访问、诊断、备份迁移、资源清理和版本维护。
|
||||||
|
|
||||||
完成标志:三个产品共用一套稳定 Runtime 和数据,能够长期日常使用。
|
完成标志:CLI、Web、Desktop 共用一套稳定 Runtime 和数据,可以长期日常使用。
|
||||||
|
|
||||||
## 执行原则
|
## 执行原则
|
||||||
|
|
||||||
- 每个阶段开始时再拆提交级任务。
|
- 每个阶段开始时再拆提交级任务;
|
||||||
- 先跑通最短链路,再根据真实压力抽象。
|
- 只创建当前阶段实际使用的 Extension;
|
||||||
- 新增普通能力默认修改 Extension;只有通用运行机制才修改 Kernel。
|
- 优先跑通最短闭环,再补充实现细节;
|
||||||
- 路线图允许随实践调整,不维护小功能状态表。
|
- 安全、取消、日志和数据保护随能力一起实现;
|
||||||
|
- 路线图只记录阶段和完成标志,不维护小功能状态表。
|
||||||
|
|||||||
@ -7,6 +7,7 @@ llm-to-agent/
|
|||||||
├── src/
|
├── src/
|
||||||
│ ├── kernel/
|
│ ├── kernel/
|
||||||
│ ├── extensions/
|
│ ├── extensions/
|
||||||
|
│ │ ├── catalog.ts
|
||||||
│ │ ├── shared/
|
│ │ ├── shared/
|
||||||
│ │ ├── cli/
|
│ │ ├── cli/
|
||||||
│ │ ├── web/
|
│ │ ├── web/
|
||||||
@ -22,18 +23,22 @@ llm-to-agent/
|
|||||||
|
|
||||||
## 目录职责
|
## 目录职责
|
||||||
|
|
||||||
- `kernel/`:只保存 Extension 运行机制。
|
- `kernel/`:Extension 生命周期、能力注册和事实事件;
|
||||||
- `extensions/shared/`:跨产品使用的具体能力。
|
- `extensions/catalog.ts`:已经实现的跨 Extension ID、Hook 和 Event;
|
||||||
- `extensions/cli|web|desktop/`:三条产品线各自的输入、展示和平台集成。
|
- `extensions/shared/`:跨产品使用的能力和 Provider;
|
||||||
- `products/`:静态选择每个产品启用哪些 Extension,不写业务逻辑。
|
- `extensions/cli|web|desktop/`:产品输入、展示和平台集成;
|
||||||
- `tests/`:只保存跨 Extension、跨进程或跨版本测试;普通测试跟随源码。
|
- `products/`:静态选择产品启用的 Extension,不写业务逻辑;
|
||||||
|
- `tests/`:跨 Extension、跨进程或跨版本测试;
|
||||||
- `tooling/`:构建、开发和发布辅助。
|
- `tooling/`:构建、开发和发布辅助。
|
||||||
|
|
||||||
Extension 的具体划分见 [Extension 目录](extensions.md)。
|
Extension 的最终职责见 [Extension 规划](extensions.md)。
|
||||||
|
|
||||||
## 放置规则
|
## 放置规则
|
||||||
|
|
||||||
- 业务概念和流程进入 Extension,不为了复用方便放进 Kernel。
|
- 源码目录只在对应能力开始实现时创建;
|
||||||
- 产品私有实现不能互相依赖,真实复用出现后再提升到 `shared/`。
|
- Extension 内部文件和目录不设统一模板;
|
||||||
- 目录和文件只在真实代码出现时创建,不预建占位层级。
|
- 业务概念和流程进入 Extension,不进入 Kernel;
|
||||||
- 当前保持单包;只有构建、平台依赖、独立分发或进程隔离造成实际问题时才拆包。
|
- 产品私有实现不能互相依赖;
|
||||||
|
- Provider 只通过公开契约和扩展点接入能力所有者;
|
||||||
|
- 普通复用代码不因此升级为 Extension;
|
||||||
|
- 当前保持单包,只有独立构建、发布或进程隔离时才拆包。
|
||||||
|
|||||||
@ -101,3 +101,46 @@ test("streams a reply, saves it, and restores the conversation after restart", a
|
|||||||
|
|
||||||
await secondKernel.stop();
|
await secondKernel.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("creates a conversation and restores it after restart", async (t) => {
|
||||||
|
const temporaryDirectory = await mkdtemp(join(tmpdir(), "llm-to-agent-"));
|
||||||
|
const home = join(temporaryDirectory, "home");
|
||||||
|
const projectPath = join(temporaryDirectory, "project");
|
||||||
|
await mkdir(projectPath);
|
||||||
|
t.after(() => rm(temporaryDirectory, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const firstKernel = new Kernel().use(
|
||||||
|
createWorkspaceExtension({ home, projectPath }),
|
||||||
|
);
|
||||||
|
await firstKernel.start();
|
||||||
|
|
||||||
|
const firstWorkspace = firstKernel.get<WorkspaceService>(Hook.Workspace);
|
||||||
|
await firstWorkspace.append("user", "旧对话消息");
|
||||||
|
const conversationId = await firstWorkspace.newConversation();
|
||||||
|
assert.deepEqual(await firstWorkspace.messages(), []);
|
||||||
|
await firstWorkspace.append("user", "新对话消息");
|
||||||
|
assert.equal(await firstWorkspace.switchConversation("default"), true);
|
||||||
|
assert.deepEqual(
|
||||||
|
(await firstWorkspace.messages()).map(({ role, content }) => ({ role, content })),
|
||||||
|
[{ role: "user", content: "旧对话消息" }],
|
||||||
|
);
|
||||||
|
assert.equal(await firstWorkspace.switchConversation("missing"), false);
|
||||||
|
assert.equal(firstWorkspace.conversationId, "default");
|
||||||
|
assert.equal(await firstWorkspace.switchConversation(conversationId), true);
|
||||||
|
await firstKernel.stop();
|
||||||
|
|
||||||
|
const secondKernel = new Kernel().use(
|
||||||
|
createWorkspaceExtension({ home, projectPath }),
|
||||||
|
);
|
||||||
|
await secondKernel.start();
|
||||||
|
|
||||||
|
const secondWorkspace = secondKernel.get<WorkspaceService>(Hook.Workspace);
|
||||||
|
assert.equal(secondWorkspace.conversationId, conversationId);
|
||||||
|
assert.deepEqual(
|
||||||
|
(await secondWorkspace.messages()).map(({ role, content }) => ({ role, content })),
|
||||||
|
[{ role: "user", content: "新对话消息" }],
|
||||||
|
);
|
||||||
|
assert.deepEqual(await readdir(projectPath), []);
|
||||||
|
|
||||||
|
await secondKernel.stop();
|
||||||
|
});
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
# CLI extensions
|
# CLI extensions
|
||||||
|
|
||||||
这里保存 REPL、终端渲染、斜杠命令和 TUI 等 CLI 私有能力。CLI 扩展把终端输入提交给 Kernel,并把 Run 事件转换成终端输出。
|
这里保存终端输入解析、斜杠命令、展示和 TUI 等 CLI 私有能力。CLI Extension 通过公开抓手调用共享能力,并把运行结果转换成终端输出。
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import {
|
|||||||
import type { Extension } from "../../kernel";
|
import type { Extension } from "../../kernel";
|
||||||
import { Event, ExtensionId, Hook } from "../catalog";
|
import { Event, ExtensionId, Hook } from "../catalog";
|
||||||
import type { AgentService } from "../shared/agent";
|
import type { AgentService } from "../shared/agent";
|
||||||
|
import type { WorkspaceService } from "../shared/workspace";
|
||||||
|
import { parseInput } from "./input";
|
||||||
|
|
||||||
export function createCliExtension(): Extension {
|
export function createCliExtension(): Extension {
|
||||||
let terminal: ReadlineInterface | undefined;
|
let terminal: ReadlineInterface | undefined;
|
||||||
@ -19,6 +21,12 @@ export function createCliExtension(): Extension {
|
|||||||
|
|
||||||
start(context) {
|
start(context) {
|
||||||
const agent = context.get<AgentService>(Hook.Agent);
|
const agent = context.get<AgentService>(Hook.Agent);
|
||||||
|
const workspace = context.get<WorkspaceService>(Hook.Workspace);
|
||||||
|
const roleNames = {
|
||||||
|
user: "用户",
|
||||||
|
assistant: "助手",
|
||||||
|
system: "系统",
|
||||||
|
};
|
||||||
terminal = createInterface({
|
terminal = createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout,
|
output: process.stdout,
|
||||||
@ -35,24 +43,59 @@ export function createCliExtension(): Extension {
|
|||||||
loop = (async () => {
|
loop = (async () => {
|
||||||
try {
|
try {
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`llm-to-agent\nWorkspace: ${process.cwd()}\n输入 /exit 退出,Ctrl+C 取消当前回复。\n\n`,
|
`llm-to-agent\nWorkspace: ${process.cwd()}\n当前对话: ${workspace.conversationId}\n命令: /new、/switch <id>、/history、/exit,Ctrl+C 取消当前回复。\n\n`,
|
||||||
);
|
);
|
||||||
terminal?.setPrompt("> ");
|
terminal?.setPrompt("> ");
|
||||||
terminal?.prompt();
|
terminal?.prompt();
|
||||||
|
|
||||||
for await (const line of terminal!) {
|
for await (const line of terminal!) {
|
||||||
const input = line.trim();
|
const input = parseInput(line);
|
||||||
|
|
||||||
if (input === "/exit") break;
|
|
||||||
if (!input) {
|
if (!input) {
|
||||||
terminal?.prompt();
|
terminal?.prompt();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.type === "command") {
|
||||||
|
if (input.name === "exit") break;
|
||||||
|
|
||||||
|
if (input.name === "new") {
|
||||||
|
const conversationId = await workspace.newConversation();
|
||||||
|
process.stdout.write(`已新建对话: ${conversationId}\n\n`);
|
||||||
|
} else if (input.name === "switch") {
|
||||||
|
if (!input.argument) {
|
||||||
|
process.stdout.write("用法: /switch <conversation-id>\n\n");
|
||||||
|
} else if (await workspace.switchConversation(input.argument)) {
|
||||||
|
process.stdout.write(`已切换到对话: ${workspace.conversationId}\n\n`);
|
||||||
|
} else {
|
||||||
|
process.stdout.write(`对话不存在: ${input.argument}\n\n`);
|
||||||
|
}
|
||||||
|
} else if (input.name === "history") {
|
||||||
|
const messages = await workspace.messages();
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
process.stdout.write("当前对话暂无消息。\n\n");
|
||||||
|
} else {
|
||||||
|
process.stdout.write(`对话 ${workspace.conversationId}\n`);
|
||||||
|
for (const message of messages) {
|
||||||
|
process.stdout.write(
|
||||||
|
`${roleNames[message.role]}: ${message.content}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
process.stdout.write("\n");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.stdout.write(`未知命令: /${input.name}\n\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal?.prompt();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
currentRequest = new AbortController();
|
currentRequest = new AbortController();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const chunk of agent.chat(input, currentRequest.signal)) {
|
for await (const chunk of agent.chat(input.content, currentRequest.signal)) {
|
||||||
process.stdout.write(chunk);
|
process.stdout.write(chunk);
|
||||||
}
|
}
|
||||||
process.stdout.write("\n\n");
|
process.stdout.write("\n\n");
|
||||||
|
|||||||
26
src/extensions/cli/input.test.ts
Normal file
26
src/extensions/cli/input.test.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { parseInput } from "./input";
|
||||||
|
|
||||||
|
test("parses CLI messages and commands", () => {
|
||||||
|
assert.equal(parseInput(" "), undefined);
|
||||||
|
assert.deepEqual(parseInput(" 你好 "), {
|
||||||
|
type: "message",
|
||||||
|
content: "你好",
|
||||||
|
});
|
||||||
|
assert.deepEqual(parseInput("/new\tdemo"), {
|
||||||
|
type: "command",
|
||||||
|
name: "new",
|
||||||
|
argument: "demo",
|
||||||
|
});
|
||||||
|
assert.deepEqual(parseInput("/switch default"), {
|
||||||
|
type: "command",
|
||||||
|
name: "switch",
|
||||||
|
argument: "default",
|
||||||
|
});
|
||||||
|
assert.deepEqual(parseInput("//path"), {
|
||||||
|
type: "message",
|
||||||
|
content: "/path",
|
||||||
|
});
|
||||||
|
});
|
||||||
19
src/extensions/cli/input.ts
Normal file
19
src/extensions/cli/input.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
type ParsedInput =
|
||||||
|
| { type: "message"; content: string }
|
||||||
|
| { type: "command"; name: string; argument: string };
|
||||||
|
|
||||||
|
export function parseInput(value: string): ParsedInput | undefined {
|
||||||
|
const input = value.trim();
|
||||||
|
|
||||||
|
if (!input) return;
|
||||||
|
if (!input.startsWith("/")) return { type: "message", content: input };
|
||||||
|
if (input.startsWith("//")) {
|
||||||
|
return { type: "message", content: input.slice(1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = input.search(/\s/);
|
||||||
|
const name = input.slice(1, separator < 0 ? undefined : separator).toLowerCase();
|
||||||
|
const argument = separator < 0 ? "" : input.slice(separator + 1).trim();
|
||||||
|
|
||||||
|
return { type: "command", name, argument };
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash, randomUUID } from "node:crypto";
|
||||||
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
|
|
||||||
@ -21,6 +21,8 @@ export interface WorkspaceService {
|
|||||||
conversationId: string;
|
conversationId: string;
|
||||||
messages(): Promise<ChatMessage[]>;
|
messages(): Promise<ChatMessage[]>;
|
||||||
append(role: MessageRole, content: string): Promise<ChatMessage>;
|
append(role: MessageRole, content: string): Promise<ChatMessage>;
|
||||||
|
newConversation(): Promise<string>;
|
||||||
|
switchConversation(conversationId: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceOptions {
|
export interface WorkspaceOptions {
|
||||||
@ -35,20 +37,42 @@ export function createWorkspaceExtension(options: WorkspaceOptions = {}): Extens
|
|||||||
);
|
);
|
||||||
const projectPath = resolve(options.projectPath ?? process.cwd());
|
const projectPath = resolve(options.projectPath ?? process.cwd());
|
||||||
const workspaceId = createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
|
const workspaceId = createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
|
||||||
const conversationId = options.conversationId ?? "default";
|
|
||||||
const workspaceDirectory = join(home, "workspaces", workspaceId);
|
const workspaceDirectory = join(home, "workspaces", workspaceId);
|
||||||
const conversationDirectory = join(workspaceDirectory, "conversations", conversationId);
|
const conversationsDirectory = join(workspaceDirectory, "conversations");
|
||||||
const messagesFile = join(conversationDirectory, "messages.jsonl");
|
const workspaceFile = join(workspaceDirectory, "workspace.json");
|
||||||
|
let conversationId = options.conversationId ?? "default";
|
||||||
|
|
||||||
|
async function saveWorkspace() {
|
||||||
|
await writeFile(
|
||||||
|
workspaceFile,
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
id: workspaceId,
|
||||||
|
kind: "linked",
|
||||||
|
projectPath,
|
||||||
|
activeConversationId: conversationId,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const workspace: WorkspaceService = {
|
const workspace: WorkspaceService = {
|
||||||
home,
|
home,
|
||||||
projectPath,
|
projectPath,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
conversationId,
|
get conversationId() {
|
||||||
|
return conversationId;
|
||||||
|
},
|
||||||
|
|
||||||
async messages() {
|
async messages() {
|
||||||
try {
|
try {
|
||||||
const content = await readFile(messagesFile, "utf8");
|
const content = await readFile(
|
||||||
|
join(conversationsDirectory, conversationId, "messages.jsonl"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
return content
|
return content
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@ -66,30 +90,57 @@ export function createWorkspaceExtension(options: WorkspaceOptions = {}): Extens
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await appendFile(messagesFile, `${JSON.stringify(message)}\n`, "utf8");
|
await appendFile(
|
||||||
|
join(conversationsDirectory, conversationId, "messages.jsonl"),
|
||||||
|
`${JSON.stringify(message)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async newConversation() {
|
||||||
|
const newConversationId = randomUUID();
|
||||||
|
await mkdir(join(conversationsDirectory, newConversationId), { recursive: true });
|
||||||
|
conversationId = newConversationId;
|
||||||
|
await saveWorkspace();
|
||||||
|
return conversationId;
|
||||||
|
},
|
||||||
|
|
||||||
|
async switchConversation(nextConversationId) {
|
||||||
|
const conversations = await readdir(conversationsDirectory, {
|
||||||
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
const exists = conversations.some(
|
||||||
|
(entry) => entry.isDirectory() && entry.name === nextConversationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) return false;
|
||||||
|
|
||||||
|
conversationId = nextConversationId;
|
||||||
|
await saveWorkspace();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: ExtensionId.Workspace,
|
id: ExtensionId.Workspace,
|
||||||
|
|
||||||
async setup(context) {
|
async setup(context) {
|
||||||
await mkdir(conversationDirectory, { recursive: true });
|
await mkdir(workspaceDirectory, { recursive: true });
|
||||||
await writeFile(
|
|
||||||
join(workspaceDirectory, "workspace.json"),
|
if (!options.conversationId) {
|
||||||
`${JSON.stringify(
|
try {
|
||||||
{
|
const savedWorkspace = JSON.parse(await readFile(workspaceFile, "utf8"));
|
||||||
id: workspaceId,
|
if (savedWorkspace.activeConversationId) {
|
||||||
kind: "linked",
|
conversationId = savedWorkspace.activeConversationId;
|
||||||
projectPath,
|
}
|
||||||
activeConversationId: conversationId,
|
} catch (error) {
|
||||||
},
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||||
null,
|
}
|
||||||
2,
|
}
|
||||||
)}\n`,
|
|
||||||
"utf8",
|
await mkdir(join(conversationsDirectory, conversationId), { recursive: true });
|
||||||
);
|
await saveWorkspace();
|
||||||
|
|
||||||
context.add(Hook.Workspace, workspace);
|
context.add(Hook.Workspace, workspace);
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user