init: 初始化开发目录
This commit is contained in:
parent
bf8c04526a
commit
c5f3ee5b3f
125
.agents/skills/write-agent-blog/SKILL.md
Normal file
125
.agents/skills/write-agent-blog/SKILL.md
Normal file
@ -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. 检查元信息、文件名、永久链接、`<!-- more -->`、事实陈述、代码片段和 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. 位于 `<!-- more -->` 之前的简洁简介,用于列表页摘要。
|
||||
3. 位于 `<!-- more -->` 之后的完整正文。
|
||||
|
||||
使用自然且能够说明内容的标题,不要直接使用“简介部分”或“正文部分”等模板化标题。简介由一至两个短段落组成,需要说明问题、此次交付的能力及其价值。
|
||||
|
||||
正文应围绕具体功能组织,不要机械套用完全相同的标题。根据主题只选择必要内容,不要求每篇全部覆盖:
|
||||
|
||||
- 变更前存在的问题或限制;
|
||||
- 当前小功能点的目标与边界;
|
||||
- 核心设计或理解模型;
|
||||
- 配合精选代码片段说明实现路径;
|
||||
- 验证方式与可观察结果;
|
||||
- 取舍、当前限制和下一项自然演进能力;
|
||||
- 用简短结语将本篇内容连接到整个系列的演进主线。
|
||||
|
||||
- 导读、概念介绍和设计点题类文章建议控制在 1000 个中文字符左右;这是保持简洁的参考值,不是硬性上限。根据章节范围和必要信息调整篇幅,优先保证主题讲清且没有无关扩展。
|
||||
- 标题给出的主题就是内容边界。只讲清当前主题,不借机扩展相关协议、实现细节或远期能力。
|
||||
|
||||
## 读者视角与系列衔接
|
||||
|
||||
- 面向第一次接触本项目的外部读者写作,不要把作者已经掌握的项目背景当作读者常识。
|
||||
- 阶段编号和文章编号用于组织系列,不是正文的叙事前提。首次出现 `P0`、`P1` 等编号时必须用自然语言解释其含义;如果编号对理解当前内容没有帮助,就不要在正文中使用。
|
||||
- 每篇文章先从读者能够理解的场景、问题或一次自然的需求变化切入,再逐步引出项目术语和设计结论。
|
||||
- 用“原本有什么—遇到什么问题—做出什么选择”推进文章,但不要为了讲故事虚构场景或增加文学化铺垫。
|
||||
- 不得使用“如前所述”“大家已经知道”等措辞假设读者读过其他文章。即使文章位于系列中间,也应提供理解当前主题所需的最少背景。
|
||||
- 相邻文章应各守边界,避免提前讲完后续主题。结尾只需自然提出下一篇的问题,不要罗列尚未解释的阶段和术语。
|
||||
- 当前导读部分暂定为:`P0-00` 项目介绍与目录导航、`P0-01` 架构设计、`P0-02` 功能划分、`P0-03` 阶段规划、`P0-04` 目录规划。创作其中一篇时,不要侵占其他篇目的主要内容。
|
||||
|
||||
## 系列文风
|
||||
|
||||
- 使用清晰的简体中文,面向理解 TypeScript 和基本 LLM 概念、但可能刚接触 Agent 工程的开发者。
|
||||
- 直接、口语化、克制。先给结论,再补必要原因;能用一句话说清的内容不要扩成一段。
|
||||
- 删除不推动观点的过渡句、重复总结、设问和修饰语。每个段落只表达一个重点,通常不超过三句话。
|
||||
- 优先使用具体动词和短句,避免“我们需要先回答一个不那么显眼、却会影响整个项目的问题”一类绕弯表达。
|
||||
- 适度使用“我们”营造共同实践感,重点仍应放在技术推理和可复现的工程过程上。
|
||||
- 按照“动机—原理—实现—验证”的顺序展开。在进入密集实现细节前,先解释为什么这样做。
|
||||
- 保持段落简短、标题明确,并与项目文档使用一致的术语。
|
||||
- 技术术语首次出现时给出简要解释。代码中的英文标识符保持原样;不要为同一概念反复更换译名。
|
||||
- 避免营销语言、泛泛的 AI 背景介绍、夸张结论、填充式总结和流水账式叙述。
|
||||
- 每篇文章都应能够独立阅读,同时在概念上承接前一项能力并引出下一项能力。
|
||||
- 用最短的具体情境引出矛盾和选择,避免从项目内部术语或文档摘要直接起笔,也避免把技术文章写成散文。
|
||||
- 不为追求完整而增加章节。点题类文章通常使用两至四个短章节即可。
|
||||
|
||||
## Markdown 与代码
|
||||
|
||||
- 使用标准 Markdown,围栏代码块必须标注语言。
|
||||
- 正文标题从 `##` 开始;页面标题由 Hexo 元信息提供,不要在正文中重复一级标题。
|
||||
- 标题、列表、代码块和 `<!-- more -->` 前后保留空行。
|
||||
- 代码片段应尽量短且与主题直接相关。只有在省略范围明显且不会造成误解时才使用 `...`。
|
||||
- 展示命令时,应区分命令和示例输出。只有命令确实存在或实际执行过,才能将其作为验证命令写入文章。
|
||||
- 除非用户明确要求,否则不要添加目录。
|
||||
|
||||
## 最终检查
|
||||
|
||||
回复用户前确认:
|
||||
|
||||
- 只新增或修改了一篇目标 Markdown 文章。
|
||||
- 文件名符合 `^2026-P[0-9]+-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$`。
|
||||
- 元信息结构有效;除非用户要求,否则只包含本系列规定的字段。
|
||||
- 有意义的简介之后恰好出现一次 `<!-- more -->`。
|
||||
- 没有遗留任何占位内容。
|
||||
- 事实陈述和代码片段符合当前仓库状态。
|
||||
- 最终回复包含文章文件链接,并用一句话说明文章主题。
|
||||
4
.agents/skills/write-agent-blog/agents/openai.yaml
Normal file
4
.agents/skills/write-agent-blog/agents/openai.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Agent 专题博客创作"
|
||||
short_description: "根据提交与项目文档创作统一风格的 Hexo Markdown 文章"
|
||||
default_prompt: "使用 $write-agent-blog 为本次提交写一篇文章。"
|
||||
40
.agents/skills/write-agent-blog/assets/article-template.md
Normal file
40
.agents/skills/write-agent-blog/assets/article-template.md
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
title: 【Agent进阶专题】{{中文标题}}
|
||||
tags: [AI, Agent]
|
||||
categories:
|
||||
- AI
|
||||
date: {{年-月-日 时:分:秒}}
|
||||
permalink: /agent/{{四位年份}}/{{两位月份}}/{{英文别名}}/
|
||||
---
|
||||
|
||||
{{面向首次接触项目的读者,从一个具体场景或问题切入,用一至两个短段落说明本篇主题及其价值,作为 Hexo 列表页摘要。}}
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## {{问题或限制}}
|
||||
|
||||
{{说明需要解决的具体问题或此前存在的限制。}}
|
||||
|
||||
## {{目标与边界}}
|
||||
|
||||
{{说明本篇交付什么,以及刻意暂不处理什么。}}
|
||||
|
||||
## {{核心设计或理解模型}}
|
||||
|
||||
{{先解释核心思路,再进入实现细节。}}
|
||||
|
||||
## {{实现过程}}
|
||||
|
||||
{{配合精选代码片段说明关键实现路径。}}
|
||||
|
||||
## {{验证结果}}
|
||||
|
||||
{{说明实际使用的验证方式和可观察结果。}}
|
||||
|
||||
## {{取舍与下一步}}
|
||||
|
||||
{{记录当前限制、实现取舍和下一项自然演进能力。}}
|
||||
|
||||
## {{结语}}
|
||||
|
||||
{{将这个小功能点连接到从 LLM 走向 Agent 的系列主线。}}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
node_modules
|
||||
node_modules/
|
||||
.env
|
||||
pnpm-lock.yaml
|
||||
.DS_Store
|
||||
tmp
|
||||
tmp/
|
||||
.agent/
|
||||
.llm-to-agent
|
||||
21
README.md
Normal file
21
README.md
Normal file
@ -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)
|
||||
@ -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
|
||||
|
||||
|
||||
@ -29,6 +29,9 @@
|
||||
|
||||
目录按需要创建,不规定尚未实现的数据文件和格式。
|
||||
|
||||
当前对话链路只会创建 Workspace 绑定信息和
|
||||
`conversations/default/messages.jsonl`,后续能力再增加自己的数据。
|
||||
|
||||
## Workspace
|
||||
|
||||
- `managed` Workspace 的工作文件位于自己的 `files/`。
|
||||
@ -43,7 +46,7 @@
|
||||
<LLM_TO_AGENT_HOME>/workspaces/<workspace-id>/
|
||||
```
|
||||
|
||||
项目中不创建 `.agent/`、`.llm-to-agent/` 或其他专属元数据。项目移动时更新绑定关系;删除 linked Workspace 绝不能删除外部项目源码。
|
||||
项目中不创建 `.llm-to-agent/` 或其他专属元数据。项目移动时更新绑定关系;删除 linked Workspace 绝不能删除外部项目源码。
|
||||
|
||||
## 数据规则
|
||||
|
||||
|
||||
@ -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 提供,调用方取得后直接调用。
|
||||
|
||||
16
package.json
16
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
console.log('🚀 LLM-to-Agent CLI 初始化已完成,待开发。\n');
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
export * from '@llm-to-agent/types';
|
||||
|
||||
export const init = () => {
|
||||
console.log('🚀 Core 入口已初始化完成,待开发。');
|
||||
}
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
console.log('🚀 Desktop 入口已初始化完成,待开发。');
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export default {}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
console.log('🚀 Server 入口已初始化完成,待开发。');
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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 函数');
|
||||
});
|
||||
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 插件 =====
|
||||
|
||||
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<any>;
|
||||
|
||||
// ===== 调度器 =====
|
||||
|
||||
export interface SchedulerConfig {
|
||||
maxSteps?: number;
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": []
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>LLM-to-Agent Web</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>🚀 LLM-to-Agent Web</h1>
|
||||
<p>占位页面,待实现 React 聊天界面。</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -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'"
|
||||
}
|
||||
}
|
||||
329
pnpm-lock.yaml
generated
Normal file
329
pnpm-lock.yaml
generated
Normal file
@ -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: {}
|
||||
@ -1,2 +0,0 @@
|
||||
packages:
|
||||
- 'packages/*'
|
||||
10
src/extensions/README.md
Normal file
10
src/extensions/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Extensions
|
||||
|
||||
除 Kernel 的稳定运行机制外,所有具体能力都在这里实现。
|
||||
|
||||
- `shared/` 保存不依赖产品界面的公共能力;
|
||||
- `cli/`、`web/`、`desktop/` 保存三条产品线各自的输入、展示、协议和系统集成;
|
||||
- 产品线私有扩展之间不能互相依赖;两个产品需要的实现应提升到 `shared/`;
|
||||
- Kernel 不导入本目录中的任何具体扩展。
|
||||
|
||||
新增扩展时直接实现 `Extension`。只有真实能力开始开发时才创建对应源码目录,不再预建空 package 或 `export {}` 文件。
|
||||
7
src/extensions/catalog.ts
Normal file
7
src/extensions/catalog.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const Event = {
|
||||
RuntimeStopRequested: "runtime.stop.requested",
|
||||
RunStarted: "run.started",
|
||||
RunCompleted: "run.completed",
|
||||
RunFailed: "run.failed",
|
||||
MessageAdded: "message.added",
|
||||
};
|
||||
3
src/extensions/cli/README.md
Normal file
3
src/extensions/cli/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# CLI extensions
|
||||
|
||||
这里保存 REPL、终端渲染、斜杠命令和 TUI 等 CLI 私有能力。CLI 扩展把终端输入提交给 Kernel,并把 Run 事件转换成终端输出。
|
||||
3
src/extensions/desktop/README.md
Normal file
3
src/extensions/desktop/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Desktop extensions
|
||||
|
||||
这里保存原生桥接、窗口、托盘、快捷键、通知与更新等 Desktop 私有能力。与 Web 共用的界面逻辑应放入 `extensions/shared/`。
|
||||
5
src/extensions/shared/README.md
Normal file
5
src/extensions/shared/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Shared extensions
|
||||
|
||||
这里保存模型、Workspace、Shell、Git、记忆、规划、多 Agent、浏览器、自动化和 System Evolution 等跨产品能力。
|
||||
|
||||
共享扩展的源码只依赖 Kernel 契约;需要协作时通过其他共享扩展注册的公开能力运行,不能认识 CLI、Web、Desktop 的界面与传输细节。
|
||||
3
src/extensions/web/README.md
Normal file
3
src/extensions/web/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Web extensions
|
||||
|
||||
这里保存 HTTP 接入、事件流、路由与 Web 页面等 Web 私有能力。Web 扩展负责在网络交互和 Kernel 运行接口之间转换。
|
||||
35
src/kernel/events.ts
Normal file
35
src/kernel/events.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export type EventHandler<T> = (payload: T) => void | Promise<void>;
|
||||
export type Unsubscribe = () => void;
|
||||
|
||||
export class EventBus {
|
||||
#listeners = new Map<string, Set<EventHandler<any>>>();
|
||||
|
||||
on<T>(event: string, handler: EventHandler<T>): 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<T>(event: string, payload: T): Promise<unknown[]> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
24
src/kernel/extension.ts
Normal file
24
src/kernel/extension.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { EventHandler, Unsubscribe } from "./events";
|
||||
|
||||
export type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
export interface ExtensionSetupContext {
|
||||
add<T>(name: string, value: T): void;
|
||||
on<T>(event: string, handler: EventHandler<T>): Unsubscribe;
|
||||
}
|
||||
|
||||
export interface ExtensionRuntimeContext {
|
||||
get<T>(name: string): T;
|
||||
all<T>(name: string): T[];
|
||||
on<T>(event: string, handler: EventHandler<T>): Unsubscribe;
|
||||
emit<T>(event: string, payload: T): Promise<unknown[]>;
|
||||
}
|
||||
|
||||
export interface Extension {
|
||||
id: string;
|
||||
setup(context: ExtensionSetupContext): Awaitable<void>;
|
||||
start?(context: ExtensionRuntimeContext): Awaitable<void>;
|
||||
stop?(context: ExtensionRuntimeContext): Awaitable<void>;
|
||||
}
|
||||
|
||||
export type ExtensionFactory = () => Extension;
|
||||
4
src/kernel/index.ts
Normal file
4
src/kernel/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./events";
|
||||
export * from "./extension";
|
||||
export * from "./kernel";
|
||||
export * from "./registry";
|
||||
136
src/kernel/kernel.test.ts
Normal file
136
src/kernel/kernel.test.ts
Normal file
@ -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<string>("test.delivery", () => {
|
||||
throw new Error("listener failed");
|
||||
});
|
||||
events.on<string>("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/);
|
||||
});
|
||||
156
src/kernel/kernel.ts
Normal file
156
src/kernel/kernel.ts
Normal file
@ -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<T>(name: string): T {
|
||||
return this.#registry.get(name);
|
||||
}
|
||||
|
||||
all<T>(name: string): T[] {
|
||||
return this.#registry.all(name);
|
||||
}
|
||||
|
||||
on<T>(event: string, handler: EventHandler<T>): Unsubscribe {
|
||||
return this.#events.on(event, handler);
|
||||
}
|
||||
|
||||
emit<T>(event: string, payload: T): Promise<unknown[]> {
|
||||
return this.#events.emit(event, payload);
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
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<void> {
|
||||
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<unknown[]> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
31
src/kernel/registry.ts
Normal file
31
src/kernel/registry.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export class ExtensionRegistry {
|
||||
#values = new Map<string, unknown[]>();
|
||||
|
||||
add<T>(name: string, value: T): void {
|
||||
const values = this.#values.get(name);
|
||||
|
||||
if (values) {
|
||||
values.push(value);
|
||||
} else {
|
||||
this.#values.set(name, [value]);
|
||||
}
|
||||
}
|
||||
|
||||
get<T>(name: string): T {
|
||||
const values = this.all<T>(name);
|
||||
|
||||
if (values.length !== 1) {
|
||||
throw new Error(`"${name}" needs exactly one value, found ${values.length}.`);
|
||||
}
|
||||
|
||||
return values[0] as T;
|
||||
}
|
||||
|
||||
all<T>(name: string): T[] {
|
||||
return [...(this.#values.get(name) ?? [])] as T[];
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#values.clear();
|
||||
}
|
||||
}
|
||||
31
src/main.ts
Normal file
31
src/main.ts
Normal file
@ -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<void>((resolve) => {
|
||||
kernel.on(Event.RuntimeStopRequested, resolve);
|
||||
process.once("SIGINT", resolve);
|
||||
process.once("SIGTERM", resolve);
|
||||
});
|
||||
|
||||
try {
|
||||
await kernel.start();
|
||||
await stopped;
|
||||
} finally {
|
||||
await kernel.stop();
|
||||
}
|
||||
6
src/products/cli.ts
Normal file
6
src/products/cli.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { ProductDefinition } from "./product";
|
||||
|
||||
export const cliProduct: ProductDefinition = {
|
||||
id: "cli",
|
||||
extensions: [],
|
||||
};
|
||||
6
src/products/desktop.ts
Normal file
6
src/products/desktop.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { ProductDefinition } from "./product";
|
||||
|
||||
export const desktopProduct: ProductDefinition = {
|
||||
id: "desktop",
|
||||
extensions: [],
|
||||
};
|
||||
22
src/products/index.ts
Normal file
22
src/products/index.ts
Normal file
@ -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<ProductId, ProductDefinition> = {
|
||||
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.`);
|
||||
}
|
||||
8
src/products/product.ts
Normal file
8
src/products/product.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { ExtensionFactory } from "../kernel";
|
||||
|
||||
export type ProductId = "cli" | "web" | "desktop";
|
||||
|
||||
export interface ProductDefinition {
|
||||
id: ProductId;
|
||||
extensions: ExtensionFactory[];
|
||||
}
|
||||
4
src/products/shared.ts
Normal file
4
src/products/shared.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import type { ExtensionFactory } from "../kernel";
|
||||
|
||||
// 公共扩展在实现真实能力时加入这里。
|
||||
export const sharedExtensions: ExtensionFactory[] = [];
|
||||
6
src/products/web.ts
Normal file
6
src/products/web.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { ProductDefinition } from "./product";
|
||||
|
||||
export const webProduct: ProductDefinition = {
|
||||
id: "web",
|
||||
extensions: [],
|
||||
};
|
||||
1
tests/e2e/.gitkeep
Normal file
1
tests/e2e/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
1
tooling/.gitkeep
Normal file
1
tooling/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
@ -4,8 +4,11 @@
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
"types": ["node"],
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user