diff --git a/.gitignore b/.gitignore index 696e5b3..38733f1 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,8 @@ htmlcov/ # Database *.db *.sqlite3 +skillos/skillos/storage/embedding_cache/ +**/storage/embedding_cache/ # Docker .dockerignore @@ -54,5 +56,7 @@ htmlcov/ *.bak idea.txt +skillos/skillos/storage/skill_repo/SkillStorage +**/storage/skill_repo/SkillStorage # Runtime SkillStorage repository skillos/skillos/storage/skill_repo/SkillStorage/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b862b74 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/anthropic-skills"] + path = vendor/anthropic-skills + url = https://github.com/anthropics/skills.git diff --git a/docs/modules/01-skill-repository.md b/docs/modules/01-skill-repository.md index ee5aef4..d32707e 100644 --- a/docs/modules/01-skill-repository.md +++ b/docs/modules/01-skill-repository.md @@ -84,6 +84,24 @@ async def get_stats() -> Dict[str, Any] 第一阶段只保证基础边、子图、依赖链、执行顺序和统计可用;根据 `sub_skill_ids` 自动建边、相似关系发现、复杂图分析放到后续阶段。 +### Heterogeneous Graph + +当前开发主线已经从“只包含 Skill 节点的同质图”升级为“异构知识图”。旧的 `SkillEdge` / `SkillSubgraph` 仍保留,用来兼容执行层、治理层和已有测试;新的 demo 图会把以下实体作为一等节点保存: + +- `skill`:抽象出的可执行 / 可复用 Skill。 +- `task`:用户任务或历史任务。 +- `trajectory`:操作轨迹来源。 +- `document`:说明文档或操作规范。 +- `api_doc`:API 文档或接口说明。 +- `tool`:外部工具或系统能力。 +- `script`:代码脚本来源。 +- `test`:测试样例或验证结果。 +- `version`:Skill 版本节点。 +- `feedback`:执行反馈、反思或演化建议。 +- `agent`:系统内管理图和生命周期的 Agent。 + +异构边使用独立关系类型,例如 `derived_from`、`uses`、`requires`、`composes_with`、`verified_by`、`evolves_from`、`version_of`、`feeds_back_to`。启动 demo 时会通过静态脚本 seed 一组 source → skill → version/test/tool/feedback 的图数据,后续可以逐步替换为 Extractor / Normalizer / Summarizer / Indexer Agents 的真实输出。 + ## API 端点 现有 API 路径保持不变: diff --git a/docs/modules/04-self-management-agents.md b/docs/modules/04-self-management-agents.md index 70c2c1a..4d9d73b 100644 --- a/docs/modules/04-self-management-agents.md +++ b/docs/modules/04-self-management-agents.md @@ -146,6 +146,34 @@ EVENT_TYPES = { } ``` +### 固定输入 Pipeline 的内部 Agent 管理 + +当前 demo 已将 Knowledge Import 的 `parse-and-create` 接入 Self-Management Agents,而不是由 API 路由直接写 Skill / Graph。 + +固定研究输入的内部链路为: + +``` +ExperiencePipeline + Extractor → Normalizer → Summarizer → Indexer + │ + ▼ +MetaControllerAgent.manage_ingested_unit() + │ + ├── SkillBuilderAgent.build_from_experience_unit() + │ 从结构化 experience unit 生成 S1 Candidate Skill + │ + ├── SkillAuditorAgent.audit() + │ 本地规则审计 schema / safety,demo 固定输入不强制走 LLM + │ 通过后推动 Skill: S1 Candidate → S2 Draft → S3 Verified + │ + └── SkillLibrarianAgent + register_new() / update() + index_ingested_unit_graph() + 写入 source、tool、api_doc、test、version、skill 节点和关系边 +``` + +API 响应会返回 `agent_trace`,前端 Knowledge Import 页面用它展示内部 Agent 管理过程。 + --- ## 12 个 Meta-Skills(Strategic L3) diff --git a/docs/modules/05-frontend-api-pipeline.md b/docs/modules/05-frontend-api-pipeline.md index d441a8d..61fa805 100644 --- a/docs/modules/05-frontend-api-pipeline.md +++ b/docs/modules/05-frontend-api-pipeline.md @@ -36,7 +36,7 @@ | `/` | `Dashboard.tsx` | 系统概览:核心指标、类型分布、健康报告、Self-Evolution Metrics、实时事件 | | `/demo` | `SelfEvolutionDemo.tsx` | **EMNLP 核心展示**:完整自演化闭环(检索→规划→执行→记录→学习) | | `/wiki` | `SkillWiki.tsx` | Skill 目录:搜索、过滤、详情抽屉、发布/废弃操作 | -| `/graph` | `SkillGraph.tsx` | 交互式知识图谱(AntV G6):节点/边类型过滤、子图展开 | +| `/graph` | `SkillGraph.tsx` | 交互式异构知识图谱(AntV G6):Source / Skill / Tool / API / Test / Version / Feedback 节点展示与关系过滤 | | `/execution` | `AgentExecution.tsx` | 任务执行:检索到的 Skill 展示、步骤结果、经验记录反馈 | | `/evolution` | `Evolution.tsx` | 健康监控:degraded/critical Skill 列表、修复、演化周期 | | `/lifecycle` | `LifecycleDemo.tsx` | 状态机可视化:S0-S7 转换演示 | @@ -157,8 +157,8 @@ class AppState: | 方法 | 路径 | 功能 | |------|------|------| -| GET | `/graph` | 完整图谱 | -| POST | `/graph/subgraph` | 子图 | +| GET | `/graph` | 完整异构图谱;内存 demo 优先返回 source / skill / tool / api_doc / test / version / feedback 等节点 | +| POST | `/graph/subgraph` | 指定 Skill 周边的异构子图 | | POST | `/graph/edges` | 添加边 | | GET | `/graph/{id}/dependencies` | 依赖链 | | GET | `/graph/{id}/execution-order` | 执行顺序 | @@ -216,6 +216,8 @@ class AppState: | `api_doc` | API 文档/OpenAPI 规范 | REST API 描述 | | `script` | 代码脚本 | Python/JavaScript 函数 | +当前 demo 优先支持固定研究输入:前端 Knowledge Import 页面提供静态 JSON fixture,后端 pipeline 会跳过真实采集,直接解析 `skills[]` 数组并生成结构化经验单元。`parse-and-create` 现在通过 `MetaControllerAgent` 调度 `SkillBuilderAgent`、`SkillAuditorAgent` 和 `SkillLibrarianAgent`,创建或复用 Skill,并同步写入异构图节点与关系,包括 source、tool、api_doc、test、version 和 Skill 节点。响应中的 `agent_trace` 用于前端展示内部 Agent 管理过程。 + ### 关键数据结构 ```python diff --git a/skillos-frontend/package-lock.json b/skillos-frontend/package-lock.json index 553806d..b00171d 100644 --- a/skillos-frontend/package-lock.json +++ b/skillos-frontend/package-lock.json @@ -16,6 +16,7 @@ "framer-motion": "^12.38.0", "react": "^19.2.5", "react-dom": "^19.2.5", + "react-force-graph-2d": "^1.29.1", "react-router-dom": "^7.14.2", "zustand": "^5.0.12" }, @@ -1954,9 +1955,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1974,9 +1972,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1994,9 +1989,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2014,9 +2006,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2034,9 +2023,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2054,9 +2040,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2143,6 +2126,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2639,6 +2628,15 @@ } } }, + "node_modules/accessor-fn": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", + "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", @@ -2792,6 +2790,16 @@ "node": ">=6.0.0" } }, + "node_modules/bezier-js": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", + "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" + } + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -2888,6 +2896,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-color-tracker": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", + "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==", + "license": "MIT", + "dependencies": { + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", @@ -3052,6 +3072,19 @@ "node": ">=12" } }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dsv": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", @@ -3259,6 +3292,15 @@ "node": ">=12" } }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", @@ -3304,6 +3346,41 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dagre": { "version": "0.8.5", "resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz", @@ -3736,6 +3813,20 @@ "dev": true, "license": "ISC" }, + "node_modules/float-tooltip": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", + "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", + "license": "MIT", + "dependencies": { + "d3-selection": "2 - 3", + "kapsule": "^1.16", + "preact": "10" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/flru": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/flru/-/flru-1.0.2.tgz", @@ -3765,6 +3856,32 @@ } } }, + "node_modules/force-graph": { + "version": "1.51.4", + "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.4.tgz", + "integrity": "sha512-TdJ2KbkoiDQ7NIRx8IPGD0mAXXpLhamS7c+b7W98b0MHG7lphnda1VOQX/98UDTsttIAdH4TcP0l0MauSnLK8w==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "18 - 25", + "accessor-fn": "1", + "bezier-js": "3 - 6", + "canvas-color-tracker": "^1.3", + "d3-array": "1 - 3", + "d3-drag": "2 - 3", + "d3-force-3d": "2 - 3", + "d3-scale": "1 - 4", + "d3-scale-chromatic": "1 - 3", + "d3-selection": "2 - 3", + "d3-zoom": "2 - 3", + "float-tooltip": "^1.7", + "index-array-by": "1", + "kapsule": "^1.16", + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", @@ -4033,6 +4150,15 @@ "node": ">=0.8.19" } }, + "node_modules/index-array-by": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", + "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", @@ -4090,11 +4216,19 @@ "dev": true, "license": "ISC" }, + "node_modules/jerrypick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", + "integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/jsesc": { @@ -4153,6 +4287,18 @@ "node": ">=6" } }, + "node_modules/kapsule": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", + "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", + "license": "MIT", + "dependencies": { + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", @@ -4320,9 +4466,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4344,9 +4487,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4368,9 +4508,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4392,9 +4529,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4472,6 +4606,24 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4622,6 +4774,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -4753,6 +4914,16 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4763,6 +4934,23 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -4803,12 +4991,44 @@ "react": "^19.2.5" } }, + "node_modules/react-force-graph-2d": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.29.1.tgz", + "integrity": "sha512-1Rl/1Z3xy2iTHKj6a0jRXGyiI86xUti81K+jBQZ+Oe46csaMikp47L5AjrzA9hY9fNGD63X8ffrqnvaORukCuQ==", + "license": "MIT", + "dependencies": { + "force-graph": "^1.51", + "prop-types": "15", + "react-kapsule": "^2.5" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-kapsule": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.7.tgz", + "integrity": "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==", + "license": "MIT", + "dependencies": { + "jerrypick": "^1.1.1" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-router": { "version": "7.14.2", "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.14.2.tgz", @@ -5051,6 +5271,12 @@ "node": ">=12.22" } }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", diff --git a/skillos-frontend/package.json b/skillos-frontend/package.json index 79bb49c..cb1e7f4 100644 --- a/skillos-frontend/package.json +++ b/skillos-frontend/package.json @@ -18,6 +18,7 @@ "framer-motion": "^12.38.0", "react": "^19.2.5", "react-dom": "^19.2.5", + "react-force-graph-2d": "^1.29.1", "react-router-dom": "^7.14.2", "zustand": "^5.0.12" }, diff --git a/skillos-frontend/src/api/client.ts b/skillos-frontend/src/api/client.ts index be886f1..bc6cdab 100644 --- a/skillos-frontend/src/api/client.ts +++ b/skillos-frontend/src/api/client.ts @@ -3,22 +3,35 @@ import type { ExecutionResult, GraphData, HealthReport, + HostSurveyPreset, + HostSurveyResponse, + MergeUpdateResult, OverviewStats, + SkillReviewResult, SkillFull, SkillSearchResult, SkillState, SkillSummary, SkillType, + SkillVisibility, SystemHealth, + ResumeExecutionPayload, } from './types' const http = axios.create({ baseURL: '/api/v1' }) +function compactParams>(params?: T): Partial | undefined { + if (!params) return undefined + return Object.fromEntries( + Object.entries(params).filter(([, value]) => value !== undefined && value !== null && value !== '') + ) as Partial +} + // ── Skills ──────────────────────────────────────────────────────────────────── export const skillsApi = { - list: (params?: { state?: SkillState; skill_type?: SkillType; limit?: number; offset?: number }) => - http.get('/skills', { params }).then(r => r.data), + list: (params?: { state?: SkillState; skill_type?: SkillType; visibility?: SkillVisibility | 'all'; query?: string; limit?: number; offset?: number }) => + http.get('/skills', { params: compactParams(params) }).then(r => r.data), get: (id: string) => http.get(`/skills/${id}`).then(r => r.data), @@ -40,7 +53,7 @@ export const skillsApi = { export const lifecycleApi = { release: (id: string) => - http.post(`/lifecycle/${id}/release`).then(r => r.data), + http.post(`/lifecycle/${id}/release`, {}).then(r => r.data), deprecate: (id: string, reason: string) => http.post(`/lifecycle/${id}/deprecate`, { reason }).then(r => r.data), @@ -48,17 +61,44 @@ export const lifecycleApi = { transition: (id: string, new_state: SkillState, reason = '') => http.post(`/lifecycle/${id}/transition`, { new_state, reason }).then(r => r.data), - review: (id: string) => - http.post(`/lifecycle/${id}/review`).then(r => r.data), + review: (id: string, auto_apply = false) => + http.post(`/lifecycle/${id}/review`, null, { params: { auto_apply } }).then(r => r.data), reviewAndRelease: (id: string) => http.post(`/lifecycle/${id}/review-and-release`).then(r => r.data), - newVersion: (id: string, bump: 'major' | 'minor' | 'patch' = 'patch') => - http.post(`/lifecycle/${id}/new-version`, { bump }).then(r => r.data), +newVersion: ( + id: string, + payload: { + bump: 'major' | 'minor' | 'patch' + description?: string + tags?: string[] + interface?: unknown + implementation?: unknown | null + test_cases?: unknown[] + domain?: string + granularity_level?: number + } +) => + http.post(`/lifecycle/${id}/new-version`, payload).then(r => r.data), getDiff: (id: string, compare_to?: string) => http.get>(`/lifecycle/${id}/diff`, { params: compare_to ? { compare_to } : {} }).then(r => r.data), + + mergeUpdate: ( + id: string, + payload: { + source_skill_ids: string[] + bump?: 'major' | 'minor' | 'patch' + merge_strategy?: 'agent_generalize' | 'field_union' + description?: string + tags?: string[] + interface?: unknown + implementation?: unknown | null + test_cases?: unknown[] + } + ) => + http.post(`/lifecycle/${id}/merge-update`, payload).then(r => r.data), } // ── Graph ───────────────────────────────────────────────────────────────────── @@ -83,6 +123,9 @@ export const executionApi = { executePlan: (goal: string, context: Record = {}) => http.post('/execution/plan', { goal, context }).then(r => r.data), + resume: (payload: ResumeExecutionPayload) => + http.post('/execution/resume', payload).then(r => r.data), + getState: () => http.get>('/execution/state').then(r => r.data), @@ -91,13 +134,16 @@ export const executionApi = { history: () => http.get[]>('/execution/history').then(r => r.data), + + activity: () => + http.get<{ time: string; event: string; data: unknown }[]>('/execution/activity').then(r => r.data), } // ── Evolution ───────────────────────────────────────────────────────────────── export const evolutionApi = { - systemHealth: () => - http.get('/evolution/health').then(r => r.data), + systemHealth: (params?: { visibility?: SkillVisibility | 'all' }) => + http.get('/evolution/health', { params: compactParams(params) }).then(r => r.data), skillHealth: (id: string) => http.get(`/evolution/health/${id}`).then(r => r.data), @@ -105,6 +151,9 @@ export const evolutionApi = { repair: (id: string) => http.post(`/evolution/repair/${id}`).then(r => r.data), + improve: (id: string) => + http.post(`/evolution/improve/${id}`).then(r => r.data), + runCycle: () => http.post('/evolution/cycle').then(r => r.data), } @@ -126,6 +175,16 @@ export interface IngestResponse { proposed_description?: string proposed_type?: string confidence: number + metadata: Record + }[] + created_skill_ids: string[] + graph_nodes_created: number + graph_edges_created: number + agent_trace: { + agent: string + action: string + status: string + details: Record }[] } @@ -137,6 +196,16 @@ export const ingestApi = { http.post('/ingest/parse-and-create', { source_type, content }).then(r => r.data), } +// ── Host Information Survey ────────────────────────────────────────────────── + +export const hostInfoApi = { + presets: () => + http.get('/host-info/presets').then(r => r.data), + + survey: (payload: { task_ids?: string[]; use_llm?: boolean; persist?: boolean; max_output_chars?: number }) => + http.post('/host-info/survey', payload).then(r => r.data), +} + // ── Stats ───────────────────────────────────────────────────────────────────── export interface EvolutionStats { diff --git a/skillos-frontend/src/api/types.ts b/skillos-frontend/src/api/types.ts index 378cbb5..47d7a71 100644 --- a/skillos-frontend/src/api/types.ts +++ b/skillos-frontend/src/api/types.ts @@ -2,6 +2,7 @@ export type SkillType = 'atomic' | 'functional' | 'strategic' export type SkillState = 'S0' | 'S1' | 'S2' | 'S3' | 'S4' | 'S5' | 'S6' | 'S7' +export type SkillVisibility = 'user' | 'kernel' export const STATE_LABELS: Record = { S0: 'Raw Experience', @@ -23,10 +24,13 @@ export interface SkillParameter { } export interface SkillInterface { - inputs: SkillParameter[] - outputs: SkillParameter[] + inputs?: SkillParameter[] + outputs?: SkillParameter[] + input_schema?: Record + output_schema?: Record preconditions: string[] postconditions: string[] + side_effects?: string[] } export interface SkillImplementation { @@ -35,6 +39,7 @@ export interface SkillImplementation { prompt_template?: string tool_calls: string[] sub_skill_ids: string[] + execution_order?: string[] } export interface SkillMetrics { @@ -55,10 +60,15 @@ export interface SkillSummary { skill_id: string name: string description: string + source_format: string + is_final: boolean + immutable: boolean skill_type: SkillType state: SkillState tags: string[] + visibility: SkillVisibility version: string + domain?: string granularity_level: number metrics: SkillMetrics created_at: string @@ -66,8 +76,48 @@ export interface SkillSummary { } export interface SkillFull extends SkillSummary { + display_name?: string + provenance?: Record + test_cases?: unknown[] + tool_refs?: unknown[] + trajectory_refs?: unknown[] + doc_refs?: unknown[] interface: SkillInterface implementation?: SkillImplementation + dependency_ids?: string[] + component_ids?: string[] +} + +export interface SkillReviewResult { + review_id: string + status: string + overall_score: number + score_ratio?: number + summary: string + comments: { + field: string + severity: string + message: string + suggestion?: string + }[] + auto_fix_suggestions: Record[] + is_approved: boolean + lifecycle_action: string + updated_skill?: SkillSummary | null +} + +export interface MergeUpdateResult { + success: boolean + updated_skill: SkillSummary + merged_skills: SkillSummary[] + rationale: string + summary: string + diff: { + field: string + type: string + old_value: string + new_value: string + }[] } export interface SkillSearchResult { @@ -77,6 +127,7 @@ export interface SkillSearchResult { skill_type: SkillType state: SkillState tags: string[] + visibility: SkillVisibility version: string score: number match_reason: string @@ -85,13 +136,20 @@ export interface SkillSearchResult { export interface GraphNodeData { id: string name: string + node_type: string + description: string skill_type: string state: string tags: string[] + labels: string[] version: string + domain: string granularity_level: number success_rate: number usage_count: number + source_type?: string + metadata: Record + visibility?: SkillVisibility } export interface GraphEdgeData { @@ -100,6 +158,9 @@ export interface GraphEdgeData { target: string edge_type: string weight: number + confidence: number + description: string + metadata: Record } export interface GraphData { @@ -131,10 +192,14 @@ export interface SystemHealth { export interface ExecutionStepResult { step_id: string + step_index?: number skill_id: string skill_name: string status: string outputs: Record + result?: Record + observations?: Record[] + step_judgment?: Record latency_ms: number error?: string } @@ -158,6 +223,22 @@ export interface ExecutionResult { retrieved_skills: RetrievedSkill[] experience_recorded: boolean suggested_skill?: Record + assistance_request?: Record + agent_trace?: { + agent: string + action: string + status: string + details: Record + }[] +} + +export interface ResumeExecutionPayload { + plan_id: string + goal: string + guidance: string + final_state: Record + assistance_request?: Record + context?: Record } export interface OverviewStats { @@ -168,3 +249,38 @@ export interface OverviewStats { avg_success_rate: number graph_stats: Record } + +export interface HostSurveyPreset { + task_id: string + name: string + description: string + labels: string[] + fallback_command: string[] +} + +export interface HostSurveyCommand { + task_id: string + name: string + description: string + command: string[] + command_source: string + status: string + summary: string + node_id?: string + stdout_preview: string + error?: string +} + +export interface HostSurveyResponse { + success: boolean + run_id: string + created_nodes: number + created_edges: number + commands: HostSurveyCommand[] + agent_trace: { + agent: string + action: string + status: string + details: Record + }[] +} diff --git a/skillos-frontend/src/components/AgentRuntimeMini.tsx b/skillos-frontend/src/components/AgentRuntimeMini.tsx new file mode 100644 index 0000000..24d02fb --- /dev/null +++ b/skillos-frontend/src/components/AgentRuntimeMini.tsx @@ -0,0 +1,223 @@ +import { useEffect, useMemo, useState } from 'react' +import { Button, Empty, Tag, Timeline, Tooltip } from 'antd' +import { ClearOutlined, WifiOutlined } from '@ant-design/icons' +import { motion } from 'framer-motion' +import { executionApi } from '@/api/client' +import { useAppStore } from '@/store/appStore' + +const PHASE_LABELS: Record = { + understand_task: 'Understanding', + read_graph_context: 'Graph Context', + select_skills: 'Selecting Skills', + execute_plan: 'Executing', + finish_execution: 'Finished', + idle: 'Idle', +} + +export default function AgentRuntimeMini({ collapsed = false }: { collapsed?: boolean }) { + const [polledEvents, setPolledEvents] = useState<{ time: string; event: string; data: unknown }[]>([]) + const { wsEvents, clearWsEvents } = useAppStore() + + useEffect(() => { + let cancelled = false + const refreshActivity = () => { + executionApi.activity() + .then(events => { + if (!cancelled) setPolledEvents(events) + }) + .catch(() => { + if (!cancelled) setPolledEvents([]) + }) + } + refreshActivity() + const timer = setInterval(refreshActivity, 1000) + return () => { + cancelled = true + clearInterval(timer) + } + }, []) + + const allEvents = useMemo( + () => [...wsEvents, ...polledEvents].filter(e => e.event !== 'pong' && e.event !== 'connected'), + [wsEvents, polledEvents], + ) + const agentEvents = allEvents.filter(e => e.event === 'agent_activity') + const currentAgentData = (agentEvents[0]?.data || {}) as Record + const phase = String(currentAgentData.phase || 'idle') + const message = String(currentAgentData.message || 'Waiting for task') + const selected = Array.isArray(currentAgentData.selected) + ? currentAgentData.selected as string[] + : Array.isArray(currentAgentData.steps) + ? currentAgentData.steps as string[] + : [] + const nodes = Array.isArray(currentAgentData.nodes) ? currentAgentData.nodes as string[] : [] + const active = phase !== 'idle' && phase !== 'finish_execution' + + if (collapsed) { + return ( + +
+ +
+ AI +
+
+
+
+ ) + } + + return ( +
+
+
+ {[0, 1].map(i => ( + + ))} + +
+ AI +
+
+
+
+ + {PHASE_LABELS[phase] || phase} + +
+ {message.length > 64 ? `${message.slice(0, 64)}...` : message} +
+
+
+ +
+ {selected.slice(0, 2).map(item => ( + + {item} + + ))} + {nodes.slice(0, 2).map(item => ( + + {item} + + ))} + {selected.length === 0 && nodes.length === 0 && No active context} +
+ +
+ {allEvents.length === 0 ? ( + No events} /> + ) : ( + ({ + color: e.event.includes('error') || e.event.includes('fail') ? 'red' : e.event.includes('complete') ? 'green' : 'blue', + children: ( +
+ {e.event} + {e.time} +
+ ), + }))} + /> + )} +
+ + +
+ ) +} diff --git a/skillos-frontend/src/components/AppLayout.tsx b/skillos-frontend/src/components/AppLayout.tsx index c0662dc..ec06af2 100644 --- a/skillos-frontend/src/components/AppLayout.tsx +++ b/skillos-frontend/src/components/AppLayout.tsx @@ -1,63 +1,31 @@ import { useState } from 'react' -import { Layout, Menu, Switch, Badge, Tooltip, Avatar, Tag } from 'antd' +import { Layout, Menu, Switch, Badge, Tooltip, Avatar } from 'antd' import { DashboardOutlined, BookOutlined, - ApartmentOutlined, PlayCircleOutlined, - SyncOutlined, ExperimentOutlined, CloudUploadOutlined, - BranchesOutlined, BulbOutlined, BulbFilled, WifiOutlined, - RocketOutlined, + ControlOutlined, } from '@ant-design/icons' import { useNavigate, useLocation } from 'react-router-dom' import { motion } from 'framer-motion' import { useAppStore } from '@/store/appStore' import { useWebSocket } from '@/hooks/useWebSocket' +import AgentRuntimeMini from './AgentRuntimeMini' const { Header, Sider, Content } = Layout const menuItems = [ - { - type: 'group' as const, - label: 'Overview', - children: [ - { key: '/', icon: , label: 'Dashboard' }, - ], - }, - { - type: 'group' as const, - label: 'Skill Management', - children: [ - { key: '/wiki', icon: , label: 'Skill Wiki' }, - { key: '/graph', icon: , label: 'Knowledge Graph' }, - { key: '/versions', icon: , label: 'Version Control' }, - { key: '/lifecycle', icon: , label: 'Lifecycle' }, - ], - }, - { - type: 'group' as const, - label: 'Agent & Evolution', - children: [ - { - key: '/demo', - icon: , - label: ( - - Self-Evolution Demo - DEMO - - ), - }, - { key: '/execution', icon: , label: 'Agent Execution' }, - { key: '/evolution', icon: , label: 'Evolution' }, - { key: '/ingest', icon: , label: 'Knowledge Import' }, - ], - }, + { key: '/', icon: , label: 'Dashboard' }, + { key: '/ingest', icon: , label: 'Knowledge Import' }, + { key: '/wiki', icon: , label: 'Skill Wiki' }, + { key: '/manage', icon: , label: 'Skill Manage' }, + { key: '/evaluation', icon: , label: 'Evaluation' }, + { key: '/execution', icon: , label: 'Execution' }, ] export default function AppLayout({ children }: { children: React.ReactNode }) { @@ -118,6 +86,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) { onClick={({ key }) => navigate(key)} style={{ borderRight: 0, marginTop: 8 }} /> + diff --git a/skillos-frontend/src/hooks/useWebSocket.ts b/skillos-frontend/src/hooks/useWebSocket.ts index 2ef430e..ad8b9d2 100644 --- a/skillos-frontend/src/hooks/useWebSocket.ts +++ b/skillos-frontend/src/hooks/useWebSocket.ts @@ -6,28 +6,46 @@ export function useWebSocket() { const pushWsEvent = useAppStore(s => s.pushWsEvent) useEffect(() => { + let closedByEffect = false + let reconnectTimer: ReturnType | null = null const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws' - const ws = new WebSocket(`${protocol}://${window.location.host}/ws`) - wsRef.current = ws - - ws.onmessage = (e) => { - try { - const msg = JSON.parse(e.data) - pushWsEvent(msg.event, msg.data) - } catch { - // ignore + const connect = () => { + const ws = new WebSocket(`${protocol}://${window.location.host}/ws`) + wsRef.current = ws + + ws.onmessage = (e) => { + try { + const msg = JSON.parse(e.data) + pushWsEvent(msg.event, msg.data) + } catch { + // ignore + } + } + + ws.onclose = () => { + if (!closedByEffect) { + reconnectTimer = setTimeout(connect, 1500) + } + } + + ws.onerror = () => { + ws.close() } } + connect() const ping = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { + const ws = wsRef.current + if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })) } }, 30_000) return () => { + closedByEffect = true clearInterval(ping) - ws.close() + if (reconnectTimer) clearTimeout(reconnectTimer) + wsRef.current?.close() } }, [pushWsEvent]) diff --git a/skillos-frontend/src/main.tsx b/skillos-frontend/src/main.tsx index ccf8b22..5dec95c 100644 --- a/skillos-frontend/src/main.tsx +++ b/skillos-frontend/src/main.tsx @@ -7,13 +7,10 @@ import { useAppStore } from '@/store/appStore' import AppLayout from '@/components/AppLayout' import Dashboard from '@/pages/Dashboard' import SkillWiki from '@/pages/SkillWiki' -import SkillGraph from '@/pages/SkillGraph' import AgentExecution from '@/pages/AgentExecution' -import Evolution from '@/pages/Evolution' -import LifecycleDemo from '@/pages/LifecycleDemo' import KnowledgeImport from '@/pages/KnowledgeImport' -import VersionControl from '@/pages/VersionControl' -import SelfEvolutionDemo from '@/pages/SelfEvolutionDemo' +import SkillManage from '@/pages/SkillManage' +import Evaluation from '@/pages/Evaluation' import './index.css' function App() { @@ -35,14 +32,11 @@ function App() { } /> + } /> } /> - } /> + } /> + } /> } /> - } /> - } /> - } /> - } /> - } /> diff --git a/skillos-frontend/src/pages/AgentExecution.tsx b/skillos-frontend/src/pages/AgentExecution.tsx index a2ac3a1..9ed1163 100644 --- a/skillos-frontend/src/pages/AgentExecution.tsx +++ b/skillos-frontend/src/pages/AgentExecution.tsx @@ -1,11 +1,12 @@ import { useState } from 'react' import { Card, Input, Button, Tag, Alert, Spin, Divider, - Typography, Space, Badge, Statistic, Row, Col, Progress, Tooltip, + Typography, Space, Statistic, Row, Col, Progress, Tooltip, Collapse, Timeline, } from 'antd' import { PlayCircleOutlined, ThunderboltOutlined, CheckCircleOutlined, CloseCircleOutlined, SearchOutlined, DatabaseOutlined, + BranchesOutlined, NodeIndexOutlined, QuestionCircleOutlined, PictureOutlined, } from '@ant-design/icons' import { motion, AnimatePresence } from 'framer-motion' import { executionApi } from '@/api/client' @@ -20,14 +21,378 @@ const STATUS_COLOR: Record = { running: '#1677ff', pending: '#d9d9d9', skipped: '#faad14', + waiting_for_user: '#faad14', } const SKILL_TYPE_COLOR: Record = { atomic: '#1677ff', functional: '#722ed1', strategic: '#faad14', } +const NODE_TYPE_COLOR: Record = { + host_information: 'geekblue', + document: 'green', + trajectory: 'red', + task: 'orange', + tool: 'cyan', + api_doc: 'purple', + script: 'gold', + agent: 'lime', +} + +const TRACE_LABEL: Record = { + interpret_without_graph_or_skills: 'Task-only interpretation', + read_graph_context: 'Graph / host information retrieval', + decompose_task_layers: 'Three-layer decomposition', + retrieve_and_judge_skill_candidates: 'Skill retrieval and grounded judgment', + predict_expected_outcome: 'Expected outcome prediction', + build_execution_plan: 'Execution plan generation', + bind_step_inputs: 'Runtime input binding', + configure_observation_loop: 'Observation loop configuration', + observe_runtime_steps: 'Runtime step observations', + browser_observe_decide_act_loop: 'Browser observe-decide-act loop', + validate_expected_outcome: 'Outcome validation', + retry_after_mismatch: 'Mismatch repair retry', + request_user_assistance: 'Human-in-the-loop pause', + execute_on_host_runtime: 'Host runtime execution', + reflect_and_update_skill_memory: 'Execution learning', +} + +const DEFAULT_EXECUTION_CONTEXT = { + username: 'demo@example.com', + password: 'pass123', + form_data: { + email: 'demo@example.com', + password: 'pass123', + }, +} + +function findTrace(result: ExecutionResult | null, action: string) { + return result?.agent_trace?.find(trace => trace.action === action) +} + +function asRecord(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) + ? value as Record + : {} +} + +function asArray(value: unknown): unknown[] { + return Array.isArray(value) ? value : [] +} + +function traceStatusColor(status: string) { + if (['success', 'completed', 'created', 'reused'].includes(status)) return 'green' + if (['partial', 'empty', 'skipped', 'mismatch', 'waiting_for_user'].includes(status)) return 'gold' + if (['failed', 'error'].includes(status)) return 'red' + return 'blue' +} + +function JsonBlock({ value, maxHeight = 280 }: { value: unknown; maxHeight?: number }) { + return ( +
+      {JSON.stringify(value, null, 2)}
+    
+ ) +} + +function getTrace(result: ExecutionResult | null, action: string) { + return result?.agent_trace?.find(trace => trace.action === action) +} + +function getHostInformationUsed(result: ExecutionResult | null) { + const all: Record> = {} + for (const trace of result?.agent_trace || []) { + const details = asRecord(trace.details) + for (const item of asArray(details.host_information_used)) { + const node = asRecord(item) + const id = String(node.id || node.name || Math.random()) + all[id] = node + } + for (const item of asArray(details.graph_context)) { + const node = asRecord(item) + if (node.node_type === 'host_information') { + const id = String(node.id || node.name || Math.random()) + all[id] = node + } + } + } + return Object.values(all) +} + +function getBrowserLoopTrace(result: ExecutionResult | null) { + return result?.agent_trace?.find(trace => + trace.action === 'desktop_resume_observe_decide_act' + || trace.action === 'browser_resume_observe_decide_act' + || trace.action === 'browser_observe_decide_act_loop' + ) || null +} + +function compactValue(value: unknown, max = 90) { + if (value === undefined || value === null || value === '') return '' + if (typeof value === 'object') return JSON.stringify(value).slice(0, max) + return String(value).slice(0, max) +} + +function BrowserLoopPanel({ result }: { result: ExecutionResult | null }) { + const browserTrace = getBrowserLoopTrace(result) + const details = asRecord(browserTrace?.details) + const finalState = asRecord(result?.final_state) + const controller = asRecord(details.controller || finalState.controller) + const observations = asArray(details.observations || finalState.observations) + const actions = asArray(details.actions || finalState.actions) + if (!browserTrace && observations.length === 0 && actions.length === 0) return null + + return ( + Browser Observation Loop} + bordered={false} + size="small" + style={{ borderRadius: 12, boxShadow: '0 2px 8px rgba(0,0,0,0.08)', marginBottom: 16 }} + > + + + + + + + + + + query: {compactValue(details.query || finalState.query, 80)} + entry: {compactValue(details.entry_url || finalState.entry_url, 120)} + {details.success ? 'success' : 'not finished'} + {Object.keys(controller).length > 0 && ( + + controller: {String(controller.name || 'browser_controller')} · DOM {controller.dom_supported ? 'on' : 'off'} + + )} + + + + + { + const action = asRecord(item) + return { + color: action.status === 'success' ? 'green' : action.status === 'blocked' ? 'red' : 'blue', + children: ( +
+ + round {String(action.round ?? index)} + {String(action.action || 'action')} + +
{String(action.reason || '')}
+ {compactValue(action.target, 140)} +
+ ), + } + })} + /> +
+ + + + { + const obs = asRecord(item) + return { + key: `browser-loop-${index}`, + label: `round ${String(obs.round ?? index)} · ${String(obs.observation_type || obs.type || 'observation')}`, + children: , + } + })} + /> + + +
+
+
+ ) +} + +function collectScreenshotObservations(result: ExecutionResult | null) { + const screenshots: Record[] = [] + for (const step of result?.steps || []) { + for (const packet of step.observations || []) { + const packetRecord = asRecord(packet) + for (const obs of asArray(packetRecord.observations)) { + const obsRecord = asRecord(obs) + const evidence = asRecord(obsRecord.evidence) + if (obsRecord.type === 'screenshot' || evidence.path) { + screenshots.push({ + step_id: step.step_id, + skill_name: step.skill_name, + phase: packetRecord.phase, + status: obsRecord.status, + ...evidence, + }) + } + } + } + const output = asRecord(step.outputs) + for (const obs of asArray(output.observations)) { + const obsRecord = asRecord(obs) + const evidence = asRecord(obsRecord.evidence) + if (obsRecord.observation_type === 'screenshot' || evidence.path) { + screenshots.push({ + step_id: step.step_id, + skill_name: step.skill_name, + phase: 'browser_round', + status: 'success', + ...evidence, + }) + } + } + } + return screenshots +} + +function AssistancePanel({ + result, + onResume, + loading = false, +}: { + result: ExecutionResult | null + onResume: (guidance: string) => void + loading?: boolean +}) { + const [guidance, setGuidance] = useState('') + const request = asRecord(result?.assistance_request) + if (Object.keys(request).length === 0) return null + const needed = asArray(request.needed_information) + const accepted = asArray(request.accepted_inputs) + const currentObservations = asArray(request.current_observations) + const screenshots = currentObservations.length > 0 ? currentObservations : collectScreenshotObservations(result) + + return ( + Agent Paused For Guidance} + bordered={false} + size="small" + style={{ borderRadius: 12, boxShadow: '0 2px 8px rgba(0,0,0,0.08)', marginBottom: 16, borderLeft: '4px solid #faad14' }} + > + + + + + + {needed.map((item, index) => ( + • {String(item)} + ))} + + {accepted.map(item => {String(item)})} + + + + + + Current Visual Evidence} style={{ height: '100%' }}> + {screenshots.length === 0 ? ( + No screenshot evidence was captured or permission was unavailable. + ) : ( + + {screenshots.slice(-4).map((item, index) => { + const obs = asRecord(item) + return ( +
+ + {String(obs.status || 'unknown')} + {String(obs.skill_name || obs.step_id || 'screen')} + +
{String(obs.path || 'no path')}
+
+ ) + })} +
+ )} +
+ +
+ +