From c273d60f9d3fe17f1f2e724a7be96797d293d6f0 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 31 Mar 2026 18:26:32 +0530 Subject: [PATCH 1/4] chore: update .gitignore and increment OpenHuman version to 0.49.32 - Added 'workflow' to .gitignore to exclude workflow files from version control. - Updated OpenHuman version in Cargo.lock from 0.49.31 to 0.49.32. --- .gitignore | 1 + app/src-tauri/Cargo.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9a2584940..478a96b34 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ tauri.key.pub src-tauri/target/ openhuman-skills +workflow \ No newline at end of file diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index 9b07a0ef3..e4906d677 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "OpenHuman" -version = "0.49.31" +version = "0.49.32" dependencies = [ "env_logger", "log", From 649c381f3d2be18958aa633e6bc950e80bfe7553 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 31 Mar 2026 19:22:37 +0530 Subject: [PATCH 2/4] feat: set up openhuman-skills git submodule - Add openhuman-skills submodule pointing to tinyhumansai/openhuman-skills - Remove openhuman-skills from .gitignore so the submodule is tracked - Update skill discovery paths in qjs_engine.rs from skills/skills to openhuman-skills/skills (dev cwd, parent, and bundled resource paths) Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 - .gitmodules | 3 +++ CONTRIBUTING.md | 15 +++++++++++++++ openhuman-skills | 1 + src/openhuman/skills/qjs_engine.rs | 10 +++++----- 5 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 .gitmodules create mode 160000 openhuman-skills diff --git a/.gitignore b/.gitignore index 478a96b34..6ba151d1a 100644 --- a/.gitignore +++ b/.gitignore @@ -53,5 +53,4 @@ tauri.key.pub /target/ src-tauri/target/ -openhuman-skills workflow \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..b3cb4a18a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "openhuman-skills"] + path = openhuman-skills + url = https://github.com/tinyhumansai/openhuman-skills diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6650b7654..5bb4d0988 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,11 +35,26 @@ This project adheres to the [Contributor Covenant Code of Conduct](CODE_OF_CONDU ```bash git clone https://github.com/YOUR_USERNAME/openhuman.git cd openhuman +git submodule update --init --recursive # pulls openhuman-skills yarn install ``` Use your own fork in place of `YOUR_USERNAME` when cloning. +The `openhuman-skills` submodule contains the built skill bundles used by the runtime. After cloning you must initialise it (the command above does this). If you forget, the runtime will fall back to fetching skills from the remote registry, which is slower and requires network access. + +### Updating Skills + +When the `openhuman-skills` submodule is updated upstream, run: + +```bash +git submodule update --remote openhuman-skills +cd openhuman-skills && yarn install && yarn build +cd .. +git add openhuman-skills +git commit -m "chore: update openhuman-skills submodule" +``` + ### Run the App - **Web only**: `yarn dev` (Vite dev server, typically port 1420) diff --git a/openhuman-skills b/openhuman-skills new file mode 160000 index 000000000..52d58295c --- /dev/null +++ b/openhuman-skills @@ -0,0 +1 @@ +Subproject commit 52d58295c0cfbc22b5ad962fd7ae34f1efd7c000 diff --git a/src/openhuman/skills/qjs_engine.rs b/src/openhuman/skills/qjs_engine.rs index a183fb3ec..f8188a86f 100644 --- a/src/openhuman/skills/qjs_engine.rs +++ b/src/openhuman/skills/qjs_engine.rs @@ -165,16 +165,16 @@ impl RuntimeEngine { let current = std::env::current_dir().map_err(|e| format!("Failed to get current dir: {e}"))?; - // 2. Dev: cwd/skills/skills - let dev_skills = current.join("skills").join("skills"); + // 2. Dev: cwd/openhuman-skills/skills + let dev_skills = current.join("openhuman-skills").join("skills"); if dev_skills.exists() { log::info!("[runtime] Using dev skills dir: {:?}", dev_skills); return Ok(dev_skills); } - // 3. Dev: ../skills/skills + // 3. Dev: ../openhuman-skills/skills if let Some(parent) = current.parent() { - let parent_skills = parent.join("skills").join("skills"); + let parent_skills = parent.join("openhuman-skills").join("skills"); if parent_skills.exists() { log::info!("[runtime] Using parent dev skills dir: {:?}", parent_skills); return Ok(parent_skills); @@ -183,7 +183,7 @@ impl RuntimeEngine { // 4. Production: bundled resources if let Some(resource_dir) = self.resource_dir.read().as_ref() { - let bundled_skills = resource_dir.join("_up_").join("skills").join("skills"); + let bundled_skills = resource_dir.join("_up_").join("openhuman-skills").join("skills"); if bundled_skills.exists() { log::info!( "[runtime] Using bundled skills from resources: {:?}", From 7b0e07f8ad315a60b1231cd9f829f6282bf7a1f6 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 31 Mar 2026 21:44:04 +0530 Subject: [PATCH 3/4] fix(agent): execute fallback tool calls in the loop Unify tool-call parsing across dispatcher paths and persist parsed fallback calls with stable IDs so execution and history stay aligned end-to-end. Closes #65 Made-with: Cursor --- src/openhuman/agent/agent.rs | 105 ++++++++++++++++++++++++- src/openhuman/agent/dispatcher.rs | 122 ++++++++++++++---------------- src/openhuman/agent/loop_/mod.rs | 1 + src/openhuman/agent/tests.rs | 58 +++++++++++++- 4 files changed, 215 insertions(+), 71 deletions(-) diff --git a/src/openhuman/agent/agent.rs b/src/openhuman/agent/agent.rs index 2f286b547..1e18a0d80 100644 --- a/src/openhuman/agent/agent.rs +++ b/src/openhuman/agent/agent.rs @@ -6,7 +6,9 @@ use super::prompt::{PromptContext, SystemPromptBuilder}; use crate::openhuman::agent::host_runtime; use crate::openhuman::config::Config; use crate::openhuman::memory::{self, Memory, MemoryCategory}; -use crate::openhuman::providers::{self, ChatMessage, ChatRequest, ConversationMessage, Provider}; +use crate::openhuman::providers::{ + self, ChatMessage, ChatRequest, ConversationMessage, Provider, ToolCall, +}; use crate::openhuman::security::SecurityPolicy; use crate::openhuman::tools::{self, Tool, ToolSpec}; use crate::openhuman::util::truncate_with_ellipsis; @@ -195,6 +197,41 @@ impl AgentBuilder { } impl Agent { + fn with_fallback_tool_call_ids( + mut parsed_calls: Vec, + iteration: usize, + ) -> Vec { + for (idx, call) in parsed_calls.iter_mut().enumerate() { + if call.tool_call_id.is_none() { + call.tool_call_id = Some(format!("parsed-{}-{}", iteration + 1, idx + 1)); + } + } + parsed_calls + } + + fn persisted_tool_calls_for_history( + response: &crate::openhuman::providers::ChatResponse, + parsed_calls: &[ParsedToolCall], + iteration: usize, + ) -> Vec { + if !response.tool_calls.is_empty() { + return response.tool_calls.clone(); + } + + parsed_calls + .iter() + .enumerate() + .map(|(idx, call)| ToolCall { + id: call + .tool_call_id + .clone() + .unwrap_or_else(|| format!("parsed-{}-{}", iteration + 1, idx + 1)), + name: call.name.clone(), + arguments: call.arguments.to_string(), + }) + .collect() + } + pub fn builder() -> AgentBuilder { AgentBuilder::new() } @@ -486,6 +523,7 @@ impl Agent { }; let (text, calls) = self.tool_dispatcher.parse_response(&response); + let calls = Self::with_fallback_tool_call_ids(calls, iteration); log::info!( "[agent_loop] parsed response i={} parsed_text_chars={} parsed_tool_calls={}", iteration + 1, @@ -540,10 +578,16 @@ impl Agent { iteration + 1, tool_names ); - + let persisted_tool_calls = Self::persisted_tool_calls_for_history(&response, &calls, iteration); + log::info!( + "[agent_loop] persisting assistant tool calls i={} persisted_tool_calls={} parsed_tool_calls={}", + iteration + 1, + persisted_tool_calls.len(), + calls.len() + ); self.history.push(ConversationMessage::AssistantToolCalls { - text: response.text.clone(), - tool_calls: response.tool_calls.clone(), + text: if text.is_empty() { None } else { Some(text.clone()) }, + tool_calls: persisted_tool_calls, }); let results = self.execute_tools(&calls).await; @@ -773,4 +817,57 @@ mod tests { .iter() .any(|msg| matches!(msg, ConversationMessage::ToolResults(_)))); } + + #[tokio::test] + async fn turn_with_native_dispatcher_persists_fallback_tool_calls() { + let workspace = tempfile::TempDir::new().expect("temp workspace"); + let workspace_path = workspace.path().to_path_buf(); + + let provider = Box::new(MockProvider { + responses: Mutex::new(vec![ + crate::openhuman::providers::ChatResponse { + text: Some( + "Checking...\n{\"name\":\"echo\",\"arguments\":{}}" + .into(), + ), + tool_calls: vec![], + }, + crate::openhuman::providers::ChatResponse { + text: Some("done".into()), + tool_calls: vec![], + }, + ]), + }); + + let memory_cfg = crate::openhuman::config::MemoryConfig { + backend: "none".into(), + ..crate::openhuman::config::MemoryConfig::default() + }; + let mem: Arc = Arc::from( + crate::openhuman::memory::create_memory(&memory_cfg, &workspace_path, None).unwrap(), + ); + + let mut agent = Agent::builder() + .provider(provider) + .tools(vec![Box::new(MockTool)]) + .memory(mem) + .tool_dispatcher(Box::new(NativeToolDispatcher)) + .workspace_dir(workspace_path) + .build() + .unwrap(); + + let response = agent.turn("hi").await.unwrap(); + assert_eq!(response, "done"); + + let persisted_calls = agent + .history() + .iter() + .find_map(|msg| match msg { + ConversationMessage::AssistantToolCalls { tool_calls, .. } => Some(tool_calls), + _ => None, + }) + .expect("assistant tool calls should be persisted"); + assert_eq!(persisted_calls.len(), 1); + assert_eq!(persisted_calls[0].name, "echo"); + } } diff --git a/src/openhuman/agent/dispatcher.rs b/src/openhuman/agent/dispatcher.rs index 29e33547d..f73b15007 100644 --- a/src/openhuman/agent/dispatcher.rs +++ b/src/openhuman/agent/dispatcher.rs @@ -2,6 +2,7 @@ use crate::openhuman::providers::{ ChatMessage, ChatResponse, ConversationMessage, ToolResultMessage, }; use crate::openhuman::tools::{Tool, ToolSpec}; +use crate::openhuman::agent::loop_::parse_tool_calls; use serde_json::Value; use std::fmt::Write; @@ -32,55 +33,17 @@ pub trait ToolDispatcher: Send + Sync { pub struct XmlToolDispatcher; impl XmlToolDispatcher { - fn parse_xml_tool_calls(response: &str) -> (String, Vec) { - let mut text_parts = Vec::new(); - let mut calls = Vec::new(); - let mut remaining = response; - - while let Some(start) = remaining.find("") { - let before = &remaining[..start]; - if !before.trim().is_empty() { - text_parts.push(before.trim().to_string()); - } - - if let Some(end) = remaining[start..].find("") { - let inner = &remaining[start + 11..start + end]; - match serde_json::from_str::(inner.trim()) { - Ok(parsed) => { - let name = parsed - .get("name") - .and_then(Value::as_str) - .unwrap_or("") - .to_string(); - if name.is_empty() { - remaining = &remaining[start + end + 12..]; - continue; - } - let arguments = parsed - .get("arguments") - .cloned() - .unwrap_or_else(|| Value::Object(serde_json::Map::new())); - calls.push(ParsedToolCall { - name, - arguments, - tool_call_id: None, - }); - } - Err(e) => { - tracing::warn!("Malformed JSON: {e}"); - } - } - remaining = &remaining[start + end + 12..]; - } else { - break; - } - } - - if !remaining.trim().is_empty() { - text_parts.push(remaining.trim().to_string()); - } - - (text_parts.join("\n"), calls) + fn parse_tool_calls_from_text(response: &str) -> (String, Vec) { + let (text, calls) = parse_tool_calls(response); + let parsed_calls = calls + .into_iter() + .map(|call| ParsedToolCall { + name: call.name, + arguments: call.arguments, + tool_call_id: None, + }) + .collect::>(); + (text, parsed_calls) } pub fn tool_specs(tools: &[Box]) -> Vec { @@ -91,7 +54,13 @@ impl XmlToolDispatcher { impl ToolDispatcher for XmlToolDispatcher { fn parse_response(&self, response: &ChatResponse) -> (String, Vec) { let text = response.text_or_empty(); - Self::parse_xml_tool_calls(text) + let (parsed_text, parsed_calls) = Self::parse_tool_calls_from_text(text); + tracing::debug!( + parse_mode = "text_fallback", + parsed_tool_calls = parsed_calls.len(), + "xml dispatcher parsed response" + ); + (parsed_text, parsed_calls) } fn format_results(&self, results: &[ToolExecutionResult]) -> ConversationMessage { @@ -163,7 +132,7 @@ pub struct NativeToolDispatcher; impl ToolDispatcher for NativeToolDispatcher { fn parse_response(&self, response: &ChatResponse) -> (String, Vec) { let text = response.text.clone().unwrap_or_default(); - let mut calls: Vec = response + let calls: Vec = response .tool_calls .iter() .map(|tc| ParsedToolCall { @@ -180,29 +149,37 @@ impl ToolDispatcher for NativeToolDispatcher { }) .collect(); - // Fallback for providers/models that emit XML-style tool calls in text - // even when native tool-calling is configured. - if calls.is_empty() && !text.is_empty() { - let (fallback_text, fallback_calls) = XmlToolDispatcher::parse_xml_tool_calls(&text); - if !fallback_calls.is_empty() { - calls = fallback_calls - .into_iter() - .map(|call| ParsedToolCall { - name: call.name, - arguments: call.arguments, - tool_call_id: None, - }) - .collect(); + if !calls.is_empty() { + tracing::debug!( + parse_mode = "native_structured", + parsed_tool_calls = calls.len(), + "native dispatcher parsed response" + ); + return (text, calls); + } + if !text.is_empty() { + let (fallback_text, fallback_calls) = XmlToolDispatcher::parse_tool_calls_from_text(&text); + if !fallback_calls.is_empty() { let display_text = if fallback_text.is_empty() { text } else { fallback_text }; - return (display_text, calls); + tracing::debug!( + parse_mode = "text_fallback", + parsed_tool_calls = fallback_calls.len(), + "native dispatcher parsed response" + ); + return (display_text, fallback_calls); } } + tracing::debug!( + parse_mode = "none", + parsed_tool_calls = 0, + "native dispatcher parsed response" + ); (text, calls) } @@ -331,6 +308,21 @@ mod tests { assert_eq!(calls[0].tool_call_id, None); } + #[test] + fn native_dispatcher_falls_back_to_invoke_tag() { + let response = ChatResponse { + text: Some( + "Let me run this.\n{\"name\":\"shell\",\"arguments\":{\"command\":\"pwd\"}}".into(), + ), + tool_calls: vec![], + }; + let dispatcher = NativeToolDispatcher; + let (text, calls) = dispatcher.parse_response(&response); + assert_eq!(text, "Let me run this."); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].name, "shell"); + } + #[test] fn xml_format_results_contains_tool_result_tags() { let dispatcher = XmlToolDispatcher; diff --git a/src/openhuman/agent/loop_/mod.rs b/src/openhuman/agent/loop_/mod.rs index 685fdb288..27b0cb361 100644 --- a/src/openhuman/agent/loop_/mod.rs +++ b/src/openhuman/agent/loop_/mod.rs @@ -9,6 +9,7 @@ mod session; mod tool_loop; pub(crate) use instructions::build_tool_instructions; +pub(crate) use parse::parse_tool_calls; pub use session::{process_message, run}; pub(crate) use tool_loop::run_tool_call_loop; diff --git a/src/openhuman/agent/tests.rs b/src/openhuman/agent/tests.rs index 4916fae37..0247bce0e 100644 --- a/src/openhuman/agent/tests.rs +++ b/src/openhuman/agent/tests.rs @@ -819,6 +819,59 @@ async fn turn_handles_multiple_tools_in_one_response() { ); } +#[tokio::test] +async fn e2e_native_loop_executes_text_fallback_tool_calls_and_persists_history() { + let provider = Box::new(ScriptedProvider::new(vec![ + ChatResponse { + text: Some( + "I'll inspect now.\n{\"name\":\"echo\",\"arguments\":{\"message\":\"from-fallback\"}}" + .into(), + ), + tool_calls: vec![], + }, + text_response("Completed via tool"), + ])); + + let mut agent = build_agent_with( + provider, + vec![Box::new(EchoTool)], + Box::new(NativeToolDispatcher), + ); + + let response = agent.turn("please use a tool").await.unwrap(); + assert_eq!(response, "Completed via tool"); + + let mut assistant_tool_calls: Option> = None; + let mut tool_results: Option> = None; + + for msg in agent.history() { + match msg { + ConversationMessage::AssistantToolCalls { tool_calls, .. } => { + assistant_tool_calls = Some(tool_calls.clone()); + } + ConversationMessage::ToolResults(results) => { + tool_results = Some(results.clone()); + } + _ => {} + } + } + + let calls = assistant_tool_calls.expect("assistant tool calls should be persisted"); + let results = tool_results.expect("tool results should be persisted"); + assert_eq!(calls.len(), 1, "expected one parsed/persisted tool call"); + assert_eq!(results.len(), 1, "expected one tool result"); + assert_eq!(calls[0].name, "echo"); + assert!( + calls[0].arguments.contains("from-fallback"), + "persisted tool-call arguments should include fallback payload" + ); + assert_eq!( + calls[0].id, results[0].tool_call_id, + "tool result must map to persisted assistant tool-call id" + ); + assert_eq!(results[0].content, "from-fallback"); +} + // ═══════════════════════════════════════════════════════════════════════════ // 14. System prompt generation & tool instructions // ═══════════════════════════════════════════════════════════════════════════ @@ -1042,8 +1095,9 @@ fn xml_dispatcher_handles_unclosed_tool_call() { let dispatcher = XmlToolDispatcher; let (text, calls) = dispatcher.parse_response(&response); - // Should not panic — just treat as text - assert!(calls.is_empty()); + // Should not panic; robust parser recovers the JSON tool call. + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].name, "shell"); assert!(text.contains("Before")); } From 331b1e4ef80233cf7f7b40ca15f51324faf18a92 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 31 Mar 2026 21:46:10 +0530 Subject: [PATCH 4/4] Remove openhuman-skills submodule and add comprehensive documentation on the skills system, detailing discovery, installation, execution, and synchronization processes for engineers. This includes a mental model, key directories, skill packaging, registry flow, and runtime behavior. --- docs/SKILLS-HOW-THEY-WORK.md | 401 +++++++++++++++++++++++++++++++++++ openhuman-skills | 1 - 2 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 docs/SKILLS-HOW-THEY-WORK.md delete mode 160000 openhuman-skills diff --git a/docs/SKILLS-HOW-THEY-WORK.md b/docs/SKILLS-HOW-THEY-WORK.md new file mode 100644 index 000000000..3215a98a0 --- /dev/null +++ b/docs/SKILLS-HOW-THEY-WORK.md @@ -0,0 +1,401 @@ +# Skills: How They Work End-to-End + +This document explains how OpenHuman skills are discovered, fetched, installed, initialized, executed, and synchronized across the desktop app and Rust core. + +It is written for engineers who need to debug, extend, or migrate the skills system. + +--- + +## 1) Mental Model + +OpenHuman has two skill-related paths: + +1. **Active runtime path (authoritative for execution):** + - QuickJS skills managed by Rust core runtime. + - Accessed through JSON-RPC methods under `openhuman.skills_*`. + - UI acts as an RPC client and orchestration layer. + +2. **Legacy metadata path (still present):** + - Workspace scanning of `skill.json` + `SKILL.md`. + - Used for older prompt/context loading flows, not the primary execution runtime. + +If you are implementing runtime behavior, use the active QuickJS path. + +--- + +## 2) Key Directories and Files + +### Frontend (app) + +- `app/src/lib/skills/skillsApi.ts` + - Typed RPC wrapper for skills methods (`list_available`, `install`, `start`, `rpc`, etc.). +- `app/src/lib/skills/manager.ts` + - Orchestrates setup, OAuth completion, tool usage, and sync triggers. +- `app/src/lib/skills/runtime.ts` + - Runtime-facing wrapper around skill lifecycle/tool calls. +- `app/src/lib/skills/hooks.ts` + - Read hooks for snapshots and available skills. +- `app/src/lib/skills/sync.ts` + - Maps snapshots to tool sync payloads. +- `app/src/lib/skills/skillEvents.ts` + - Event emitter for local invalidation/re-fetch. +- `app/src/utils/desktopDeepLinkListener.ts` + - Handles deep links (including OAuth complete/error) and notifies runtime. +- `app/src/utils/config.ts` + - Frontend config values including `VITE_SKILLS_GITHUB_REPO`. + +### Rust core + +- `src/core/jsonrpc.rs` + - Core server startup and runtime bootstrap (`bootstrap_skill_runtime`). +- `src/openhuman/skills/schemas.rs` + - Controller schemas and handlers for `openhuman.skills_*` methods. +- `src/openhuman/skills/registry_ops.rs` + - Remote registry fetch/cache/search/install/uninstall/list logic. +- `src/openhuman/skills/registry_types.rs` + - Registry and available/installed type shapes. +- `src/openhuman/skills/qjs_engine.rs` + - Runtime engine for discovery/start/stop/rpc/tool execution. +- `src/openhuman/skills/manifest.rs` + - Manifest parsing and platform/runtime eligibility checks. +- `src/openhuman/skills/skill_registry.rs` + - Running skill registry, message routing, snapshots. +- `src/openhuman/skills/qjs_skill_instance/*` + - QuickJS instance lifecycle, event loop, JS handlers. +- `src/openhuman/skills/quickjs_libs/bootstrap.js` + - JS environment bootstrap and bridged APIs. +- `src/openhuman/skills/socket_manager.rs` + - Socket integration for tool sync and tool-call routing. +- `src/openhuman/skills/preferences.rs` + - Persisted per-skill preference state (enabled/setup flags). + +### Legacy path (non-authoritative for runtime execution) + +- `src/openhuman/skills/ops.rs` + - `workspace/skills` scanner for `skill.json` and `SKILL.md`. + +--- + +## 3) Skill Packaging and Storage + +The active runtime expects each skill directory to contain at minimum: + +- `manifest.json` +- JS entry file (usually `index.js`, but depends on manifest `entry`) + +Installed skills are written to: + +- `${workspace_dir}/skills//manifest.json` +- `${workspace_dir}/skills//` + +The runtime also has a skills data area: + +- `${base_dir}/skills_data//...` + +Where: + +- `base_dir` is `$OPENHUMAN_WORKSPACE` if set, otherwise `~/.openhuman` +- `workspace_dir` is `${base_dir}/workspace` + +--- + +## 4) Registry Fetch and Availability Flow + +### Registry source + +The core fetches a JSON registry from: + +- `SKILLS_REGISTRY_URL` if set, else +- default: + `https://raw.githubusercontent.com/tinyhumansai/openhuman-skills/refs/heads/build/skills/registry.json` + +### Caching + +The registry is cached to: + +- `${workspace_dir}/skills/.registry-cache.json` + +Cache TTL is one hour. + +### Availability API + +When UI calls `openhuman.skills_list_available`, core: + +1. Fetches or reads cached registry. +2. Scans installed skill directories under `workspace/skills`. +3. Merges both views: + - `installed` boolean + - `installed_version` + - `update_available` + +### Install API + +When UI calls `openhuman.skills_install`, core: + +1. Finds skill entry by ID in registry. +2. Downloads `manifest_url` and `download_url`. +3. Verifies checksum if `checksum_sha256` exists. +4. Writes files under `workspace/skills//`. + +Uninstall removes that directory. + +--- + +## 5) Runtime Bootstrap and Auto-Start + +On core startup, `bootstrap_skill_runtime()`: + +1. Resolves `base_dir`. +2. Creates `skills_data` directory. +3. Creates `RuntimeEngine`. +4. Sets `workspace_dir` on engine (`/workspace`). +5. Registers engine globally for RPC handlers. +6. Starts ping and cron schedulers. +7. Launches async auto-start. + +Auto-start behavior is driven by: + +- discovered manifests (`discover_skills`) +- manifest defaults (`auto_start`) +- preference overrides (`enable/disable` and setup state persistence) + +--- + +## 6) Discovery and Start Rules + +`discover_skills()` scans two locations: + +1. Runtime source directory (bundled/dev source path resolution). +2. Workspace installed directory (`workspace/skills`). + +For each candidate: + +- Reads `manifest.json` +- Requires JavaScript runtime compatibility +- Checks current platform compatibility +- Deduplicates by `manifest.id` + +`start_skill(skill_id)` behavior: + +1. Returns existing running/initializing snapshot if already active. +2. Resolves directory (source dir first, workspace fallback). +3. Validates manifest runtime/platform. +4. Creates a QuickJS skill instance. +5. Spawns event loop and registers skill in registry. +6. Runs lifecycle (`init`, then `start`). +7. Exposes current snapshot/tools/state. + +--- + +## 7) Runtime Message Model + +Most interactions become messages from engine to skill instance event loop. + +Typical operations: + +- `start` / `stop` +- generic rpc (`openhuman.skills_rpc`) +- tool call (`openhuman.skills_call_tool`) +- setup events (`setup/start`, `oauth/complete`) +- sync/tick events (`skill/tick`) + +Tool calls can be sync or async in JS. Async calls are awaited with runtime polling and timeout handling in the QuickJS event loop layer. + +--- + +## 8) JSON-RPC Surface (`openhuman.skills_*`) + +The skills controllers are registered in `src/openhuman/skills/schemas.rs`. + +Current method families: + +- Registry/catalog: + - `openhuman.skills_registry_fetch` + - `openhuman.skills_search` + - `openhuman.skills_list_available` + - `openhuman.skills_list_installed` + - `openhuman.skills_install` + - `openhuman.skills_uninstall` +- Runtime lifecycle/state: + - `openhuman.skills_discover` + - `openhuman.skills_list` + - `openhuman.skills_start` + - `openhuman.skills_stop` + - `openhuman.skills_status` + - `openhuman.skills_get_all_snapshots` +- Runtime actions: + - `openhuman.skills_list_tools` + - `openhuman.skills_call_tool` + - `openhuman.skills_rpc` + - `openhuman.skills_sync` + - `openhuman.skills_setup_start` +- Persistence/control: + - `openhuman.skills_enable` + - `openhuman.skills_disable` + - `openhuman.skills_is_enabled` + - `openhuman.skills_set_setup_complete` + - `openhuman.skills_data_read` + - `openhuman.skills_data_write` + - `openhuman.skills_data_dir` + +--- + +## 9) OAuth and Setup Completion Flow (Desktop) + +OAuth callback is handled in `desktopDeepLinkListener.ts`. + +For `openhuman://oauth/success?...`: + +1. Persist setup complete via `openhuman.skills_set_setup_complete`. +2. Ensure skill is running via `openhuman.skills_start`. +3. Send `oauth/complete` via `openhuman.skills_rpc`. +4. Trigger initial sync (`skillManager.triggerSync`). +5. Emit local skill-state refresh event. + +This keeps persistence, runtime, and UI in sync after browser-based auth. + +--- + +## 10) State and Snapshot Model + +Skill state can be published from JS via bridge APIs (`state.*` in bootstrap environment). + +Core tracks snapshots containing: + +- skill id/name/status +- tools +- runtime error (if any) +- published state map +- setup and connection status + +Frontend hooks (`useSkillSnapshot`, `useAllSkillSnapshots`, etc.) render from these snapshots and refresh on skill events. + +--- + +## 11) Tool Sync and Socket Integration + +Socket manager bridges runtime tool inventory and MCP-style calls. + +High-level pattern: + +1. Core publishes available tools from running skills. +2. Frontend/runtime sync maps snapshots to tool payload. +3. Incoming tool calls route to `skill_id` + `tool_name`. +4. Core executes via runtime and returns `ToolResult`. + +--- + +## 12) Environment Variables and Configuration + +### Core/runtime relevant + +- `SKILLS_REGISTRY_URL` + - Override skill catalog URL. +- `OPENHUMAN_WORKSPACE` + - Sets base workspace root (`skills_data`, `workspace/skills`, config). +- `OPENHUMAN_CORE_PORT` + - Core JSON-RPC HTTP port. +- `OPENHUMAN_CORE_RUN_MODE` + - Tauri core launch mode behavior. +- `OPENHUMAN_CORE_BIN` + - Override core binary path. + +### Frontend relevant + +- `VITE_SKILLS_GITHUB_REPO` + - UI-side repository slug default for skills registry context/display. + - Note: runtime fetch authority is still `SKILLS_REGISTRY_URL` in core. +- `VITE_OPENHUMAN_CORE_RPC_URL` / `OPENHUMAN_CORE_RPC_URL` + - Core RPC endpoint override for app client. + +--- + +## 13) End-to-End Sequence (Install + OAuth + Tool Call) + +1. User opens Skills screen. +2. UI calls `openhuman.skills_list_available`. +3. Core returns registry + installed/enriched availability. +4. User clicks install/connect. +5. UI calls `openhuman.skills_install`. +6. UI starts skill via `openhuman.skills_start`. +7. Skill initializes in QuickJS (`init` then `start`). +8. OAuth browser completes and deep links back. +9. UI marks setup complete, sends `oauth/complete`, triggers sync. +10. Agent or UI calls tool. +11. Core routes tool call to skill event loop and returns result. + +--- + +## 14) Debugging Guide + +### Common checks + +1. Registry errors: + - verify `SKILLS_REGISTRY_URL` + - inspect cache file under `workspace/skills/.registry-cache.json` +2. Install issues: + - check `manifest_url`/`download_url` accessibility + - validate checksum mismatch logs +3. Startup issues: + - ensure `manifest.json` exists and `runtime` is JS-compatible + - verify platform filter in manifest +4. OAuth issues: + - confirm deep-link callback includes `integrationId` and `skillId` + - verify `set_setup_complete` and `oauth/complete` RPCs are invoked +5. Tool-call failures: + - verify skill status is `Running` + - inspect skill error in snapshot + +### Useful runtime truths + +- Catalog truth: remote registry (+ cache) +- Installed truth: `workspace/skills/*/manifest.json` +- Running truth: runtime snapshots from `openhuman.skills_status` / `openhuman.skills_get_all_snapshots` + +--- + +## 15) Known Split-Brain Risks + +There is an intentional but risky overlap between: + +- QuickJS runtime manifests (`manifest.json`) and +- legacy loader semantics (`skill.json` + `SKILL.md`) + +Impact: + +- Different subsystems can report different views of "what skills exist." +- Documentation or migration work can accidentally target the wrong system. + +Recommendation: + +- Treat `openhuman.skills_*` + QuickJS manifests as canonical for execution paths. +- Keep legacy path use explicitly scoped until fully migrated. + +--- + +## 16) Testing Coverage Pointers + +- Registry/install/runtime e2e validations: + - `tests/json_rpc_e2e.rs` +- Core unit tests: + - `src/openhuman/skills/*` (registry/runtime modules) +- App integration points: + - `app/src/lib/skills/*` + - deep-link flow in `app/src/utils/desktopDeepLinkListener.ts` + +When changing behavior, test both: + +1. JSON-RPC behavior from core (`openhuman.skills_*` methods) +2. App orchestration behavior (especially OAuth/setup/sync) + +--- + +## 17) Practical Rules for Contributors + +- Put business/runtime behavior in Rust core. +- Keep frontend as orchestration and UX. +- Prefer adding/using explicit `openhuman.skills_*` methods over side channels. +- Preserve setup + enabled flags coherently across restarts. +- Avoid introducing new legacy skill metadata paths. +- Add traceable logs around install/start/setup/tool call boundaries. + diff --git a/openhuman-skills b/openhuman-skills deleted file mode 160000 index 52d58295c..000000000 --- a/openhuman-skills +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 52d58295c0cfbc22b5ad962fd7ae34f1efd7c000