Files
openhuman/src/lib/skills/runtime.ts
T
f4801726f3 Fix async tool calls not fetching data in QuickJS runtime (#102)
* Implement data synchronization feature in SkillManagementPanel

- Added a new `handleSync` function to trigger manual synchronization for skills, enhancing user control over data updates.
- Integrated skill state management to track synchronization progress, errors, and completion status.
- Updated the UI to display synchronization status, including progress indicators and error messages, improving user feedback during sync operations.
- Enhanced the SkillManager and SkillRuntime classes to support the new sync functionality, ensuring proper handling of sync requests and lifecycle hooks.

Updated subproject commit reference in the skills directory.

* Implement Conversations UI with two-panel thread layout and resizable sidebar

Add full Conversations feature: thread types, API service, Redux slice, and a
two-panel page with a draggable resize handle. Uncomment Conversations nav item
in MiniSidebar and enable prefix-based active state matching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add Intelligence page and integrate intelligence stats hook

- Introduced a new Intelligence page with a skill management interface, allowing users to view and manage skills.
- Implemented the `useIntelligenceStats` hook to fetch and display session statistics, memory files, and entity counts.
- Enhanced the SkillsGrid component to utilize shared skill icons and status displays, improving UI consistency.
- Added functionality for skill synchronization and management, including action buttons for skill setup and syncing.
- Updated the Conversations page to support sending messages with optimistic UI updates, enhancing user experience.

This commit lays the groundwork for a more interactive and informative intelligence management feature.

* Update subproject commit reference in skills directory and enhance fetch handling in bootstrap.js

- Updated the subproject commit reference in the skills directory to the latest version.
- Modified the fetch implementation in bootstrap.js to ensure options are sent as a JSON string and to parse the JSON response, improving data handling and error management.

* Update subproject commit reference in skills directory and comment out Conversations nav item in MiniSidebar

- Updated the subproject commit reference in the skills directory to the latest version.
- Commented out the Conversations navigation item in MiniSidebar for future implementation.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 17:56:47 +05:30

229 lines
6.0 KiB
TypeScript

/**
* Skill runtime — higher-level wrapper around SkillTransport
* for managing a single skill's lifecycle.
*
* Skills are managed by the Rust QuickJS runtime engine.
* This class wraps the transport layer to provide the same API
* that the SkillManager expects.
*/
import { invoke } from "@tauri-apps/api/core";
import { SkillTransport, type ReverseRpcHandler } from "./transport";
import type {
SkillManifest,
SetupStep,
SetupResult,
SkillToolDefinition,
SkillOptionDefinition,
PingResult,
} from "./types";
export class SkillRuntime {
private transport: SkillTransport;
private manifest: SkillManifest;
private _started = false;
constructor(manifest: SkillManifest) {
this.transport = new SkillTransport();
this.manifest = manifest;
}
/**
* Set a handler for reverse RPC calls from the skill process.
* Reverse RPC is handled by bridge globals, so this
* is kept for API compatibility.
*/
onReverseRpc(handler: ReverseRpcHandler): void {
this.transport.onReverseRpc(handler);
}
/**
* Start the skill in the QuickJS runtime engine.
* The Rust engine handles process management, so we just tell it to start
* and then initialize the transport for RPC routing.
*/
async start(): Promise<void> {
// Start the skill in the Rust QuickJS runtime
await invoke("runtime_start_skill", { skillId: this.manifest.id });
// Initialize the transport for RPC routing
await this.transport.start(this.manifest.id);
this._started = true;
}
/**
* Send skill/load with manifest + data dir.
* Loading is handled by the Rust engine during start_skill,
* so this sends a no-op skill/load RPC for protocol compatibility.
*/
async load(additionalParams?: Record<string, unknown>): Promise<void> {
const dataDir = await invoke<string>("runtime_skill_data_dir", {
skillId: this.manifest.id,
});
await this.transport.request("skill/load", {
manifest: this.manifest,
dataDir,
...(additionalParams || {}),
});
}
/**
* Start the setup flow. Returns the first SetupStep.
* Returns null if the skill does not implement setup/start (e.g. OAuth-only skills).
*/
async setupStart(): Promise<SetupStep | null> {
console.log("[SkillRuntime] setupStart", this.skillId);
const result = await this.transport.request<{ step: SetupStep } | null>(
"setup/start"
);
console.log("[SkillRuntime] setupStart result", this.skillId, result);
if (!result || !result.step) {
return null;
}
return result.step;
}
/**
* Submit a setup step. Returns SetupResult with next/error/complete.
*/
async setupSubmit(
stepId: string,
values: Record<string, unknown>
): Promise<SetupResult> {
return this.transport.request<SetupResult>("setup/submit", {
stepId,
values,
});
}
/**
* Cancel the setup flow.
*/
async setupCancel(): Promise<void> {
await this.transport.request("setup/cancel");
}
/**
* List available tools.
*/
async listTools(): Promise<SkillToolDefinition[]> {
const result = await this.transport.request<{
tools: SkillToolDefinition[];
}>("tools/list");
return result.tools;
}
/**
* List runtime-configurable options with current values.
*/
async listOptions(): Promise<SkillOptionDefinition[]> {
const result = await this.transport.request<{
options: SkillOptionDefinition[];
}>("options/list");
return result.options;
}
/**
* Set a single option value.
*/
async setOption(name: string, value: unknown): Promise<void> {
await this.transport.request("options/set", { name, value });
}
/**
* Call a tool by name with arguments.
*/
async callTool(
name: string,
args: Record<string, unknown>
): Promise<{
content: Array<{ type: string; text: string }>;
isError: boolean;
}> {
return this.transport.request("tools/call", { name, arguments: args });
}
/**
* Ping the skill to verify its external service connection is healthy.
* Returns null if the skill doesn't implement onPing (backward compatible).
*/
async ping(): Promise<PingResult | null> {
return this.transport.request<PingResult | null>("skill/ping");
}
/**
* Trigger the skill's onSync lifecycle hook.
* Progress updates flow via published state fields, not the RPC response.
*/
async triggerSync(): Promise<unknown> {
return this.transport.request("skill/sync");
}
/**
* Trigger periodic tick.
*/
async tick(): Promise<void> {
await this.transport.request("skill/tick");
}
/**
* Notify skill of session start.
*/
async sessionStart(sessionId: string): Promise<void> {
await this.transport.request("skill/sessionStart", { sessionId });
}
/**
* Notify skill of session end.
*/
async sessionEnd(sessionId: string): Promise<void> {
await this.transport.request("skill/sessionEnd", { sessionId });
}
/**
* Notify the skill that OAuth completed successfully.
* Sets the credential on the bridge and calls onOAuthComplete.
*/
async oauthComplete(args: {
credentialId: string;
provider: string;
grantedScopes?: string[];
accountLabel?: string;
}): Promise<void> {
await this.transport.request("oauth/complete", args as unknown as Record<string, unknown>);
}
/**
* Notify the skill that an OAuth credential was revoked.
*/
async oauthRevoked(args: {
credentialId: string;
reason: string;
}): Promise<void> {
await this.transport.request("oauth/revoked", args as unknown as Record<string, unknown>);
}
/**
* Unload and stop the skill.
*/
async stop(): Promise<void> {
if (!this._started) return;
try {
await this.transport.request("skill/shutdown");
} catch {
// Skill may already be stopped
}
await this.transport.kill();
this._started = false;
}
get isRunning(): boolean {
return this._started && this.transport.isRunning;
}
get skillId(): string {
return this.manifest.id;
}
}