diff --git a/.agents/skills/write-agent-blog/SKILL.md b/.agents/skills/write-agent-blog/SKILL.md new file mode 100644 index 0000000..ed953f7 --- /dev/null +++ b/.agents/skills/write-agent-blog/SKILL.md @@ -0,0 +1,125 @@ +--- +name: write-agent-blog +description: 为 llm-to-agent 的《Agent进阶专题》系列创作中文文章,并在项目 blog 目录输出单个 Markdown 文件。当用户要求为某次提交、代码变更、小功能点、路线图事项、里程碑或实现过程撰写、生成、修改博客或文章时使用,包括“为本次提交写篇文章”“给这个功能写博客”“记录这次实现”等表达。 +--- + +# 创作 Agent 系列博客 + +为《Agent进阶专题》系列创作一篇以项目事实为依据的 Hexo 文章,并将成果保存为 `<项目根目录>/blog/` 下的单个 Markdown 文件。 + +## 工作流程 + +1. 使用 `git rev-parse --show-toplevel` 定位项目根目录。 +2. 确定用户要求的写作范围。对于“本次提交”等表达,使用 `git show` 检查 `HEAD`;用户明确指向尚未提交的工作时,还要检查工作区变更。 +3. 阅读相关实现、测试、包脚本、路线图事项和功能文档。阅读已有的 `blog/*.md`,延续编号、术语和内容深度,并避免重复。 +4. 确定文章对应的单个小功能点、所属阶段(如 `P1`、`P2`)、阶段内篇号、标题和仅含 ASCII 小写字符的短横线式别名。 +5. 以 `assets/article-template.md` 为写作骨架。替换所有占位内容并删除模板说明。 +6. 如果 `blog/` 不存在则创建它。除非用户明确要求修改已有文章,否则只新建一个 `.md` 文件。 +7. 检查元信息、文件名、永久链接、``、事实陈述、代码片段和 Git 差异。除非用户明确要求,否则不要创建提交。 + +## 事实依据 + +- 将源代码、测试和实际差异视为事实依据;使用文档和提交信息理解设计意图与背景。 +- 只描述选定变更已经体现的行为。明确标注计划和未来工作,不得将其写成已经实现的能力。 +- 不得虚构调试经历、性能数据、设计争论、执行命令、命令输出或用户反馈。 +- 优先选用能够揭示核心思路的短代码片段。确保片段与仓库内容一致,并在附近正文中注明仓库相对路径。 +- 如果目标提交主要是脚手架、配置或文档,应解释这项基础工作的目的和后续价值,但不得夸大现有能力。 + +## 文件名与元信息 + +文件名使用 `2026-PX-0X-xxx.md` 格式: + +- 除非用户修改约定,否则本系列固定使用四位年份 `2026`。 +- 将 `PX` 替换为路线图阶段,例如 `P1`。 +- 将 `0X` 替换为该阶段内的两位文章序号,从 `01` 开始。 +- 将 `xxx` 替换为描述该功能的简短英文别名,只使用小写 ASCII 字母、数字和短横线。 +- 示例:`2026-P1-03-streaming-chat.md`。 +- 根据已有文章推断下一个未使用的序号。如果无法可靠判断所属阶段,应暂停并询问用户,不要自行猜测。 +- 不得覆盖已有文件。如果目标名称已经存在,先判断它是否对应同一功能;否则递增序号。 + +严格使用以下元信息结构: + +```yaml +--- +title: 【Agent进阶专题】中文标题 +tags: [AI, Agent] +categories: +- AI +date: YYYY-MM-DD HH:mm:ss +permalink: /agent/YYYY/MM/slug/ +--- +``` + +- 新建文件时使用当前本地日期和时间。 +- 标题应具体并体现结果,不使用章节编号,也不要使用“一些思考”之类含糊标题。 +- 文件名和永久链接使用相同的英文别名。 +- 永久链接中的 `YYYY/MM` 从 `date` 推导。 + +## 文章固定结构 + +保持以下三部分顺序: + +1. Hexo 元信息。 +2. 位于 `` 之前的简洁简介,用于列表页摘要。 +3. 位于 `` 之后的完整正文。 + +使用自然且能够说明内容的标题,不要直接使用“简介部分”或“正文部分”等模板化标题。简介由一至两个短段落组成,需要说明问题、此次交付的能力及其价值。 + +正文应围绕具体功能组织,不要机械套用完全相同的标题。根据主题只选择必要内容,不要求每篇全部覆盖: + +- 变更前存在的问题或限制; +- 当前小功能点的目标与边界; +- 核心设计或理解模型; +- 配合精选代码片段说明实现路径; +- 验证方式与可观察结果; +- 取舍、当前限制和下一项自然演进能力; +- 用简短结语将本篇内容连接到整个系列的演进主线。 + +- 导读、概念介绍和设计点题类文章建议控制在 1000 个中文字符左右;这是保持简洁的参考值,不是硬性上限。根据章节范围和必要信息调整篇幅,优先保证主题讲清且没有无关扩展。 +- 标题给出的主题就是内容边界。只讲清当前主题,不借机扩展相关协议、实现细节或远期能力。 + +## 读者视角与系列衔接 + +- 面向第一次接触本项目的外部读者写作,不要把作者已经掌握的项目背景当作读者常识。 +- 阶段编号和文章编号用于组织系列,不是正文的叙事前提。首次出现 `P0`、`P1` 等编号时必须用自然语言解释其含义;如果编号对理解当前内容没有帮助,就不要在正文中使用。 +- 每篇文章先从读者能够理解的场景、问题或一次自然的需求变化切入,再逐步引出项目术语和设计结论。 +- 用“原本有什么—遇到什么问题—做出什么选择”推进文章,但不要为了讲故事虚构场景或增加文学化铺垫。 +- 不得使用“如前所述”“大家已经知道”等措辞假设读者读过其他文章。即使文章位于系列中间,也应提供理解当前主题所需的最少背景。 +- 相邻文章应各守边界,避免提前讲完后续主题。结尾只需自然提出下一篇的问题,不要罗列尚未解释的阶段和术语。 +- 当前导读部分暂定为:`P0-00` 项目介绍与目录导航、`P0-01` 架构设计、`P0-02` 功能划分、`P0-03` 阶段规划、`P0-04` 目录规划。创作其中一篇时,不要侵占其他篇目的主要内容。 + +## 系列文风 + +- 使用清晰的简体中文,面向理解 TypeScript 和基本 LLM 概念、但可能刚接触 Agent 工程的开发者。 +- 直接、口语化、克制。先给结论,再补必要原因;能用一句话说清的内容不要扩成一段。 +- 删除不推动观点的过渡句、重复总结、设问和修饰语。每个段落只表达一个重点,通常不超过三句话。 +- 优先使用具体动词和短句,避免“我们需要先回答一个不那么显眼、却会影响整个项目的问题”一类绕弯表达。 +- 适度使用“我们”营造共同实践感,重点仍应放在技术推理和可复现的工程过程上。 +- 按照“动机—原理—实现—验证”的顺序展开。在进入密集实现细节前,先解释为什么这样做。 +- 保持段落简短、标题明确,并与项目文档使用一致的术语。 +- 技术术语首次出现时给出简要解释。代码中的英文标识符保持原样;不要为同一概念反复更换译名。 +- 避免营销语言、泛泛的 AI 背景介绍、夸张结论、填充式总结和流水账式叙述。 +- 每篇文章都应能够独立阅读,同时在概念上承接前一项能力并引出下一项能力。 +- 用最短的具体情境引出矛盾和选择,避免从项目内部术语或文档摘要直接起笔,也避免把技术文章写成散文。 +- 不为追求完整而增加章节。点题类文章通常使用两至四个短章节即可。 + +## Markdown 与代码 + +- 使用标准 Markdown,围栏代码块必须标注语言。 +- 正文标题从 `##` 开始;页面标题由 Hexo 元信息提供,不要在正文中重复一级标题。 +- 标题、列表、代码块和 `` 前后保留空行。 +- 代码片段应尽量短且与主题直接相关。只有在省略范围明显且不会造成误解时才使用 `...`。 +- 展示命令时,应区分命令和示例输出。只有命令确实存在或实际执行过,才能将其作为验证命令写入文章。 +- 除非用户明确要求,否则不要添加目录。 + +## 最终检查 + +回复用户前确认: + +- 只新增或修改了一篇目标 Markdown 文章。 +- 文件名符合 `^2026-P[0-9]+-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$`。 +- 元信息结构有效;除非用户要求,否则只包含本系列规定的字段。 +- 有意义的简介之后恰好出现一次 ``。 +- 没有遗留任何占位内容。 +- 事实陈述和代码片段符合当前仓库状态。 +- 最终回复包含文章文件链接,并用一句话说明文章主题。 diff --git a/.agents/skills/write-agent-blog/agents/openai.yaml b/.agents/skills/write-agent-blog/agents/openai.yaml new file mode 100644 index 0000000..4e4915a --- /dev/null +++ b/.agents/skills/write-agent-blog/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Agent 专题博客创作" + short_description: "根据提交与项目文档创作统一风格的 Hexo Markdown 文章" + default_prompt: "使用 $write-agent-blog 为本次提交写一篇文章。" diff --git a/.agents/skills/write-agent-blog/assets/article-template.md b/.agents/skills/write-agent-blog/assets/article-template.md new file mode 100644 index 0000000..5b32b80 --- /dev/null +++ b/.agents/skills/write-agent-blog/assets/article-template.md @@ -0,0 +1,40 @@ +--- +title: 【Agent进阶专题】{{中文标题}} +tags: [AI, Agent] +categories: +- AI +date: {{年-月-日 时:分:秒}} +permalink: /agent/{{四位年份}}/{{两位月份}}/{{英文别名}}/ +--- + +{{面向首次接触项目的读者,从一个具体场景或问题切入,用一至两个短段落说明本篇主题及其价值,作为 Hexo 列表页摘要。}} + + + +## {{问题或限制}} + +{{说明需要解决的具体问题或此前存在的限制。}} + +## {{目标与边界}} + +{{说明本篇交付什么,以及刻意暂不处理什么。}} + +## {{核心设计或理解模型}} + +{{先解释核心思路,再进入实现细节。}} + +## {{实现过程}} + +{{配合精选代码片段说明关键实现路径。}} + +## {{验证结果}} + +{{说明实际使用的验证方式和可观察结果。}} + +## {{取舍与下一步}} + +{{记录当前限制、实现取舍和下一项自然演进能力。}} + +## {{结语}} + +{{将这个小功能点连接到从 LLM 走向 Agent 的系列主线。}} diff --git a/.gitignore b/.gitignore index 4c5307c..04fae43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -node_modules +node_modules/ .env -pnpm-lock.yaml .DS_Store -tmp \ No newline at end of file +tmp/ +.agent/ +.llm-to-agent \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bdbf8b3 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# llm-to-agent + +个人使用、本地优先的开发助手与电脑管家。 + +项目坚持简单的 `Kernel + Extensions`:Kernel 只提供运行机制,具体能力全部由 Extension 实现;所有专属数据集中在用户目录,不写入绑定的项目。 + +运行 CLI: + +```bash +pnpm dev +``` + +启动时自动加载项目根目录的 `.env`。 + +文档: + +- [架构总览](docs/architecture.md) +- [源码目录](docs/source-layout.md) +- [数据目录](docs/data-layout.md) +- [Extension 目录](docs/extensions.md) +- [路线图](docs/roadmap.md) diff --git a/docs/architecture.md b/docs/architecture.md index 1440fff..21115f6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,16 +18,16 @@ Kernel 管“Extension 怎样连接和运行”,Extension 管“系统具体 ## Kernel -Kernel 不认识 Workspace、Conversation、Run、Agent、模型或 Tool,只提供所有 Extension 共同需要的机制: +第一版 Kernel 只保留几件直观的事: -- 生命周期与资源清理; -- 单个能力的提供与调用; -- 多个实现的注册与选择; -- 事实事件的发布与监听; -- 隔离存储、取消和错误传播; -- 副作用能力共用的调用接缝。 +- 按顺序装入 Extension; +- 依次执行 `setup` 和 `start`,停止时倒序执行 `stop`; +- 通过命名抓手提供和取得能力; +- 通过命名事件发布和监听事实。 -只有所有 Extension 都必须遵守的运行规则才进入 Kernel。 +抓手和事件暂时使用带命名空间的字符串,不为它们建立复杂的 TypeScript 类型系统。Setup Context 负责注册,Runtime Context 负责使用;Kernel 信任 Extension 遵守生命周期,不增加 Context 失效检查和不可变包装。 + +Kernel 不认识 Workspace、Conversation、Run、Agent、模型或 Tool。只有所有 Extension 都必须遵守的运行规则才进入 Kernel。 ## Extensions diff --git a/docs/data-layout.md b/docs/data-layout.md index 0f17dcd..67f4861 100644 --- a/docs/data-layout.md +++ b/docs/data-layout.md @@ -29,6 +29,9 @@ 目录按需要创建,不规定尚未实现的数据文件和格式。 +当前对话链路只会创建 Workspace 绑定信息和 +`conversations/default/messages.jsonl`,后续能力再增加自己的数据。 + ## Workspace - `managed` Workspace 的工作文件位于自己的 `files/`。 @@ -43,7 +46,7 @@ /workspaces// ``` -项目中不创建 `.agent/`、`.llm-to-agent/` 或其他专属元数据。项目移动时更新绑定关系;删除 linked Workspace 绝不能删除外部项目源码。 +项目中不创建 `.llm-to-agent/` 或其他专属元数据。项目移动时更新绑定关系;删除 linked Workspace 绝不能删除外部项目源码。 ## 数据规则 diff --git a/docs/extensions.md b/docs/extensions.md index 272006a..6d82603 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -6,6 +6,7 @@ ```text src/extensions/ +├── catalog.ts ├── shared/ │ ├── workspace/ │ ├── agent/ @@ -20,6 +21,17 @@ src/extensions/ 第一版只静态装配这四个 Extension。 +`catalog.ts` 集中列出所有跨 Extension 使用的 Extension ID、能力抓手和事实事件;它只保存字符串,不保存类型或运行逻辑。 + +当前协作链路: + +```text +workspace -> 提供 workspace +deepseek -> 添加 model.providers +agent -> 使用二者并提供 agent +cli -> 调用 agent +``` + ## 协作 - 一个明确能力由 Extension 提供,调用方取得后直接调用。 diff --git a/packages/old/knowledge/llm-to-agent-guide.md b/legacy/old/knowledge/llm-to-agent-guide.md similarity index 100% rename from packages/old/knowledge/llm-to-agent-guide.md rename to legacy/old/knowledge/llm-to-agent-guide.md diff --git a/packages/old/package.json b/legacy/old/package.json similarity index 100% rename from packages/old/package.json rename to legacy/old/package.json diff --git a/packages/old/src/agents/agent.ts b/legacy/old/src/agents/agent.ts similarity index 100% rename from packages/old/src/agents/agent.ts rename to legacy/old/src/agents/agent.ts diff --git a/packages/old/src/agents/index.ts b/legacy/old/src/agents/index.ts similarity index 100% rename from packages/old/src/agents/index.ts rename to legacy/old/src/agents/index.ts diff --git a/packages/old/src/agents/manager.ts b/legacy/old/src/agents/manager.ts similarity index 100% rename from packages/old/src/agents/manager.ts rename to legacy/old/src/agents/manager.ts diff --git a/packages/old/src/agents/tools.ts b/legacy/old/src/agents/tools.ts similarity index 100% rename from packages/old/src/agents/tools.ts rename to legacy/old/src/agents/tools.ts diff --git a/packages/old/src/context.ts b/legacy/old/src/context.ts similarity index 100% rename from packages/old/src/context.ts rename to legacy/old/src/context.ts diff --git a/packages/old/src/hooks/index.ts b/legacy/old/src/hooks/index.ts similarity index 100% rename from packages/old/src/hooks/index.ts rename to legacy/old/src/hooks/index.ts diff --git a/packages/old/src/hooks/log.hooks.ts b/legacy/old/src/hooks/log.hooks.ts similarity index 100% rename from packages/old/src/hooks/log.hooks.ts rename to legacy/old/src/hooks/log.hooks.ts diff --git a/packages/old/src/hooks/registry.ts b/legacy/old/src/hooks/registry.ts similarity index 100% rename from packages/old/src/hooks/registry.ts rename to legacy/old/src/hooks/registry.ts diff --git a/packages/old/src/index.ts b/legacy/old/src/index.ts similarity index 100% rename from packages/old/src/index.ts rename to legacy/old/src/index.ts diff --git a/packages/old/src/knowledge/knowledge-base.ts b/legacy/old/src/knowledge/knowledge-base.ts similarity index 100% rename from packages/old/src/knowledge/knowledge-base.ts rename to legacy/old/src/knowledge/knowledge-base.ts diff --git a/packages/old/src/knowledge/search-tool.ts b/legacy/old/src/knowledge/search-tool.ts similarity index 100% rename from packages/old/src/knowledge/search-tool.ts rename to legacy/old/src/knowledge/search-tool.ts diff --git a/packages/old/src/llm.ts b/legacy/old/src/llm.ts similarity index 100% rename from packages/old/src/llm.ts rename to legacy/old/src/llm.ts diff --git a/packages/old/src/prompts/orchestrator.ts b/legacy/old/src/prompts/orchestrator.ts similarity index 100% rename from packages/old/src/prompts/orchestrator.ts rename to legacy/old/src/prompts/orchestrator.ts diff --git a/packages/old/src/prompts/reAct.ts b/legacy/old/src/prompts/reAct.ts similarity index 100% rename from packages/old/src/prompts/reAct.ts rename to legacy/old/src/prompts/reAct.ts diff --git a/packages/old/src/prompts/system.ts b/legacy/old/src/prompts/system.ts similarity index 100% rename from packages/old/src/prompts/system.ts rename to legacy/old/src/prompts/system.ts diff --git a/packages/old/src/tools/calculator.ts b/legacy/old/src/tools/calculator.ts similarity index 100% rename from packages/old/src/tools/calculator.ts rename to legacy/old/src/tools/calculator.ts diff --git a/packages/old/src/tools/guess.ts b/legacy/old/src/tools/guess.ts similarity index 100% rename from packages/old/src/tools/guess.ts rename to legacy/old/src/tools/guess.ts diff --git a/packages/old/src/tools/registry.ts b/legacy/old/src/tools/registry.ts similarity index 100% rename from packages/old/src/tools/registry.ts rename to legacy/old/src/tools/registry.ts diff --git a/packages/old/src/tools/weather.ts b/legacy/old/src/tools/weather.ts similarity index 100% rename from packages/old/src/tools/weather.ts rename to legacy/old/src/tools/weather.ts diff --git a/packages/old/src/types/index.ts b/legacy/old/src/types/index.ts similarity index 100% rename from packages/old/src/types/index.ts rename to legacy/old/src/types/index.ts diff --git a/packages/cli/tsconfig.json b/legacy/old/tsconfig.json similarity index 100% rename from packages/cli/tsconfig.json rename to legacy/old/tsconfig.json diff --git a/package.json b/package.json index 7597174..c21eb9c 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,17 @@ "private": true, "type": "module", "scripts": { - "dev": "pnpm --filter @llm-to-agent/cli dev", - "dev:old": "pnpm --filter @llm-to-agent/old dev", - "dev:core": "pnpm --filter @llm-to-agent/core dev", - "dev:server": "pnpm --filter @llm-to-agent/server dev", - "dev:web": "pnpm --filter @llm-to-agent/web dev", - "dev:desktop": "pnpm --filter @llm-to-agent/desktop dev", - "test": "pnpm --filter @llm-to-agent/tests test" + "dev": "node --import tsx src/main.ts cli", + "dev:runtime": "node --import tsx src/main.ts cli web desktop", + "dev:cli": "node --import tsx src/main.ts cli", + "dev:web": "node --import tsx src/main.ts web", + "dev:desktop": "node --import tsx src/main.ts desktop", + "typecheck": "tsc --noEmit", + "test": "node --import tsx --test \"src/**/*.test.ts\"" }, "devDependencies": { "@types/node": "^25.9.1", "tsx": "^4.x", "typescript": "^5.x" } -} \ No newline at end of file +} diff --git a/packages/cli/package.json b/packages/cli/package.json deleted file mode 100644 index 04314a7..0000000 --- a/packages/cli/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@llm-to-agent/cli", - "version": "0.1.0", - "type": "module", - "private": true, - "main": "./src/index.ts", - "scripts": { - "dev": "tsx src/index.ts" - }, - "dependencies": { - "@llm-to-agent/core": "workspace:*", - "@llm-to-agent/plugins-builtin": "workspace:*" - }, - "devDependencies": { - "@types/node": "^25.9.1", - "tsx": "^4.x", - "typescript": "^5.x" - } -} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts deleted file mode 100644 index ad1964e..0000000 --- a/packages/cli/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log('🚀 LLM-to-Agent CLI 初始化已完成,待开发。\n'); diff --git a/packages/core/package.json b/packages/core/package.json deleted file mode 100644 index 0a7ee4e..0000000 --- a/packages/core/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@llm-to-agent/core", - "version": "0.1.0", - "type": "module", - "private": true, - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "dev": "tsx src/index.ts" - }, - "dependencies": { - "@llm-to-agent/types": "workspace:*" - }, - "devDependencies": { - "@types/node": "^25.9.1", - "tsx": "^4.x", - "typescript": "^5.x" - } -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts deleted file mode 100644 index 817e4a2..0000000 --- a/packages/core/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from '@llm-to-agent/types'; - -export const init = () => { - console.log('🚀 Core 入口已初始化完成,待开发。'); -} - diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json deleted file mode 100644 index f27ff83..0000000 --- a/packages/core/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "module": "esnext", - "moduleResolution": "bundler", - "types": ["node"] - } -} diff --git a/packages/desktop/package.json b/packages/desktop/package.json deleted file mode 100644 index a2f5e4e..0000000 --- a/packages/desktop/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@llm-to-agent/desktop", - "version": "0.1.0", - "type": "module", - "private": true, - "main": "./src/main.ts", - "scripts": { - "dev": "tsx './src/main.ts'" - }, - "dependencies": { - "@llm-to-agent/core": "workspace:*", - "@llm-to-agent/plugins-builtin": "workspace:*" - }, - "devDependencies": { - "@types/node": "^25.9.1", - "tsx": "^4.x", - "typescript": "^5.x" - } -} diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts deleted file mode 100644 index 1c84edc..0000000 --- a/packages/desktop/src/main.ts +++ /dev/null @@ -1 +0,0 @@ -console.log('🚀 Desktop 入口已初始化完成,待开发。'); diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json deleted file mode 100644 index f27ff83..0000000 --- a/packages/desktop/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "module": "esnext", - "moduleResolution": "bundler", - "types": ["node"] - } -} diff --git a/packages/old/tsconfig.json b/packages/old/tsconfig.json deleted file mode 100644 index f27ff83..0000000 --- a/packages/old/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "module": "esnext", - "moduleResolution": "bundler", - "types": ["node"] - } -} diff --git a/packages/plugins-builtin/package.json b/packages/plugins-builtin/package.json deleted file mode 100644 index b5dcd57..0000000 --- a/packages/plugins-builtin/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "@llm-to-agent/plugins-builtin", - "version": "0.1.0", - "type": "module", - "private": true, - "main": "./src/index.ts", - "dependencies": { - "@llm-to-agent/core": "workspace:*", - "@llm-to-agent/types": "workspace:*" - }, - "devDependencies": { - "@types/node": "^25.9.1", - "tsx": "^4.x", - "typescript": "^5.x" - } -} diff --git a/packages/plugins-builtin/src/index.ts b/packages/plugins-builtin/src/index.ts deleted file mode 100644 index 56004c9..0000000 --- a/packages/plugins-builtin/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export default {} \ No newline at end of file diff --git a/packages/plugins-builtin/tsconfig.json b/packages/plugins-builtin/tsconfig.json deleted file mode 100644 index f27ff83..0000000 --- a/packages/plugins-builtin/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "module": "esnext", - "moduleResolution": "bundler", - "types": ["node"] - } -} diff --git a/packages/server/package.json b/packages/server/package.json deleted file mode 100644 index 6e16493..0000000 --- a/packages/server/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@llm-to-agent/server", - "version": "0.1.0", - "type": "module", - "private": true, - "main": "./src/index.ts", - "scripts": { - "dev": "tsx src/index.ts" - }, - "dependencies": { - "@llm-to-agent/core": "workspace:*", - "@llm-to-agent/plugins-builtin": "workspace:*" - }, - "devDependencies": { - "@types/node": "^25.9.1", - "tsx": "^4.x", - "typescript": "^5.x" - } -} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts deleted file mode 100644 index 49a6c64..0000000 --- a/packages/server/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log('🚀 Server 入口已初始化完成,待开发。'); diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json deleted file mode 100644 index f27ff83..0000000 --- a/packages/server/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "module": "esnext", - "moduleResolution": "bundler", - "types": ["node"] - } -} diff --git a/packages/tests/package.json b/packages/tests/package.json deleted file mode 100644 index a6ee9b9..0000000 --- a/packages/tests/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@llm-to-agent/tests", - "version": "0.1.0", - "type": "module", - "private": true, - "scripts": { - "test": "tsx --test src/**/*.test.ts" - }, - "dependencies": { - "@llm-to-agent/types": "workspace:*", - "@llm-to-agent/core": "workspace:*", - "@llm-to-agent/plugins-builtin": "workspace:*" - }, - "devDependencies": { - "@types/node": "^25.9.1", - "tsx": "^4.x", - "typescript": "^5.x" - } -} diff --git a/packages/tests/src/core.test.ts b/packages/tests/src/core.test.ts deleted file mode 100644 index 716ded6..0000000 --- a/packages/tests/src/core.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -import { init } from '@llm-to-agent/core'; - -describe('@llm-to-agent/core', () => { - - it('init() 不抛异常', () => { - assert.doesNotThrow(() => init()); - }); - - it('core 通过 re-export 暴露 types', async () => { - const mod = await import('@llm-to-agent/core'); - assert.equal(typeof mod.init, 'function', '应导出 init 函数'); - }); - -}); diff --git a/packages/tests/src/plugins-builtin.test.ts b/packages/tests/src/plugins-builtin.test.ts deleted file mode 100644 index c7cc2fe..0000000 --- a/packages/tests/src/plugins-builtin.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -describe('@llm-to-agent/plugins-builtin', () => { - - it('入口文件可正常 import', async () => { - const mod = await import('@llm-to-agent/plugins-builtin'); - assert.ok(mod.default !== undefined); - }); - -}); diff --git a/packages/tests/src/types.test.ts b/packages/tests/src/types.test.ts deleted file mode 100644 index 54a4fb7..0000000 --- a/packages/tests/src/types.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -import { Message, ToolCall, ToolDef, PluginManifest, SchedulerConfig, EventName, Listener } from '@llm-to-agent/types'; - -describe('@llm-to-agent/types', () => { - - it('Message 类型可正常构造', () => { - const msg: Message = { role: 'user', content: 'hello' }; - assert.equal(msg.role, 'user'); - assert.equal(msg.content, 'hello'); - }); - - it('Message 支持 tool_calls 和 tool_call_id', () => { - const tc: ToolCall = { id: '1', function: { name: 'test', arguments: '{}' } }; - const msg: Message = { role: 'assistant', content: '', tool_calls: [tc] }; - assert.equal(msg.tool_calls![0].id, '1'); - }); - - it('ToolDef 可正常构造', () => { - const tool: ToolDef = { - type: 'function', - function: { - name: 'weather', - description: '查询天气', - parameters: { type: 'object', properties: {}, required: [] }, - }, - }; - assert.equal(tool.function.name, 'weather'); - }); - - it('PluginManifest 类型完整', () => { - const manifest: PluginManifest = { - name: '@agent/test', - version: '1.0.0', - type: 'tool', - provides: ['test-tool'], - entry: './index.ts', - platforms: ['cli', 'desktop'], - }; - assert.equal(manifest.type, 'tool'); - assert.deepEqual(manifest.platforms, ['cli', 'desktop']); - }); - - it('SchedulerConfig 可正常构造', () => { - const config: SchedulerConfig = { maxSteps: 5 }; - assert.equal(config.maxSteps, 5); - }); - - it('EventName 和 Listener 类型定义正确', () => { - const name: EventName = 'llm:call'; - const fn: Listener = (data: any) => data; - assert.equal(typeof fn, 'function'); - assert.equal(name, 'llm:call'); - }); - -}); diff --git a/packages/tests/tsconfig.json b/packages/tests/tsconfig.json deleted file mode 100644 index f27ff83..0000000 --- a/packages/tests/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "module": "esnext", - "moduleResolution": "bundler", - "types": ["node"] - } -} diff --git a/packages/types/package.json b/packages/types/package.json deleted file mode 100644 index 9844c38..0000000 --- a/packages/types/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "@llm-to-agent/types", - "version": "0.1.0", - "type": "module", - "private": true, - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": {}, - "devDependencies": { - "typescript": "^5.x" - } -} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts deleted file mode 100644 index 99f958c..0000000 --- a/packages/types/src/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -// ===== Agent 消息 ===== - -export interface Message { - role: 'system' | 'user' | 'assistant' | 'tool'; - content: string; - tool_calls?: ToolCall[]; - tool_call_id?: string; -} - -export interface ToolCall { - id: string; - function: { name: string; arguments: string }; -} - -// ===== 工具 ===== - -export interface ToolDef { - type: 'function'; - function: { - name: string; - description: string; - parameters: Record; - }; -} - -// ===== 插件 ===== - -export interface PluginManifest { - name: string; - version: string; - type: 'provider' | 'tool' | 'hook' | 'prompt'; - provides: string | string[]; - entry: string; - platforms?: ('cli' | 'desktop' | 'web')[]; -} - -// ===== 事件总线 ===== - -export type EventName = string; -export type Listener = (data: any) => any | Promise; - -// ===== 调度器 ===== - -export interface SchedulerConfig { - maxSteps?: number; -} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json deleted file mode 100644 index efdc208..0000000 --- a/packages/types/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "module": "esnext", - "moduleResolution": "bundler", - "types": [] - } -} diff --git a/packages/web/index.html b/packages/web/index.html deleted file mode 100644 index b2cd966..0000000 --- a/packages/web/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - LLM-to-Agent Web - - -
-

