mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Update @tauri-apps/api to version 2.10.0 in package.json and yarn.lock; add detailed documentation for memory inference flow in new markdown file; refactor Login component to remove unnecessary isWeb prop; enhance Skills component with improved skill filtering and synchronization UI.
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
# Skills → Memory Layer → Agent Inference: Full Flow
|
||||
|
||||
## Overview
|
||||
|
||||
This document traces the complete data flow from skill discovery and OAuth/sync events, through
|
||||
the TinyHumans Neocortex memory layer (tinyhumansai SDK), and into the Rust-side agentic
|
||||
inference loop — showing exactly how skill data is written to and read from memory and how it
|
||||
reaches the LLM context at inference time.
|
||||
|
||||
---
|
||||
|
||||
## 1. Skill Discovery & Lifecycle (SkillProvider.tsx)
|
||||
|
||||
**File**: `src/providers/SkillProvider.tsx`
|
||||
|
||||
On app mount (when a JWT token is present) `SkillProvider` calls
|
||||
`invoke('runtime_discover_skills')` → the Rust runtime scans
|
||||
`skills/skills/{skill-id}/manifest.json` from the git submodule and returns a manifest list.
|
||||
|
||||
```
|
||||
Token present
|
||||
→ discoverSkills()
|
||||
→ invoke('runtime_discover_skills') // Rust: qjs_engine.rs:184
|
||||
→ reads skills/skills/*/manifest.json
|
||||
→ filters: is_javascript() && supports_current_platform()
|
||||
→ returns manifests[]
|
||||
→ skillManager.registerSkill(manifest) // in-memory registry
|
||||
→ for each manifest with setupComplete:
|
||||
skillManager.startSkill(manifest) // starts V8/QuickJS instance
|
||||
```
|
||||
|
||||
Two Tauri event listeners run continuously:
|
||||
|
||||
| Event | What it does |
|
||||
|---|---|
|
||||
| `skill-state-changed` | Dispatches `setSkillState` into Redux `skillsSlice.skillStates[skillId]` |
|
||||
| `runtime:skill-status-changed` | Updates `skillsSlice.skills[skillId].status`; surfaces errors |
|
||||
|
||||
---
|
||||
|
||||
## 2. Memory Client Initialisation
|
||||
|
||||
**File**: `src-tauri/src/memory/mod.rs`
|
||||
**Tauri command**: `init_memory_client` (called by frontend after auth)
|
||||
|
||||
The `MemoryClient` wraps the `TinyHumansMemoryClient` from the `tinyhumansai` Rust crate.
|
||||
It is constructed with the user's JWT (`authSlice.token`) and stored as `Arc<MemoryClient>`
|
||||
in `MemoryState` (a `Mutex<Option<MemoryClientRef>>`).
|
||||
|
||||
Base URL resolution (in priority order):
|
||||
1. `ALPHAHUMAN_BASE_URL` env var
|
||||
2. `TINYHUMANS_BASE_URL` env var
|
||||
3. SDK default
|
||||
|
||||
---
|
||||
|
||||
## 3. Skill → Memory Sync: Two Write Paths
|
||||
|
||||
Both trigger inside `src-tauri/src/runtime/qjs_skill_instance.rs`.
|
||||
|
||||
### 3a. OAuth Completion (`skill/oauth-complete`)
|
||||
|
||||
When a user connects a skill via OAuth, the JS runtime calls back with `skill/oauth-complete`.
|
||||
After the OAuth flow completes the skill's **ops state** (data published via `state.set()`) is
|
||||
snapshotted and stored to memory:
|
||||
|
||||
```
|
||||
skill/oauth-complete handler
|
||||
→ handle_js_call(rt, ctx, "onOAuthComplete", params) // runs skill JS
|
||||
→ ops_state.read().data.clone() // snapshot skill state
|
||||
→ tokio::spawn (fire-and-forget):
|
||||
MemoryClient::store_skill_sync(
|
||||
skill_id = e.g. "gmail"
|
||||
integration_id = params["integrationId"] // e.g. user email
|
||||
title = "{skill} OAuth sync — {integrationId}"
|
||||
content = JSON snapshot of ops state
|
||||
namespace = "skill:{skill_id}:{integration_id}"
|
||||
)
|
||||
→ tinyhumansai::TinyHumansMemoryClient::insert_memory(InsertMemoryParams { ... })
|
||||
→ HTTP POST to TinyHumans Neocortex API
|
||||
```
|
||||
|
||||
### 3b. Periodic Sync (`skill/sync`)
|
||||
|
||||
The runtime fires `skill/sync` events on a cron schedule. The flow is identical to OAuth
|
||||
completion but uses `integration_id = "default"` and title `"{skill} periodic sync"`:
|
||||
|
||||
```
|
||||
skill/sync handler
|
||||
→ handle_js_call(rt, ctx, "onSync", "{}")
|
||||
→ ops_state.read().data.clone()
|
||||
→ tokio::spawn:
|
||||
MemoryClient::store_skill_sync(
|
||||
skill_id = e.g. "notion"
|
||||
integration_id = "default"
|
||||
namespace = "skill:notion:default"
|
||||
content = JSON snapshot
|
||||
)
|
||||
```
|
||||
|
||||
### Namespace Pattern
|
||||
|
||||
All skill memories are stored under `skill:{skill_id}:{integration_id}`.
|
||||
Examples: `skill:gmail:user@example.com`, `skill:notion:default`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Memory Operations Reference
|
||||
|
||||
**File**: `src-tauri/src/memory/mod.rs`
|
||||
|
||||
| Method | SDK call | When used |
|
||||
|---|---|---|
|
||||
| `store_skill_sync(...)` | `insert_memory` | OAuth complete, periodic sync |
|
||||
| `query_skill_context(...)` | `query_memory` | RAG query — fetch relevant chunks for a user question |
|
||||
| `recall_skill_context(...)` | `recall_memory` | Recall synthesised summary from Master node |
|
||||
| `clear_skill_memory(...)` | `delete_memory` | OAuth revoke / disconnect |
|
||||
|
||||
---
|
||||
|
||||
## 5. Conversation → Inference: Two Code Paths
|
||||
|
||||
**File**: `src/pages/Conversations.tsx`
|
||||
|
||||
`useRustChat()` returns `true` when running in Tauri (desktop). This selects between two paths:
|
||||
|
||||
```
|
||||
handleSendMessage(text)
|
||||
├─ rustChat == true → Rust path (invoke chat_send)
|
||||
└─ rustChat == false → Web path (handleSendMessageWeb — TypeScript loop)
|
||||
```
|
||||
|
||||
### 5a. Rust Path (Desktop)
|
||||
|
||||
```
|
||||
chatSend({ threadId, message, model, authToken, backendUrl, messages, notionContext })
|
||||
→ invoke('chat_send') // src-tauri/src/commands/chat.rs:359
|
||||
→ spawns background task
|
||||
→ chat_send_inner(...)
|
||||
```
|
||||
|
||||
Completion events flow back over Tauri events:
|
||||
|
||||
| Event | Frontend handler |
|
||||
|---|---|
|
||||
| `chat:tool_call` | shows active tool indicator |
|
||||
| `chat:tool_result` | clears tool indicator |
|
||||
| `chat:done` | `dispatch(addInferenceResponse(...))` |
|
||||
| `chat:error` | shows error, clears loading state |
|
||||
|
||||
### 5b. Web/Fallback Path
|
||||
|
||||
Used when not running in Tauri (browser). Runs the agentic loop entirely in TypeScript:
|
||||
- Calls `inferenceApi.createChatCompletion(request)` directly
|
||||
- Executes tools via `skillManager.callTool(skillId, toolName, args)`
|
||||
- Both paths share the same 5-round `MAX_TOOL_ROUNDS` limit and `{skillId}__{toolName}` naming convention
|
||||
|
||||
---
|
||||
|
||||
## 6. Rust Agentic Loop in Detail
|
||||
|
||||
**File**: `src-tauri/src/commands/chat.rs` — `chat_send_inner()`
|
||||
|
||||
```
|
||||
Step 1: Load OpenClaw context
|
||||
→ load_openclaw_context(app)
|
||||
→ reads ai/SOUL.md, IDENTITY.md, AGENTS.md, USER.md, BOOTSTRAP.md, MEMORY.md, TOOLS.md
|
||||
→ cached in static AI_CONFIG_CACHE (cleared on restart)
|
||||
→ truncated to MAX_CONTEXT_CHARS (20,000 chars)
|
||||
|
||||
Step 2: Recall memory context
|
||||
→ MemoryClient::recall_skill_context("conversations", thread_id, 10)
|
||||
→ tinyhumansai recall_memory(namespace="skill:conversations:{thread_id}")
|
||||
→ returns synthesised summary string or None
|
||||
|
||||
Step 3: Build processed user message
|
||||
processed = user_message
|
||||
if openclaw_context → prepend as "## Project Context\n...\n\nUser message: {processed}"
|
||||
if memory_context → prepend as "[MEMORY_CONTEXT]\n{mem}\n[/MEMORY_CONTEXT]\n\n{processed}"
|
||||
if notion_context → prepend as "{notionContext}\n\n{processed}"
|
||||
|
||||
Step 4: Build messages array
|
||||
→ history (ChatMessagePayload[]) + processed user message
|
||||
|
||||
Step 5: Discover tools
|
||||
→ engine.all_tools()
|
||||
→ returns all tools from running skills
|
||||
→ namespaced: "{skill_id}__{tool_name}"
|
||||
→ formatted as OpenAI function-calling schema
|
||||
|
||||
Step 6: Agentic loop (max 5 rounds)
|
||||
for round in 0..MAX_TOOL_ROUNDS:
|
||||
POST {backend_url}/openai/v1/chat/completions
|
||||
body: { model, messages, tools, tool_choice: "auto" }
|
||||
timeout: 120s
|
||||
auth: Bearer {auth_token}
|
||||
|
||||
if finish_reason == "tool_calls":
|
||||
emit chat:tool_call
|
||||
engine.call_tool(skill_id, tool_name, args) // 60s timeout
|
||||
→ QuickJS/V8 runtime executes skill JS tool handler
|
||||
emit chat:tool_result
|
||||
append tool result to messages
|
||||
continue loop
|
||||
|
||||
else (finish_reason == "stop"):
|
||||
emit chat:done { full_response, rounds_used, token_counts }
|
||||
return Ok(())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Tool Execution: Rust → Skill JS
|
||||
|
||||
**File**: `src-tauri/src/runtime/qjs_engine.rs` — `call_tool(skill_id, tool_name, args)`
|
||||
|
||||
The Rust runtime routes `call_tool` into the running QuickJS/V8 skill instance:
|
||||
- Serialises `args` as JSON
|
||||
- Calls the JS tool handler registered by the skill
|
||||
- Returns `ToolCallResult { content: Vec<ToolContent>, is_error: bool }`
|
||||
- Text content is extracted and appended as the `tool` role message in the loop
|
||||
|
||||
---
|
||||
|
||||
## 8. End-to-End Flow Summary
|
||||
|
||||
```
|
||||
User types message (Conversations.tsx)
|
||||
│
|
||||
├─ [Web path only] invoke('recall_memory') → TinyHumans API (recall)
|
||||
│ returns synthesised context
|
||||
├─ buildNotionContext() → from Redux skillStates.notion
|
||||
│
|
||||
▼
|
||||
invoke('chat_send') [Rust/desktop path]
|
||||
│
|
||||
▼
|
||||
Rust: chat_send_inner()
|
||||
├─ load_openclaw_context() → ai/*.md files (cached)
|
||||
├─ recall_skill_context("conversations", tid) → TinyHumans API (recall)
|
||||
├─ Build prompt: [OpenClaw] + [MEMORY] + [Notion] + user_message
|
||||
├─ discover_tools() → all running skill tools
|
||||
│
|
||||
└─ Agentic loop (≤5 rounds):
|
||||
POST /openai/v1/chat/completions → Backend LLM (neocortex-mk1)
|
||||
│
|
||||
├─ tool_calls → engine.call_tool() → QuickJS/V8 skill instance
|
||||
│ └─ JS tool handler
|
||||
│ └─ returns result string
|
||||
│ └─ appended as tool message
|
||||
│
|
||||
└─ stop → emit chat:done
|
||||
└─ Frontend: dispatch(addInferenceResponse(...))
|
||||
|
||||
Separately (async, fire-and-forget):
|
||||
Skill onSync() / onOAuthComplete()
|
||||
→ MemoryClient::store_skill_sync()
|
||||
→ TinyHumans insert_memory (namespace: skill:{id}:{integrationId})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/providers/SkillProvider.tsx` | Discovery, lifecycle, Redux state sync |
|
||||
| `src/pages/Conversations.tsx` | Message send, both code paths, Notion context |
|
||||
| `src/services/chatService.ts` | `chatSend()`, `chatCancel()`, `useRustChat()` |
|
||||
| `src-tauri/src/commands/chat.rs` | Rust agentic loop, context assembly, tool dispatch |
|
||||
| `src-tauri/src/commands/memory.rs` | `recall_memory` Tauri command |
|
||||
| `src-tauri/src/memory/mod.rs` | `MemoryClient` wrapping tinyhumansai SDK |
|
||||
| `src-tauri/src/runtime/qjs_skill_instance.rs` | Skill sync → memory write triggers |
|
||||
| `src-tauri/src/runtime/qjs_engine.rs` | `discover_skills()`, `call_tool()`, `all_tools()` |
|
||||
| `skills/skills/*/manifest.json` | Skill metadata (git submodule) |
|
||||
+1
-1
@@ -47,7 +47,7 @@
|
||||
"@scure/bip32": "^2.0.1",
|
||||
"@scure/bip39": "^2.0.1",
|
||||
"@sentry/react": "^10.38.0",
|
||||
"@tauri-apps/api": "2.9.1",
|
||||
"@tauri-apps/api": "^2.10.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
|
||||
@@ -525,21 +525,76 @@ async fn chat_send_inner(
|
||||
let openclaw_context = load_openclaw_context(app);
|
||||
|
||||
// ── Step 2: Recall memory context ───────────────────────────────────
|
||||
log::info!("[chat] Recalling conversation memory (thread_id={thread_id})");
|
||||
let memory_context: Option<String> = if let Some(ref mem) = memory_client {
|
||||
match mem
|
||||
.recall_skill_context("conversations", thread_id, 10)
|
||||
.await
|
||||
{
|
||||
Ok(ctx) => ctx,
|
||||
Ok(ctx) => {
|
||||
log::info!(
|
||||
"[chat] Conversation memory recall: has_data={}, len={}",
|
||||
ctx.is_some(),
|
||||
ctx.as_deref().map(|s| s.len()).unwrap_or(0)
|
||||
);
|
||||
if let Some(ref data) = ctx {
|
||||
log::debug!("[chat] Conversation memory content:\n{}", data);
|
||||
}
|
||||
ctx
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("[chat] Memory recall failed: {}", e);
|
||||
log::warn!("[chat] Conversation memory recall failed: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("[chat] No memory client — skipping conversation memory recall");
|
||||
None
|
||||
};
|
||||
|
||||
// ── Step 2b: Recall skill contexts ──────────────────────────────────
|
||||
let skill_ids: std::collections::HashSet<String> = engine
|
||||
.all_tools()
|
||||
.into_iter()
|
||||
.map(|(skill_id, _)| skill_id)
|
||||
.collect();
|
||||
|
||||
log::info!("[chat] Recalling skill contexts for {} skill(s): {:?}", skill_ids.len(), skill_ids);
|
||||
|
||||
let mut skill_contexts: Vec<String> = Vec::new();
|
||||
for sid in &skill_ids {
|
||||
if let Some(ref mem) = memory_client {
|
||||
log::info!("[chat] Recalling memory for skill={sid}");
|
||||
match mem.recall_skill_context(sid, sid, 10).await {
|
||||
Ok(Some(ctx)) => {
|
||||
log::info!(
|
||||
"[chat] Skill memory recall ok: skill={sid}, len={}",
|
||||
ctx.len()
|
||||
);
|
||||
log::debug!("[chat] Skill memory content (skill={sid}):\n{}", ctx);
|
||||
skill_contexts.push(format!(
|
||||
"[{}_CONTEXT]\n{}\n[/{}_CONTEXT]",
|
||||
sid.to_uppercase(),
|
||||
ctx,
|
||||
sid.to_uppercase()
|
||||
));
|
||||
}
|
||||
Ok(None) => {
|
||||
log::info!("[chat] Skill memory recall: no data for skill={sid}");
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("[chat] Skill memory recall failed for skill={sid}: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"[chat] Context assembly: conversation_memory={}, skill_contexts={}",
|
||||
memory_context.is_some(),
|
||||
skill_contexts.len()
|
||||
);
|
||||
|
||||
// ── Step 3: Build processed user message ────────────────────────────
|
||||
let mut processed = user_message.to_string();
|
||||
|
||||
@@ -554,6 +609,10 @@ async fn chat_send_inner(
|
||||
);
|
||||
}
|
||||
|
||||
if !skill_contexts.is_empty() {
|
||||
processed = format!("{}\n\n{}", skill_contexts.join("\n\n"), processed);
|
||||
}
|
||||
|
||||
if let Some(ref notion) = notion_context {
|
||||
processed = format!("{}\n\n{}", notion, processed);
|
||||
}
|
||||
|
||||
+96
-22
@@ -7,7 +7,7 @@
|
||||
use std::sync::Arc;
|
||||
use tinyhumansai::{
|
||||
DeleteMemoryParams, InsertMemoryParams, Priority, QueryMemoryParams, RecallMemoryParams,
|
||||
SourceType, TinyHumanConfig, TinyHumanMemoryClient,
|
||||
SourceType, TinyHumanConfig, TinyHumansMemoryClient,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -53,7 +53,9 @@ impl MemoryClient {
|
||||
|
||||
/// Store a skill data-sync result.
|
||||
///
|
||||
/// Namespace pattern: `skill:{skill_id}:{integration_id}`
|
||||
/// Inserts the document then polls `ingestion_job_status` every 30 s until
|
||||
/// the job reaches `completed` (or `failed`/`error`). Returns only after the
|
||||
/// ingestion job is confirmed complete.
|
||||
pub async fn store_skill_sync(
|
||||
&self,
|
||||
skill_id: &str,
|
||||
@@ -67,16 +69,23 @@ impl MemoryClient {
|
||||
updated_at: Option<f64>,
|
||||
document_id: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let namespace = format!("skill:{skill_id}:{integration_id}");
|
||||
log::info!("[memory] store_skill_sync: entry (namespace={namespace}, title={title:?}, content_len={})", content.len());
|
||||
log::debug!(
|
||||
"[memory] store_skill_sync: payload → namespace={namespace} | title={title} | content={}",
|
||||
content
|
||||
let namespace = skill_id.to_string();
|
||||
log::info!(
|
||||
"[memory] store_skill_sync: entry (namespace={namespace}, title={title:?}, content_len={})",
|
||||
content.len()
|
||||
);
|
||||
log::info!("[memory] insert_memory: calling SDK (namespace={namespace}, title={title:?}), content_len={}", content.len());
|
||||
let result = self.inner
|
||||
|
||||
let document_id_final = document_id.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
|
||||
log::info!(
|
||||
"[memory] insert_memory: calling SDK (namespace={namespace}, title={title:?}), content_len={}",
|
||||
content.len()
|
||||
);
|
||||
|
||||
let insert_resp = self
|
||||
.inner
|
||||
.insert_memory(InsertMemoryParams {
|
||||
document_id: Some(Uuid::new_v4().to_string()),
|
||||
document_id: Some(document_id_final),
|
||||
title: title.to_string(),
|
||||
content: content.to_string(),
|
||||
namespace: namespace.clone(),
|
||||
@@ -85,22 +94,86 @@ impl MemoryClient {
|
||||
priority,
|
||||
created_at,
|
||||
updated_at,
|
||||
document_id,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.map(|_| {
|
||||
log::info!("[memory] insert_memory: success (namespace={namespace}, title={title:?})");
|
||||
})
|
||||
.map_err(|e| {
|
||||
log::warn!("[memory] insert_memory: SDK error — kind={:?} msg={e}", classify_insert_error(&e));
|
||||
log::warn!(
|
||||
"[memory] insert_memory: SDK error — kind={:?} msg={e}",
|
||||
classify_insert_error(&e)
|
||||
);
|
||||
format!("Memory insert failed: {e}")
|
||||
});
|
||||
match &result {
|
||||
Ok(()) => log::info!("[memory] store_skill_sync: exit — ok (namespace={namespace})"),
|
||||
Err(e) => log::warn!("[memory] store_skill_sync: exit — error (namespace={namespace}): {e}"),
|
||||
})?;
|
||||
|
||||
log::info!(
|
||||
"[memory] insert_memory: accepted (namespace={namespace}, status={:?}, job_id={:?})",
|
||||
insert_resp.data.status,
|
||||
insert_resp.data.job_id
|
||||
);
|
||||
|
||||
// If the API returned a job_id, poll until the job completes.
|
||||
if let Some(job_id) = insert_resp.data.job_id {
|
||||
log::info!("[memory] ingestion job queued (job_id={job_id}), polling every 30s...");
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||
|
||||
match self.inner.ingestion_job_status(&job_id).await {
|
||||
Ok(status_resp) => {
|
||||
let state = status_resp
|
||||
.data
|
||||
.state
|
||||
.as_deref()
|
||||
.unwrap_or("unknown");
|
||||
|
||||
log::info!(
|
||||
"[memory] ingestion job status: job_id={job_id}, state={state}, \
|
||||
attempts={:?}, completed_at={:?}",
|
||||
status_resp.data.attempts,
|
||||
status_resp.data.completed_at
|
||||
);
|
||||
|
||||
match state {
|
||||
"completed" => {
|
||||
log::info!(
|
||||
"[memory] ingestion job completed (job_id={job_id}, namespace={namespace})"
|
||||
);
|
||||
break;
|
||||
}
|
||||
"failed" | "error" => {
|
||||
let err_msg = status_resp
|
||||
.data
|
||||
.error
|
||||
.unwrap_or_else(|| format!("job state={state}"));
|
||||
log::warn!(
|
||||
"[memory] ingestion job failed: job_id={job_id}, error={err_msg}"
|
||||
);
|
||||
log::warn!(
|
||||
"[memory] store_skill_sync: exit — ingestion failed (namespace={namespace})"
|
||||
);
|
||||
return Err(format!("Ingestion job failed: {err_msg}"));
|
||||
}
|
||||
_ => {
|
||||
// pending / processing / queued — keep waiting
|
||||
log::info!(
|
||||
"[memory] ingestion job still in progress (state={state}), waiting 30s..."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"[memory] ingestion job status poll error (job_id={job_id}): {e} — retrying in 30s"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("[memory] no job_id returned — insert assumed synchronous, proceeding");
|
||||
}
|
||||
result
|
||||
|
||||
log::info!("[memory] store_skill_sync: exit — ok (namespace={namespace})");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Query relevant context for a skill integration (RAG).
|
||||
@@ -139,10 +212,10 @@ impl MemoryClient {
|
||||
pub async fn recall_skill_context(
|
||||
&self,
|
||||
skill_id: &str,
|
||||
integration_id: &str,
|
||||
_integration_id: &str,
|
||||
max_chunks: u32,
|
||||
) -> Result<Option<String>, String> {
|
||||
let namespace = format!("skill:{skill_id}:{integration_id}");
|
||||
let namespace = skill_id.to_string();
|
||||
log::info!(
|
||||
"[memory] recall_skill_context: entry (namespace={namespace}, max_chunks={max_chunks})"
|
||||
);
|
||||
@@ -255,6 +328,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -583,50 +583,7 @@ async fn handle_message(
|
||||
}).await;
|
||||
log::info!("[skill:{}] OAuth credential set and persisted to store", skill_id);
|
||||
let params_str = serde_json::to_string(¶ms).unwrap_or_else(|_| "{}".to_string());
|
||||
let result = handle_js_call(rt, ctx, "onOAuthComplete", ¶ms_str).await;
|
||||
|
||||
// Fire-and-forget: persist published ops state to TinyHumans memory.
|
||||
// Skills publish data via state.set()/setPartial() into ops_state.data,
|
||||
// not as the return value of onOAuthComplete() (which is typically undefined).
|
||||
let state_snapshot = ops_state.read().data.clone();
|
||||
if !state_snapshot.is_empty() {
|
||||
if let Some(client) = memory_client_opt.clone() {
|
||||
let skill = skill_id.to_string();
|
||||
let integration_id = params
|
||||
.get("integrationId")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let content = serde_json::to_string_pretty(
|
||||
&serde_json::Value::Object(state_snapshot),
|
||||
)
|
||||
.unwrap_or_else(|_| "{}".to_string());
|
||||
let title = format!("{} OAuth sync — {}", skill, integration_id);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = client
|
||||
.store_skill_sync(
|
||||
&skill,
|
||||
&integration_id,
|
||||
&title,
|
||||
&content,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::warn!("[memory] store_skill_sync failed: {e}");
|
||||
} else {
|
||||
log::info!("[memory] Stored sync for {}:{}", skill, integration_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
handle_js_call(rt, ctx, "onOAuthComplete", ¶ms_str).await
|
||||
}
|
||||
"skill/ping" => {
|
||||
handle_js_call(rt, ctx, "onPing", "{}").await
|
||||
|
||||
+2
-2
@@ -8,11 +8,11 @@ import Conversations from './pages/Conversations';
|
||||
import Home from './pages/Home';
|
||||
import Intelligence from './pages/Intelligence';
|
||||
import Invites from './pages/Invites';
|
||||
import Skills from './pages/Skills';
|
||||
import Login from './pages/Login';
|
||||
import Mnemonic from './pages/Mnemonic';
|
||||
import Onboarding from './pages/onboarding/Onboarding';
|
||||
import Settings from './pages/Settings';
|
||||
import Skills from './pages/Skills';
|
||||
import Welcome from './pages/Welcome';
|
||||
import { selectHasEncryptionKey, selectIsOnboarded } from './store/authSelectors';
|
||||
import { useAppSelector } from './store/hooks';
|
||||
@@ -69,7 +69,7 @@ const AppRoutes = () => {
|
||||
path="/login"
|
||||
element={
|
||||
<PublicRoute>
|
||||
<Login isWeb={isWeb} />
|
||||
<Login />
|
||||
</PublicRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
+3
-3
@@ -2,11 +2,11 @@ import DownloadScreen from '../components/DownloadScreen';
|
||||
import OAuthLoginSection from '../components/oauth/OAuthLoginSection';
|
||||
import TypewriterGreeting from '../components/TypewriterGreeting';
|
||||
|
||||
interface WelcomeProps {
|
||||
isWeb: boolean;
|
||||
interface LoginProps {
|
||||
isWeb?: boolean;
|
||||
}
|
||||
|
||||
const Login = ({ isWeb }: WelcomeProps) => {
|
||||
const Login = ({ isWeb }: LoginProps) => {
|
||||
const greetings = ['Hello HAL9000! 👋', "Let's cook! 🔥", 'The A-Team is here! 👊'];
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,9 +14,9 @@ import SkillSetupModal from '../components/skills/SkillSetupModal';
|
||||
import { deriveConnectionStatus, useSkillConnectionStatus } from '../lib/skills/hooks';
|
||||
import { skillManager } from '../lib/skills/manager';
|
||||
import type { SkillConnectionStatus, SkillHostConnectionState } from '../lib/skills/types';
|
||||
import { deriveSkillSyncUiState } from './skillsSyncUi';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { IS_DEV } from '../utils/config';
|
||||
import { deriveSkillSyncUiState } from './skillsSyncUi';
|
||||
|
||||
/** Format large numbers: 1200 → "1.2K", 1200000 → "1.2M" */
|
||||
function formatNumber(n: number): string {
|
||||
@@ -53,7 +53,10 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
|
||||
| (SkillHostConnectionState & Record<string, unknown>)
|
||||
| undefined;
|
||||
const [manualSyncing, setManualSyncing] = useState(false);
|
||||
const syncUi = useMemo(() => deriveSkillSyncUiState(skill.id, skillState), [skill.id, skillState]);
|
||||
const syncUi = useMemo(
|
||||
() => deriveSkillSyncUiState(skill.id, skillState),
|
||||
[skill.id, skillState]
|
||||
);
|
||||
const isSyncing = manualSyncing || syncUi.isSyncing;
|
||||
|
||||
const handleSync = async (e: React.MouseEvent) => {
|
||||
@@ -212,10 +215,11 @@ export default function Skills() {
|
||||
}
|
||||
|
||||
const manifests = await invoke<Array<Record<string, unknown>>>('runtime_discover_skills');
|
||||
const ALLOWED_SKILLS = new Set(['gmail', 'notion']);
|
||||
const validManifests = manifests.filter(m => {
|
||||
const id = m.id as string;
|
||||
if (id.includes('_')) return false;
|
||||
return true;
|
||||
return ALLOWED_SKILLS.has(id);
|
||||
});
|
||||
|
||||
const processed: SkillListEntry[] = validManifests
|
||||
@@ -300,11 +304,7 @@ export default function Skills() {
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{sortedSkillsList.map(skill => (
|
||||
<SkillCard
|
||||
key={skill.id}
|
||||
skill={skill}
|
||||
onSetup={() => openSkillSetup(skill)}
|
||||
/>
|
||||
<SkillCard key={skill.id} skill={skill} onSetup={() => openSkillSetup(skill)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -326,7 +326,6 @@ export default function Skills() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1299,7 +1299,12 @@
|
||||
dependencies:
|
||||
postcss-selector-parser "6.0.10"
|
||||
|
||||
"@tauri-apps/api@2.9.1", "@tauri-apps/api@^2.8.0":
|
||||
"@tauri-apps/api@^2.10.0":
|
||||
version "2.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.10.1.tgz#57c1bae6114ec33d977eb2b50dfefc25fa84fc93"
|
||||
integrity sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==
|
||||
|
||||
"@tauri-apps/api@^2.8.0":
|
||||
version "2.9.1"
|
||||
resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz"
|
||||
integrity sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==
|
||||
@@ -7443,16 +7448,7 @@ strict-event-emitter@^0.5.1:
|
||||
resolved "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz"
|
||||
integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -7551,14 +7547,7 @@ stringify-entities@^4.0.0:
|
||||
character-entities-html4 "^2.0.0"
|
||||
character-entities-legacy "^3.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -8408,7 +8397,7 @@ workerpool@^6.5.1:
|
||||
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
|
||||
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -8426,15 +8415,6 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user