diff --git a/CHANGELOG.md b/CHANGELOG.md index a3699440825..89d6b8e1597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai ### Breaking - Browser/Chrome MCP: remove the legacy Chrome extension relay path, bundled extension assets, `driver: "extension"`, and `browser.relayBindHost`. Run `openclaw doctor --fix` to migrate host-local browser config to `existing-session` / `user`; Docker, headless, sandbox, and remote browser flows still use raw CDP. (#47893) Thanks @vincentkoc. +- Plugins/runtime: remove the public `openclaw/extension-api` surface with no compatibility shim. Bundled plugins must use injected runtime for host-side operations (for example `api.runtime.agent.runEmbeddedPiAgent`) and any remaining direct imports must come from narrow `openclaw/plugin-sdk/*` subpaths instead of the monolithic SDK root. ### Fixes diff --git a/extensions/llm-task/src/llm-task-tool.test.ts b/extensions/llm-task/src/llm-task-tool.test.ts index 49feb7929ff..6d21ec69654 100644 --- a/extensions/llm-task/src/llm-task-tool.test.ts +++ b/extensions/llm-task/src/llm-task-tool.test.ts @@ -1,17 +1,11 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; - -vi.mock("openclaw/extension-api", () => { - return { - runEmbeddedPiAgent: vi.fn(async () => ({ - meta: { startedAt: Date.now() }, - payloads: [{ text: "{}" }], - })), - }; -}); - -import { runEmbeddedPiAgent } from "openclaw/extension-api"; import { createLlmTaskTool } from "./llm-task-tool.js"; +const runEmbeddedPiAgent = vi.fn(async () => ({ + meta: { startedAt: Date.now() }, + payloads: [{ text: "{}" }], +})); + // oxlint-disable-next-line typescript/no-explicit-any function fakeApi(overrides: any = {}) { return { @@ -22,7 +16,12 @@ function fakeApi(overrides: any = {}) { agents: { defaults: { workspace: "/tmp", model: { primary: "openai-codex/gpt-5.2" } } }, }, pluginConfig: {}, - runtime: { version: "test" }, + runtime: { + version: "test", + agent: { + runEmbeddedPiAgent, + }, + }, logger: { debug() {}, info() {}, warn() {}, error() {} }, registerTool() {}, ...overrides, diff --git a/extensions/llm-task/src/llm-task-tool.ts b/extensions/llm-task/src/llm-task-tool.ts index d79e0a51130..bcc422290c6 100644 --- a/extensions/llm-task/src/llm-task-tool.ts +++ b/extensions/llm-task/src/llm-task-tool.ts @@ -2,7 +2,6 @@ import fs from "node:fs/promises"; import path from "node:path"; import { Type } from "@sinclair/typebox"; import Ajv from "ajv"; -import { runEmbeddedPiAgent } from "openclaw/extension-api"; import { formatThinkingLevels, formatXHighModelHint, @@ -179,7 +178,7 @@ export function createLlmTaskTool(api: OpenClawPluginApi) { const sessionId = `llm-task-${Date.now()}`; const sessionFile = path.join(tmpDir, "session.json"); - const result = await runEmbeddedPiAgent({ + const result = await api.runtime.agent.runEmbeddedPiAgent({ sessionId, sessionFile, workspaceDir: api.config?.agents?.defaults?.workspace ?? process.cwd(), diff --git a/extensions/test-utils/plugin-runtime-mock.ts b/extensions/test-utils/plugin-runtime-mock.ts index 81e3fdedeec..19a17e0811a 100644 --- a/extensions/test-utils/plugin-runtime-mock.ts +++ b/extensions/test-utils/plugin-runtime-mock.ts @@ -1,6 +1,7 @@ import type { PluginRuntime } from "openclaw/plugin-sdk/test-utils"; import { removeAckReactionAfterReply, shouldAckReaction } from "openclaw/plugin-sdk/test-utils"; import { vi } from "vitest"; +import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../src/agents/defaults.js"; type DeepPartial = { [K in keyof T]?: T[K] extends (...args: never[]) => unknown @@ -39,6 +40,50 @@ export function createPluginRuntimeMock(overrides: DeepPartial = loadConfig: vi.fn(() => ({})) as unknown as PluginRuntime["config"]["loadConfig"], writeConfigFile: vi.fn() as unknown as PluginRuntime["config"]["writeConfigFile"], }, + agent: { + defaults: { + model: DEFAULT_MODEL, + provider: DEFAULT_PROVIDER, + }, + resolveAgentDir: vi.fn( + () => "/tmp/agent", + ) as unknown as PluginRuntime["agent"]["resolveAgentDir"], + resolveAgentWorkspaceDir: vi.fn( + () => "/tmp/workspace", + ) as unknown as PluginRuntime["agent"]["resolveAgentWorkspaceDir"], + resolveAgentIdentity: vi.fn(() => ({ + name: "test-agent", + })) as unknown as PluginRuntime["agent"]["resolveAgentIdentity"], + resolveThinkingDefault: vi.fn( + () => "off", + ) as unknown as PluginRuntime["agent"]["resolveThinkingDefault"], + runEmbeddedPiAgent: vi.fn().mockResolvedValue({ + payloads: [], + meta: {}, + }) as unknown as PluginRuntime["agent"]["runEmbeddedPiAgent"], + resolveAgentTimeoutMs: vi.fn( + () => 30_000, + ) as unknown as PluginRuntime["agent"]["resolveAgentTimeoutMs"], + ensureAgentWorkspace: vi + .fn() + .mockResolvedValue(undefined) as unknown as PluginRuntime["agent"]["ensureAgentWorkspace"], + session: { + resolveStorePath: vi.fn( + () => "/tmp/agent-sessions.json", + ) as unknown as PluginRuntime["agent"]["session"]["resolveStorePath"], + loadSessionStore: vi.fn( + () => ({}), + ) as unknown as PluginRuntime["agent"]["session"]["loadSessionStore"], + saveSessionStore: vi + .fn() + .mockResolvedValue( + undefined, + ) as unknown as PluginRuntime["agent"]["session"]["saveSessionStore"], + resolveSessionFilePath: vi.fn( + (sessionId: string) => `/tmp/${sessionId}.json`, + ) as unknown as PluginRuntime["agent"]["session"]["resolveSessionFilePath"], + }, + }, system: { enqueueSystemEvent: vi.fn() as unknown as PluginRuntime["system"]["enqueueSystemEvent"], requestHeartbeatNow: vi.fn() as unknown as PluginRuntime["system"]["requestHeartbeatNow"], diff --git a/extensions/voice-call/index.ts b/extensions/voice-call/index.ts index 7393fb03c9b..7d14270bcf8 100644 --- a/extensions/voice-call/index.ts +++ b/extensions/voice-call/index.ts @@ -180,6 +180,7 @@ const voiceCallPlugin = { runtimePromise = createVoiceCallRuntime({ config, coreConfig: api.config as CoreConfig, + agentRuntime: api.runtime.agent, ttsRuntime: api.runtime.tts, logger: api.logger, }); diff --git a/extensions/voice-call/src/core-bridge.ts b/extensions/voice-call/src/core-bridge.ts index 0425eef9dbd..13ed56302fe 100644 --- a/extensions/voice-call/src/core-bridge.ts +++ b/extensions/voice-call/src/core-bridge.ts @@ -1,6 +1,4 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/voice-call"; import type { VoiceCallTtsConfig } from "./config.js"; export type CoreConfig = { @@ -13,147 +11,4 @@ export type CoreConfig = { [key: string]: unknown; }; -type CoreAgentDeps = { - resolveAgentDir: (cfg: CoreConfig, agentId: string) => string; - resolveAgentWorkspaceDir: (cfg: CoreConfig, agentId: string) => string; - resolveAgentIdentity: ( - cfg: CoreConfig, - agentId: string, - ) => { name?: string | null } | null | undefined; - resolveThinkingDefault: (params: { - cfg: CoreConfig; - provider?: string; - model?: string; - }) => string; - runEmbeddedPiAgent: (params: { - sessionId: string; - sessionKey?: string; - messageProvider?: string; - sessionFile: string; - workspaceDir: string; - config?: CoreConfig; - prompt: string; - provider?: string; - model?: string; - thinkLevel?: string; - verboseLevel?: string; - timeoutMs: number; - runId: string; - lane?: string; - extraSystemPrompt?: string; - agentDir?: string; - }) => Promise<{ - payloads?: Array<{ text?: string; isError?: boolean }>; - meta?: { aborted?: boolean }; - }>; - resolveAgentTimeoutMs: (opts: { cfg: CoreConfig }) => number; - ensureAgentWorkspace: (params?: { dir: string }) => Promise; - resolveStorePath: (store?: string, opts?: { agentId?: string }) => string; - loadSessionStore: (storePath: string) => Record; - saveSessionStore: (storePath: string, store: Record) => Promise; - resolveSessionFilePath: ( - sessionId: string, - entry: unknown, - opts?: { agentId?: string }, - ) => string; - DEFAULT_MODEL: string; - DEFAULT_PROVIDER: string; -}; - -let coreRootCache: string | null = null; -let coreDepsPromise: Promise | null = null; - -function findPackageRoot(startDir: string, name: string): string | null { - let dir = startDir; - for (;;) { - const pkgPath = path.join(dir, "package.json"); - try { - if (fs.existsSync(pkgPath)) { - const raw = fs.readFileSync(pkgPath, "utf8"); - const pkg = JSON.parse(raw) as { name?: string }; - if (pkg.name === name) { - return dir; - } - } - } catch { - // ignore parse errors and keep walking - } - const parent = path.dirname(dir); - if (parent === dir) { - return null; - } - dir = parent; - } -} - -function resolveOpenClawRoot(): string { - if (coreRootCache) { - return coreRootCache; - } - const override = process.env.OPENCLAW_ROOT?.trim(); - if (override) { - coreRootCache = override; - return override; - } - - const candidates = new Set(); - if (process.argv[1]) { - candidates.add(path.dirname(process.argv[1])); - } - candidates.add(process.cwd()); - try { - const urlPath = fileURLToPath(import.meta.url); - candidates.add(path.dirname(urlPath)); - } catch { - // ignore - } - - for (const start of candidates) { - for (const name of ["openclaw"]) { - const found = findPackageRoot(start, name); - if (found) { - coreRootCache = found; - return found; - } - } - } - - throw new Error("Unable to resolve core root. Set OPENCLAW_ROOT to the package root."); -} - -async function importCoreExtensionAPI(): Promise<{ - resolveAgentDir: CoreAgentDeps["resolveAgentDir"]; - resolveAgentWorkspaceDir: CoreAgentDeps["resolveAgentWorkspaceDir"]; - DEFAULT_MODEL: string; - DEFAULT_PROVIDER: string; - resolveAgentIdentity: CoreAgentDeps["resolveAgentIdentity"]; - resolveThinkingDefault: CoreAgentDeps["resolveThinkingDefault"]; - runEmbeddedPiAgent: CoreAgentDeps["runEmbeddedPiAgent"]; - resolveAgentTimeoutMs: CoreAgentDeps["resolveAgentTimeoutMs"]; - ensureAgentWorkspace: CoreAgentDeps["ensureAgentWorkspace"]; - resolveStorePath: CoreAgentDeps["resolveStorePath"]; - loadSessionStore: CoreAgentDeps["loadSessionStore"]; - saveSessionStore: CoreAgentDeps["saveSessionStore"]; - resolveSessionFilePath: CoreAgentDeps["resolveSessionFilePath"]; -}> { - // Do not import any other module. You can't touch this or you will be fired. - const distPath = path.join(resolveOpenClawRoot(), "dist", "extensionAPI.js"); - if (!fs.existsSync(distPath)) { - throw new Error( - `Missing core module at ${distPath}. Run \`pnpm build\` or install the official package.`, - ); - } - return await import(pathToFileURL(distPath).href); -} - -export async function loadCoreAgentDeps(): Promise { - if (coreDepsPromise) { - return coreDepsPromise; - } - - coreDepsPromise = (async () => { - return await importCoreExtensionAPI(); - })(); - - return coreDepsPromise; -} +export type CoreAgentDeps = OpenClawPluginApi["runtime"]["agent"]; diff --git a/extensions/voice-call/src/response-generator.ts b/extensions/voice-call/src/response-generator.ts index abb02cb7b1d..3c8a45eadfb 100644 --- a/extensions/voice-call/src/response-generator.ts +++ b/extensions/voice-call/src/response-generator.ts @@ -4,14 +4,17 @@ */ import crypto from "node:crypto"; +import type { SessionEntry } from "openclaw/plugin-sdk/voice-call"; import type { VoiceCallConfig } from "./config.js"; -import { loadCoreAgentDeps, type CoreConfig } from "./core-bridge.js"; +import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js"; export type VoiceResponseParams = { /** Voice call config */ voiceConfig: VoiceCallConfig; /** Core OpenClaw config */ coreConfig: CoreConfig; + /** Injected host agent runtime */ + agentRuntime: CoreAgentDeps; /** Call ID for session tracking */ callId: string; /** Caller's phone number */ @@ -27,11 +30,6 @@ export type VoiceResponseResult = { error?: string; }; -type SessionEntry = { - sessionId: string; - updatedAt: number; -}; - /** * Generate a voice response using the embedded Pi agent with full tool support. * Uses the same agent infrastructure as messaging for consistent behavior. @@ -39,21 +37,11 @@ type SessionEntry = { export async function generateVoiceResponse( params: VoiceResponseParams, ): Promise { - const { voiceConfig, callId, from, transcript, userMessage, coreConfig } = params; + const { voiceConfig, callId, from, transcript, userMessage, coreConfig, agentRuntime } = params; if (!coreConfig) { return { text: null, error: "Core config unavailable for voice response" }; } - - let deps: Awaited>; - try { - deps = await loadCoreAgentDeps(); - } catch (err) { - return { - text: null, - error: err instanceof Error ? err.message : "Unable to load core agent dependencies", - }; - } const cfg = coreConfig; // Build voice-specific session key based on phone number @@ -62,15 +50,15 @@ export async function generateVoiceResponse( const agentId = "main"; // Resolve paths - const storePath = deps.resolveStorePath(cfg.session?.store, { agentId }); - const agentDir = deps.resolveAgentDir(cfg, agentId); - const workspaceDir = deps.resolveAgentWorkspaceDir(cfg, agentId); + const storePath = agentRuntime.session.resolveStorePath(cfg.session?.store, { agentId }); + const agentDir = agentRuntime.resolveAgentDir(cfg, agentId); + const workspaceDir = agentRuntime.resolveAgentWorkspaceDir(cfg, agentId); // Ensure workspace exists - await deps.ensureAgentWorkspace({ dir: workspaceDir }); + await agentRuntime.ensureAgentWorkspace({ dir: workspaceDir }); // Load or create session entry - const sessionStore = deps.loadSessionStore(storePath); + const sessionStore = agentRuntime.session.loadSessionStore(storePath); const now = Date.now(); let sessionEntry = sessionStore[sessionKey] as SessionEntry | undefined; @@ -80,25 +68,27 @@ export async function generateVoiceResponse( updatedAt: now, }; sessionStore[sessionKey] = sessionEntry; - await deps.saveSessionStore(storePath, sessionStore); + await agentRuntime.session.saveSessionStore(storePath, sessionStore); } const sessionId = sessionEntry.sessionId; - const sessionFile = deps.resolveSessionFilePath(sessionId, sessionEntry, { + const sessionFile = agentRuntime.session.resolveSessionFilePath(sessionId, sessionEntry, { agentId, }); // Resolve model from config - const modelRef = voiceConfig.responseModel || `${deps.DEFAULT_PROVIDER}/${deps.DEFAULT_MODEL}`; + const modelRef = + voiceConfig.responseModel || `${agentRuntime.defaults.provider}/${agentRuntime.defaults.model}`; const slashIndex = modelRef.indexOf("/"); - const provider = slashIndex === -1 ? deps.DEFAULT_PROVIDER : modelRef.slice(0, slashIndex); + const provider = + slashIndex === -1 ? agentRuntime.defaults.provider : modelRef.slice(0, slashIndex); const model = slashIndex === -1 ? modelRef : modelRef.slice(slashIndex + 1); // Resolve thinking level - const thinkLevel = deps.resolveThinkingDefault({ cfg, provider, model }); + const thinkLevel = agentRuntime.resolveThinkingDefault({ cfg, provider, model }); // Resolve agent identity for personalized prompt - const identity = deps.resolveAgentIdentity(cfg, agentId); + const identity = agentRuntime.resolveAgentIdentity(cfg, agentId); const agentName = identity?.name?.trim() || "assistant"; // Build system prompt with conversation history @@ -115,11 +105,11 @@ export async function generateVoiceResponse( } // Resolve timeout - const timeoutMs = voiceConfig.responseTimeoutMs ?? deps.resolveAgentTimeoutMs({ cfg }); + const timeoutMs = voiceConfig.responseTimeoutMs ?? agentRuntime.resolveAgentTimeoutMs({ cfg }); const runId = `voice:${callId}:${Date.now()}`; try { - const result = await deps.runEmbeddedPiAgent({ + const result = await agentRuntime.runEmbeddedPiAgent({ sessionId, sessionKey, messageProvider: "voice", diff --git a/extensions/voice-call/src/runtime.test.ts b/extensions/voice-call/src/runtime.test.ts index dcb8fa2a158..ffe9093c4e2 100644 --- a/extensions/voice-call/src/runtime.test.ts +++ b/extensions/voice-call/src/runtime.test.ts @@ -76,6 +76,7 @@ describe("createVoiceCallRuntime lifecycle", () => { createVoiceCallRuntime({ config: createBaseConfig(), coreConfig: {}, + agentRuntime: {} as never, }), ).rejects.toThrow("init failed"); @@ -95,6 +96,7 @@ describe("createVoiceCallRuntime lifecycle", () => { const runtime = await createVoiceCallRuntime({ config: createBaseConfig(), coreConfig: {} as CoreConfig, + agentRuntime: {} as never, }); await runtime.stop(); diff --git a/extensions/voice-call/src/runtime.ts b/extensions/voice-call/src/runtime.ts index d725e44bf06..384ac209a76 100644 --- a/extensions/voice-call/src/runtime.ts +++ b/extensions/voice-call/src/runtime.ts @@ -1,6 +1,6 @@ import type { VoiceCallConfig } from "./config.js"; import { resolveVoiceCallConfig, validateProviderConfig } from "./config.js"; -import type { CoreConfig } from "./core-bridge.js"; +import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js"; import { CallManager } from "./manager.js"; import type { VoiceCallProvider } from "./providers/base.js"; import { MockProvider } from "./providers/mock.js"; @@ -135,10 +135,11 @@ function resolveProvider(config: VoiceCallConfig): VoiceCallProvider { export async function createVoiceCallRuntime(params: { config: VoiceCallConfig; coreConfig: CoreConfig; + agentRuntime: CoreAgentDeps; ttsRuntime?: TelephonyTtsRuntime; logger?: Logger; }): Promise { - const { config: rawConfig, coreConfig, ttsRuntime, logger } = params; + const { config: rawConfig, coreConfig, agentRuntime, ttsRuntime, logger } = params; const log = logger ?? { info: console.log, warn: console.warn, @@ -165,7 +166,13 @@ export async function createVoiceCallRuntime(params: { const provider = resolveProvider(config); const manager = new CallManager(config); - const webhookServer = new VoiceCallWebhookServer(config, manager, provider, coreConfig); + const webhookServer = new VoiceCallWebhookServer( + config, + manager, + provider, + coreConfig, + agentRuntime, + ); const lifecycle = createRuntimeResourceLifecycle({ config, webhookServer }); const localUrl = await webhookServer.start(); diff --git a/extensions/voice-call/src/webhook.ts b/extensions/voice-call/src/webhook.ts index 1258229735e..2b79309c9f0 100644 --- a/extensions/voice-call/src/webhook.ts +++ b/extensions/voice-call/src/webhook.ts @@ -6,7 +6,7 @@ import { requestBodyErrorToText, } from "openclaw/plugin-sdk/voice-call"; import { normalizeVoiceCallConfig, type VoiceCallConfig } from "./config.js"; -import type { CoreConfig } from "./core-bridge.js"; +import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js"; import type { CallManager } from "./manager.js"; import type { MediaStreamConfig } from "./media-stream.js"; import { MediaStreamHandler } from "./media-stream.js"; @@ -55,6 +55,7 @@ export class VoiceCallWebhookServer { private manager: CallManager; private provider: VoiceCallProvider; private coreConfig: CoreConfig | null; + private agentRuntime: CoreAgentDeps | null; private stopStaleCallReaper: (() => void) | null = null; /** Media stream handler for bidirectional audio (when streaming enabled) */ @@ -65,11 +66,13 @@ export class VoiceCallWebhookServer { manager: CallManager, provider: VoiceCallProvider, coreConfig?: CoreConfig, + agentRuntime?: CoreAgentDeps, ) { this.config = normalizeVoiceCallConfig(config); this.manager = manager; this.provider = provider; this.coreConfig = coreConfig ?? null; + this.agentRuntime = agentRuntime ?? null; // Initialize media stream handler if streaming is enabled if (this.config.streaming.enabled) { @@ -458,6 +461,10 @@ export class VoiceCallWebhookServer { console.warn("[voice-call] Core config missing; skipping auto-response"); return; } + if (!this.agentRuntime) { + console.warn("[voice-call] Agent runtime missing; skipping auto-response"); + return; + } try { const { generateVoiceResponse } = await import("./response-generator.js"); @@ -465,6 +472,7 @@ export class VoiceCallWebhookServer { const result = await generateVoiceResponse({ voiceConfig: this.config, coreConfig: this.coreConfig, + agentRuntime: this.agentRuntime, callId, from: call.from, transcript: call.transcript, diff --git a/knip.config.ts b/knip.config.ts index 6a76a8238b7..9ceda2575d8 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -3,7 +3,6 @@ const rootEntries = [ "src/index.ts!", "src/entry.ts!", "src/cli/daemon-cli.ts!", - "src/extensionAPI.ts!", "src/infra/warning-filter.ts!", "src/channels/plugins/agent-tools/whatsapp-login.ts!", "src/channels/plugins/actions/discord.ts!", diff --git a/package.json b/package.json index a0b5e9581df..f0904418919 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,6 @@ "types": "./dist/plugin-sdk/keyed-async-queue.d.ts", "default": "./dist/plugin-sdk/keyed-async-queue.js" }, - "./extension-api": "./dist/extensionAPI.js", "./cli-entry": "./openclaw.mjs" }, "scripts": { diff --git a/scripts/test-extension.mjs b/scripts/test-extension.mjs index 84fd91b0436..eb7eef78925 100644 --- a/scripts/test-extension.mjs +++ b/scripts/test-extension.mjs @@ -76,7 +76,10 @@ export function detectChangedExtensionIds(changedPaths) { const extensionMatch = relativePath.match(/^extensions\/([^/]+)(?:\/|$)/); if (extensionMatch) { - extensionIds.add(extensionMatch[1]); + const extensionId = extensionMatch[1]; + if (hasExtensionPackage(extensionId)) { + extensionIds.add(extensionId); + } continue; } diff --git a/src/extensionAPI.ts b/src/extensionAPI.ts deleted file mode 100644 index 8b886dfef5a..00000000000 --- a/src/extensionAPI.ts +++ /dev/null @@ -1,14 +0,0 @@ -export { resolveAgentDir, resolveAgentWorkspaceDir } from "./agents/agent-scope.ts"; - -export { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./agents/defaults.ts"; -export { resolveAgentIdentity } from "./agents/identity.ts"; -export { resolveThinkingDefault } from "./agents/model-selection.ts"; -export { runEmbeddedPiAgent } from "./agents/pi-embedded.ts"; -export { resolveAgentTimeoutMs } from "./agents/timeout.ts"; -export { ensureAgentWorkspace } from "./agents/workspace.ts"; -export { - resolveStorePath, - loadSessionStore, - saveSessionStore, - resolveSessionFilePath, -} from "./config/sessions.ts"; diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index eff2820af79..7b15bcfce97 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -1,4 +1,3 @@ -import * as extensionApi from "openclaw/extension-api"; import * as compatSdk from "openclaw/plugin-sdk/compat"; import * as coreSdk from "openclaw/plugin-sdk/core"; import type { @@ -242,8 +241,4 @@ describe("plugin-sdk subpath exports", () => { const zalo = await import("openclaw/plugin-sdk/zalo"); expect(typeof zalo.resolveClientIp).toBe("function"); }); - - it("exports the extension api bridge", () => { - expect(typeof extensionApi.runEmbeddedPiAgent).toBe("function"); - }); }); diff --git a/src/plugin-sdk/voice-call.ts b/src/plugin-sdk/voice-call.ts index c50b979a145..7dea0885862 100644 --- a/src/plugin-sdk/voice-call.ts +++ b/src/plugin-sdk/voice-call.ts @@ -15,5 +15,6 @@ export { requestBodyErrorToText, } from "../infra/http-body.js"; export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; +export type { SessionEntry } from "../config/sessions/types.js"; export type { OpenClawPluginApi } from "../plugins/types.js"; export { sleep } from "../utils.js"; diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index a91b6c939ab..567568c3a1f 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -297,22 +297,6 @@ function createPluginSdkAliasFixture(params?: { return { root, srcFile, distFile }; } -function createExtensionApiAliasFixture(params?: { srcBody?: string; distBody?: string }) { - const root = makeTempDir(); - const srcFile = path.join(root, "src", "extensionAPI.ts"); - const distFile = path.join(root, "dist", "extensionAPI.js"); - mkdirSafe(path.dirname(srcFile)); - mkdirSafe(path.dirname(distFile)); - fs.writeFileSync( - path.join(root, "package.json"), - JSON.stringify({ name: "openclaw", type: "module" }, null, 2), - "utf-8", - ); - fs.writeFileSync(srcFile, params?.srcBody ?? "export {};\n", "utf-8"); - fs.writeFileSync(distFile, params?.distBody ?? "export {};\n", "utf-8"); - return { root, srcFile, distFile }; -} - function createPluginRuntimeAliasFixture(params?: { srcBody?: string; distBody?: string }) { const root = makeTempDir(); const srcFile = path.join(root, "src", "plugins", "runtime", "index.ts"); @@ -2125,6 +2109,7 @@ module.exports = { channels: { "setup-runtime-preferred-test": { enabled: true, + token: "configured", }, }, plugins: { @@ -2232,6 +2217,7 @@ module.exports = { channels: { "setup-runtime-not-preferred-test": { enabled: true, + token: "configured", }, }, plugins: { @@ -3205,26 +3191,6 @@ module.exports = { expect(resolved).toBe(srcFile); }); - it("prefers dist extension-api alias when loader runs from dist", () => { - const { root, distFile } = createExtensionApiAliasFixture(); - - const resolved = __testing.resolveExtensionApiAlias({ - modulePath: path.join(root, "dist", "plugins", "loader.js"), - }); - expect(resolved).toBe(distFile); - }); - - it("prefers src extension-api alias when loader runs from src in non-production", () => { - const { root, srcFile } = createExtensionApiAliasFixture(); - - const resolved = withEnv({ NODE_ENV: undefined }, () => - __testing.resolveExtensionApiAlias({ - modulePath: path.join(root, "src", "plugins", "loader.ts"), - }), - ); - expect(resolved).toBe(srcFile); - }); - it("resolves plugin-sdk alias from package root when loader runs from transpiler cache path", () => { const { root, srcFile } = createPluginSdkAliasFixture(); @@ -3239,18 +3205,6 @@ module.exports = { expect(resolved).toBe(srcFile); }); - it("resolves extension-api alias from package root when loader runs from transpiler cache path", () => { - const { root, srcFile } = createExtensionApiAliasFixture(); - - const resolved = withEnv({ NODE_ENV: undefined }, () => - __testing.resolveExtensionApiAlias({ - modulePath: "/tmp/tsx-cache/openclaw-loader.js", - argv1: path.join(root, "openclaw.mjs"), - }), - ); - expect(resolved).toBe(srcFile); - }); - it("resolves plugin runtime module from package root when loader runs from transpiler cache path", () => { const { root, srcFile } = createPluginRuntimeAliasFixture(); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index dc3bf5139c6..67432910a62 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -68,6 +68,7 @@ const openAllowlistWarningCache = new Set(); const LAZY_RUNTIME_REFLECTION_KEYS = [ "version", "config", + "agent", "subagent", "system", "media", @@ -197,34 +198,6 @@ const resolvePluginSdkAliasFile = (params: { const resolvePluginSdkAlias = (): string | null => resolvePluginSdkAliasFile({ srcFile: "root-alias.cjs", distFile: "root-alias.cjs" }); -const resolveExtensionApiAlias = (params: LoaderModuleResolveParams = {}): string | null => { - try { - const modulePath = resolveLoaderModulePath(params); - const packageRoot = resolveLoaderPackageRoot({ ...params, modulePath }); - if (!packageRoot) { - return null; - } - - const orderedKinds = resolvePluginSdkAliasCandidateOrder({ - modulePath, - isProduction: process.env.NODE_ENV === "production", - }); - const candidateMap = { - src: path.join(packageRoot, "src", "extensionAPI.ts"), - dist: path.join(packageRoot, "dist", "extensionAPI.js"), - } as const; - for (const kind of orderedKinds) { - const candidate = candidateMap[kind]; - if (fs.existsSync(candidate)) { - return candidate; - } - } - } catch { - // ignore - } - return null; -}; - function resolvePluginRuntimeModulePath(params: LoaderModuleResolveParams = {}): string | null { try { const modulePath = resolveLoaderModulePath(params); @@ -302,7 +275,6 @@ const resolvePluginSdkScopedAliasMap = (): Record => { export const __testing = { listPluginSdkAliasCandidates, listPluginSdkExportedSubpaths, - resolveExtensionApiAlias, resolvePluginSdkAliasCandidateOrder, resolvePluginSdkAliasFile, resolvePluginRuntimeModulePath, @@ -856,9 +828,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi return jitiLoader; } const pluginSdkAlias = resolvePluginSdkAlias(); - const extensionApiAlias = resolveExtensionApiAlias(); const aliasMap = { - ...(extensionApiAlias ? { "openclaw/extension-api": extensionApiAlias } : {}), ...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}), ...resolvePluginSdkScopedAliasMap(), }; diff --git a/src/plugins/runtime/index.test.ts b/src/plugins/runtime/index.test.ts index 5ec2df28199..4759b5813d7 100644 --- a/src/plugins/runtime/index.test.ts +++ b/src/plugins/runtime/index.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js"; import { onAgentEvent } from "../../infra/agent-events.js"; import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js"; import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; @@ -54,6 +55,17 @@ describe("plugin runtime command execution", () => { expect(runtime.system.requestHeartbeatNow).toBe(requestHeartbeatNow); }); + it("exposes runtime.agent host helpers", () => { + const runtime = createPluginRuntime(); + expect(runtime.agent.defaults).toEqual({ + model: DEFAULT_MODEL, + provider: DEFAULT_PROVIDER, + }); + expect(typeof runtime.agent.runEmbeddedPiAgent).toBe("function"); + expect(typeof runtime.agent.resolveAgentDir).toBe("function"); + expect(typeof runtime.agent.session.resolveSessionFilePath).toBe("function"); + }); + it("exposes runtime.modelAuth with getApiKeyForModel and resolveApiKeyForProvider", () => { const runtime = createPluginRuntime(); expect(runtime.modelAuth).toBeDefined(); diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts index 12d33168cd3..8d590899bf4 100644 --- a/src/plugins/runtime/index.ts +++ b/src/plugins/runtime/index.ts @@ -6,6 +6,7 @@ import { import { resolveStateDir } from "../../config/paths.js"; import { transcribeAudioFile } from "../../media-understanding/transcribe-audio.js"; import { textToSpeechTelephony } from "../../tts/tts.js"; +import { createRuntimeAgent } from "./runtime-agent.js"; import { createRuntimeChannel } from "./runtime-channel.js"; import { createRuntimeConfig } from "./runtime-config.js"; import { createRuntimeEvents } from "./runtime-events.js"; @@ -53,6 +54,7 @@ export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}): const runtime = { version: resolveVersion(), config: createRuntimeConfig(), + agent: createRuntimeAgent(), subagent: _options.subagent ?? createUnavailableSubagentRuntime(), system: createRuntimeSystem(), media: createRuntimeMedia(), diff --git a/src/plugins/runtime/runtime-agent.ts b/src/plugins/runtime/runtime-agent.ts new file mode 100644 index 00000000000..ae56d1e4bd3 --- /dev/null +++ b/src/plugins/runtime/runtime-agent.ts @@ -0,0 +1,36 @@ +import { resolveAgentDir, resolveAgentWorkspaceDir } from "../../agents/agent-scope.js"; +import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js"; +import { resolveAgentIdentity } from "../../agents/identity.js"; +import { resolveThinkingDefault } from "../../agents/model-selection.js"; +import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; +import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; +import { ensureAgentWorkspace } from "../../agents/workspace.js"; +import { + loadSessionStore, + resolveSessionFilePath, + resolveStorePath, + saveSessionStore, +} from "../../config/sessions.js"; +import type { PluginRuntime } from "./types.js"; + +export function createRuntimeAgent(): PluginRuntime["agent"] { + return { + defaults: { + model: DEFAULT_MODEL, + provider: DEFAULT_PROVIDER, + }, + resolveAgentDir, + resolveAgentWorkspaceDir, + resolveAgentIdentity, + resolveThinkingDefault, + runEmbeddedPiAgent, + resolveAgentTimeoutMs, + ensureAgentWorkspace, + session: { + resolveStorePath, + loadSessionStore, + saveSessionStore, + resolveSessionFilePath, + }, + }; +} diff --git a/src/plugins/runtime/types-core.ts b/src/plugins/runtime/types-core.ts index c25c3afa86b..c1bb753fb11 100644 --- a/src/plugins/runtime/types-core.ts +++ b/src/plugins/runtime/types-core.ts @@ -13,6 +13,25 @@ export type PluginRuntimeCore = { loadConfig: typeof import("../../config/config.js").loadConfig; writeConfigFile: typeof import("../../config/config.js").writeConfigFile; }; + agent: { + defaults: { + model: typeof import("../../agents/defaults.js").DEFAULT_MODEL; + provider: typeof import("../../agents/defaults.js").DEFAULT_PROVIDER; + }; + resolveAgentDir: typeof import("../../agents/agent-scope.js").resolveAgentDir; + resolveAgentWorkspaceDir: typeof import("../../agents/agent-scope.js").resolveAgentWorkspaceDir; + resolveAgentIdentity: typeof import("../../agents/identity.js").resolveAgentIdentity; + resolveThinkingDefault: typeof import("../../agents/model-selection.js").resolveThinkingDefault; + runEmbeddedPiAgent: typeof import("../../agents/pi-embedded.js").runEmbeddedPiAgent; + resolveAgentTimeoutMs: typeof import("../../agents/timeout.js").resolveAgentTimeoutMs; + ensureAgentWorkspace: typeof import("../../agents/workspace.js").ensureAgentWorkspace; + session: { + resolveStorePath: typeof import("../../config/sessions.js").resolveStorePath; + loadSessionStore: typeof import("../../config/sessions.js").loadSessionStore; + saveSessionStore: typeof import("../../config/sessions.js").saveSessionStore; + resolveSessionFilePath: typeof import("../../config/sessions.js").resolveSessionFilePath; + }; + }; system: { enqueueSystemEvent: typeof import("../../infra/system-events.js").enqueueSystemEvent; requestHeartbeatNow: typeof import("../../infra/heartbeat-wake.js").requestHeartbeatNow; diff --git a/src/types/extension-api.d.ts b/src/types/extension-api.d.ts deleted file mode 100644 index ca711425cab..00000000000 --- a/src/types/extension-api.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module "../../../dist/extensionAPI.js" { - export const runEmbeddedPiAgent: (params: Record) => Promise; -} diff --git a/tsconfig.json b/tsconfig.json index e2f9e4ff97e..bc6439e921f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,6 @@ "target": "es2023", "useDefineForClassFields": false, "paths": { - "openclaw/extension-api": ["./src/extensionAPI.ts"], "openclaw/plugin-sdk": ["./src/plugin-sdk/index.ts"], "openclaw/plugin-sdk/*": ["./src/plugin-sdk/*.ts"], "openclaw/plugin-sdk/account-id": ["./src/plugin-sdk/account-id.ts"] diff --git a/tsdown.config.ts b/tsdown.config.ts index c577b1cc1e8..2bb86b24c21 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -119,7 +119,6 @@ function buildCoreDistEntries(): Record { // Ensure this module is bundled as an entry so legacy CLI shims can resolve its exports. "cli/daemon-cli": "src/cli/daemon-cli.ts", "infra/warning-filter": "src/infra/warning-filter.ts", - extensionAPI: "src/extensionAPI.ts", // Keep sync lazy-runtime channel modules as concrete dist files. "channels/plugins/agent-tools/whatsapp-login": "src/channels/plugins/agent-tools/whatsapp-login.ts", diff --git a/vitest.config.ts b/vitest.config.ts index 564065be9e3..2ed4ed07f7c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,10 +13,6 @@ export default defineConfig({ resolve: { // Keep this ordered: the base `openclaw/plugin-sdk` alias is a prefix match. alias: [ - { - find: "openclaw/extension-api", - replacement: path.join(repoRoot, "src", "extensionAPI.ts"), - }, ...pluginSdkSubpaths.map((subpath) => ({ find: `openclaw/plugin-sdk/${subpath}`, replacement: path.join(repoRoot, "src", "plugin-sdk", `${subpath}.ts`), @@ -86,7 +82,6 @@ export default defineConfig({ "src/index.ts", "src/runtime.ts", "src/channel-web.ts", - "src/extensionAPI.ts", "src/logging.ts", "src/cli/**", "src/commands/**",