🚀 LLM-to-Agent Web

-

占位页面,待实现 React 聊天界面。

-
- - diff --git a/packages/web/package.json b/packages/web/package.json deleted file mode 100644 index cf96ca1..0000000 --- a/packages/web/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@llm-to-agent/web", - "version": "0.1.0", - "type": "module", - "private": true, - "scripts": { - "dev": "echo 'TODO: vite dev'", - "build": "echo 'TODO: vite build'" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..ea3bef0 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,329 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^25.9.1 + version: 25.9.5 + tsx: + specifier: ^4.x + version: 4.23.1 + typescript: + specifier: ^5.x + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + +snapshots: + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@types/node@25.9.5': + dependencies: + undici-types: 7.24.6 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + fsevents@2.3.3: + optional: true + + tsx@4.23.1: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@7.24.6: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 18ec407..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - - 'packages/*' diff --git a/src/extensions/README.md b/src/extensions/README.md new file mode 100644 index 0000000..077c884 --- /dev/null +++ b/src/extensions/README.md @@ -0,0 +1,10 @@ +# Extensions + +除 Kernel 的稳定运行机制外,所有具体能力都在这里实现。 + +- `shared/` 保存不依赖产品界面的公共能力; +- `cli/`、`web/`、`desktop/` 保存三条产品线各自的输入、展示、协议和系统集成; +- 产品线私有扩展之间不能互相依赖;两个产品需要的实现应提升到 `shared/`; +- Kernel 不导入本目录中的任何具体扩展。 + +新增扩展时直接实现 `Extension`。只有真实能力开始开发时才创建对应源码目录,不再预建空 package 或 `export {}` 文件。 diff --git a/src/extensions/catalog.ts b/src/extensions/catalog.ts new file mode 100644 index 0000000..41638b1 --- /dev/null +++ b/src/extensions/catalog.ts @@ -0,0 +1,7 @@ +export const Event = { + RuntimeStopRequested: "runtime.stop.requested", + RunStarted: "run.started", + RunCompleted: "run.completed", + RunFailed: "run.failed", + MessageAdded: "message.added", +}; diff --git a/src/extensions/cli/README.md b/src/extensions/cli/README.md new file mode 100644 index 0000000..748f59c --- /dev/null +++ b/src/extensions/cli/README.md @@ -0,0 +1,3 @@ +# CLI extensions + +这里保存 REPL、终端渲染、斜杠命令和 TUI 等 CLI 私有能力。CLI 扩展把终端输入提交给 Kernel,并把 Run 事件转换成终端输出。 diff --git a/src/extensions/desktop/README.md b/src/extensions/desktop/README.md new file mode 100644 index 0000000..5e6bb43 --- /dev/null +++ b/src/extensions/desktop/README.md @@ -0,0 +1,3 @@ +# Desktop extensions + +这里保存原生桥接、窗口、托盘、快捷键、通知与更新等 Desktop 私有能力。与 Web 共用的界面逻辑应放入 `extensions/shared/`。 diff --git a/src/extensions/shared/README.md b/src/extensions/shared/README.md new file mode 100644 index 0000000..79b7593 --- /dev/null +++ b/src/extensions/shared/README.md @@ -0,0 +1,5 @@ +# Shared extensions + +这里保存模型、Workspace、Shell、Git、记忆、规划、多 Agent、浏览器、自动化和 System Evolution 等跨产品能力。 + +共享扩展的源码只依赖 Kernel 契约;需要协作时通过其他共享扩展注册的公开能力运行,不能认识 CLI、Web、Desktop 的界面与传输细节。 diff --git a/src/extensions/web/README.md b/src/extensions/web/README.md new file mode 100644 index 0000000..275c7a5 --- /dev/null +++ b/src/extensions/web/README.md @@ -0,0 +1,3 @@ +# Web extensions + +这里保存 HTTP 接入、事件流、路由与 Web 页面等 Web 私有能力。Web 扩展负责在网络交互和 Kernel 运行接口之间转换。 diff --git a/src/kernel/events.ts b/src/kernel/events.ts new file mode 100644 index 0000000..339511e --- /dev/null +++ b/src/kernel/events.ts @@ -0,0 +1,35 @@ +export type EventHandler = (payload: T) => void | Promise; +export type Unsubscribe = () => void; + +export class EventBus { + #listeners = new Map>>(); + + on(event: string, handler: EventHandler): Unsubscribe { + const listeners = this.#listeners.get(event) ?? new Set(); + listeners.add(handler); + this.#listeners.set(event, listeners); + + return () => { + listeners.delete(handler); + if (listeners.size === 0) this.#listeners.delete(event); + }; + } + + async emit(event: string, payload: T): Promise { + const errors: unknown[] = []; + + for (const listener of this.#listeners.get(event) ?? []) { + try { + await listener(payload); + } catch (error) { + errors.push(error); + } + } + + return errors; + } + + clear(): void { + this.#listeners.clear(); + } +} diff --git a/src/kernel/extension.ts b/src/kernel/extension.ts new file mode 100644 index 0000000..d0c0b50 --- /dev/null +++ b/src/kernel/extension.ts @@ -0,0 +1,24 @@ +import type { EventHandler, Unsubscribe } from "./events"; + +export type Awaitable = T | Promise; + +export interface ExtensionSetupContext { + add(name: string, value: T): void; + on(event: string, handler: EventHandler): Unsubscribe; +} + +export interface ExtensionRuntimeContext { + get(name: string): T; + all(name: string): T[]; + on(event: string, handler: EventHandler): Unsubscribe; + emit(event: string, payload: T): Promise; +} + +export interface Extension { + id: string; + setup(context: ExtensionSetupContext): Awaitable; + start?(context: ExtensionRuntimeContext): Awaitable; + stop?(context: ExtensionRuntimeContext): Awaitable; +} + +export type ExtensionFactory = () => Extension; diff --git a/src/kernel/index.ts b/src/kernel/index.ts new file mode 100644 index 0000000..4d8ae2d --- /dev/null +++ b/src/kernel/index.ts @@ -0,0 +1,4 @@ +export * from "./events"; +export * from "./extension"; +export * from "./kernel"; +export * from "./registry"; diff --git a/src/kernel/kernel.test.ts b/src/kernel/kernel.test.ts new file mode 100644 index 0000000..d3fb59d --- /dev/null +++ b/src/kernel/kernel.test.ts @@ -0,0 +1,136 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { EventBus, Kernel, type Extension } from "./index"; + +test("sets up every extension before starting them and stops in reverse order", async () => { + const calls: string[] = []; + + const first: Extension = { + id: "first", + setup(context) { + calls.push("first.setup"); + context.add("test.values", "first"); + }, + start(context) { + calls.push(`first.start:${context.all("test.values").join(",")}`); + }, + stop() { + calls.push("first.stop"); + }, + }; + + const second: Extension = { + id: "second", + setup(context) { + calls.push("second.setup"); + context.add("test.values", "second"); + }, + start(context) { + calls.push(`second.start:${context.all("test.values").join(",")}`); + }, + stop() { + calls.push("second.stop"); + }, + }; + + const kernel = new Kernel().use(first, second); + await kernel.start(); + await kernel.stop(); + + assert.deepEqual(calls, [ + "first.setup", + "second.setup", + "first.start:first,second", + "second.start:first,second", + "second.stop", + "first.stop", + ]); +}); + +test("rejects duplicate extension ids without partially installing a batch", () => { + const first: Extension = { id: "first", setup() {} }; + const duplicate: Extension = { id: "first", setup() {} }; + const kernel = new Kernel(); + + assert.throws(() => kernel.use(first, duplicate), /already installed/); + assert.deepEqual(kernel.installedExtensionIds, []); +}); + +test("gets one value or collects multiple values", async () => { + const kernel = new Kernel().use({ + id: "values", + setup(context) { + context.add("single", "one"); + context.add("multiple", "one"); + context.add("multiple", "two"); + }, + }); + + await kernel.start(); + + assert.equal(kernel.get("single"), "one"); + assert.deepEqual(kernel.all("multiple"), ["one", "two"]); + assert.throws(() => kernel.get("missing"), /found 0/); + assert.throws(() => kernel.get("multiple"), /found 2/); +}); + +test("clears registrations when setup fails", async () => { + const kernel = new Kernel().use({ + id: "broken-setup", + setup(context) { + context.add("temporary", "value"); + throw new Error("setup failed"); + }, + }); + + await assert.rejects(kernel.start(), /setup failed/); + + assert.equal(kernel.state, "stopped"); + assert.deepEqual(kernel.all("temporary"), []); +}); + +test("stops every active extension after cleanup errors", async () => { + const calls: string[] = []; + const kernel = new Kernel().use( + { + id: "first", + setup() {}, + stop() { + calls.push("first.stop"); + }, + }, + { + id: "second", + setup() {}, + stop() { + calls.push("second.stop"); + throw new Error("cleanup failed"); + }, + }, + ); + + await kernel.start(); + await assert.rejects(kernel.stop(), /failed to stop/); + + assert.equal(kernel.state, "stopped"); + assert.deepEqual(calls, ["second.stop", "first.stop"]); +}); + +test("publishes events to every listener and reports listener errors", async () => { + const events = new EventBus(); + const received: string[] = []; + + events.on("test.delivery", () => { + throw new Error("listener failed"); + }); + events.on("test.delivery", (payload) => { + received.push(payload); + }); + + const errors = await events.emit("test.delivery", "delivered"); + + assert.deepEqual(received, ["delivered"]); + assert.equal(errors.length, 1); + assert.match(String(errors[0]), /listener failed/); +}); diff --git a/src/kernel/kernel.ts b/src/kernel/kernel.ts new file mode 100644 index 0000000..7faaef0 --- /dev/null +++ b/src/kernel/kernel.ts @@ -0,0 +1,156 @@ +import { EventBus, type EventHandler, type Unsubscribe } from "./events"; +import type { + Extension, + ExtensionRuntimeContext, + ExtensionSetupContext, +} from "./extension"; +import { ExtensionRegistry } from "./registry"; + +export class Kernel { + #extensions: Extension[] = []; + #activeExtensions: Extension[] = []; + #registry = new ExtensionRegistry(); + #events = new EventBus(); + #setupContext: ExtensionSetupContext; + #runtimeContext: ExtensionRuntimeContext; + #state = "created"; + + constructor() { + const registry = this.#registry; + const events = this.#events; + + this.#setupContext = { + add(name, value) { + registry.add(name, value); + }, + on(event, handler) { + return events.on(event, handler); + }, + }; + + this.#runtimeContext = { + get(name) { + return registry.get(name); + }, + all(name) { + return registry.all(name); + }, + on(event, handler) { + return events.on(event, handler); + }, + emit(event, payload) { + return events.emit(event, payload); + }, + }; + } + + get state(): string { + return this.#state; + } + + get installedExtensionIds(): string[] { + return this.#extensions.map((extension) => extension.id); + } + + use(...extensions: Extension[]): this { + if (this.#state !== "created") { + throw new Error(`Cannot install extensions while kernel is ${this.#state}.`); + } + + const ids = new Set(this.#extensions.map((extension) => extension.id)); + + for (const extension of extensions) { + if (ids.has(extension.id)) { + throw new Error(`Extension "${extension.id}" is already installed.`); + } + ids.add(extension.id); + } + + this.#extensions.push(...extensions); + return this; + } + + get(name: string): T { + return this.#registry.get(name); + } + + all(name: string): T[] { + return this.#registry.all(name); + } + + on(event: string, handler: EventHandler): Unsubscribe { + return this.#events.on(event, handler); + } + + emit(event: string, payload: T): Promise { + return this.#events.emit(event, payload); + } + + async start(): Promise { + if (this.#state !== "created") { + throw new Error(`Cannot start kernel while it is ${this.#state}.`); + } + + this.#state = "starting"; + + try { + for (const extension of this.#extensions) { + await extension.setup(this.#setupContext); + } + + for (const extension of this.#extensions) { + this.#activeExtensions.push(extension); + await extension.start?.(this.#runtimeContext); + } + + this.#state = "running"; + } catch (error) { + const cleanupErrors = await this.#stopActiveExtensions(); + this.#events.clear(); + this.#registry.clear(); + this.#state = "stopped"; + + if (cleanupErrors.length > 0) { + throw new AggregateError( + [error, ...cleanupErrors], + "Kernel failed to start and one or more extensions failed to clean up.", + ); + } + + throw error; + } + } + + async stop(): Promise { + if (this.#state === "stopped") return; + + if (this.#state !== "running") { + throw new Error(`Cannot stop kernel while it is ${this.#state}.`); + } + + this.#state = "stopping"; + const errors = await this.#stopActiveExtensions(); + this.#events.clear(); + this.#registry.clear(); + this.#state = "stopped"; + + if (errors.length > 0) { + throw new AggregateError(errors, "One or more extensions failed to stop."); + } + } + + async #stopActiveExtensions(): Promise { + const errors: unknown[] = []; + + for (const extension of [...this.#activeExtensions].reverse()) { + try { + await extension.stop?.(this.#runtimeContext); + } catch (error) { + errors.push(error); + } + } + + this.#activeExtensions = []; + return errors; + } +} diff --git a/src/kernel/registry.ts b/src/kernel/registry.ts new file mode 100644 index 0000000..db86037 --- /dev/null +++ b/src/kernel/registry.ts @@ -0,0 +1,31 @@ +export class ExtensionRegistry { + #values = new Map(); + + add(name: string, value: T): void { + const values = this.#values.get(name); + + if (values) { + values.push(value); + } else { + this.#values.set(name, [value]); + } + } + + get(name: string): T { + const values = this.all(name); + + if (values.length !== 1) { + throw new Error(`"${name}" needs exactly one value, found ${values.length}.`); + } + + return values[0] as T; + } + + all(name: string): T[] { + return [...(this.#values.get(name) ?? [])] as T[]; + } + + clear(): void { + this.#values.clear(); + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..9f79375 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,31 @@ +import { existsSync } from "node:fs"; +import { loadEnvFile } from "node:process"; + +import { Event } from "./extensions/catalog"; +import { Kernel } from "./kernel"; +import { getProduct, sharedExtensions } from "./products"; + +if (existsSync(".env")) { + loadEnvFile(); +} + +const productIds = process.argv.slice(2); +const products = (productIds.length > 0 ? productIds : ["cli"]).map(getProduct); +const factories = [ + ...sharedExtensions, + ...products.flatMap((product) => product.extensions), +]; +const kernel = new Kernel().use(...factories.map((factory) => factory())); + +const stopped = new Promise((resolve) => { + kernel.on(Event.RuntimeStopRequested, resolve); + process.once("SIGINT", resolve); + process.once("SIGTERM", resolve); +}); + +try { + await kernel.start(); + await stopped; +} finally { + await kernel.stop(); +} diff --git a/src/products/cli.ts b/src/products/cli.ts new file mode 100644 index 0000000..bc757d2 --- /dev/null +++ b/src/products/cli.ts @@ -0,0 +1,6 @@ +import type { ProductDefinition } from "./product"; + +export const cliProduct: ProductDefinition = { + id: "cli", + extensions: [], +}; diff --git a/src/products/desktop.ts b/src/products/desktop.ts new file mode 100644 index 0000000..c879471 --- /dev/null +++ b/src/products/desktop.ts @@ -0,0 +1,6 @@ +import type { ProductDefinition } from "./product"; + +export const desktopProduct: ProductDefinition = { + id: "desktop", + extensions: [], +}; diff --git a/src/products/index.ts b/src/products/index.ts new file mode 100644 index 0000000..466b5ec --- /dev/null +++ b/src/products/index.ts @@ -0,0 +1,22 @@ +import { cliProduct } from "./cli"; +import { desktopProduct } from "./desktop"; +import { webProduct } from "./web"; + +import type { ProductDefinition, ProductId } from "./product"; + +export * from "./product"; +export { sharedExtensions } from "./shared"; + +const products: Record = { + cli: cliProduct, + web: webProduct, + desktop: desktopProduct, +}; + +export function getProduct(id: string): ProductDefinition { + if (id === "cli" || id === "web" || id === "desktop") { + return products[id]; + } + + throw new Error(`Unknown product "${id}". Expected cli, web, or desktop.`); +} diff --git a/src/products/product.ts b/src/products/product.ts new file mode 100644 index 0000000..eef795d --- /dev/null +++ b/src/products/product.ts @@ -0,0 +1,8 @@ +import type { ExtensionFactory } from "../kernel"; + +export type ProductId = "cli" | "web" | "desktop"; + +export interface ProductDefinition { + id: ProductId; + extensions: ExtensionFactory[]; +} diff --git a/src/products/shared.ts b/src/products/shared.ts new file mode 100644 index 0000000..d7a2c3f --- /dev/null +++ b/src/products/shared.ts @@ -0,0 +1,4 @@ +import type { ExtensionFactory } from "../kernel"; + +// 公共扩展在实现真实能力时加入这里。 +export const sharedExtensions: ExtensionFactory[] = []; diff --git a/src/products/web.ts b/src/products/web.ts new file mode 100644 index 0000000..9b4c390 --- /dev/null +++ b/src/products/web.ts @@ -0,0 +1,6 @@ +import type { ProductDefinition } from "./product"; + +export const webProduct: ProductDefinition = { + id: "web", + extensions: [], +}; diff --git a/tests/e2e/.gitkeep b/tests/e2e/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/e2e/.gitkeep @@ -0,0 +1 @@ + diff --git a/tooling/.gitkeep b/tooling/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tooling/.gitkeep @@ -0,0 +1 @@ + diff --git a/tsconfig.json b/tsconfig.json index 2a77da6..c2832bc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,8 +4,11 @@ "module": "esnext", "moduleResolution": "bundler", "strict": true, + "noUncheckedIndexedAccess": true, "esModuleInterop": true, "skipLibCheck": true, - "types": ["node"] - } -} \ No newline at end of file + "types": ["node"], + "noEmit": true + }, + "include": ["src/**/*.ts"] +}