mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 04:50:51 +00:00
Plugins: remove public extension-api surface (#48462)
* Plugins: remove public extension-api surface * Plugins: fix loader setup routing follow-ups * CI: ignore non-extension helper dirs in extension-fast * Docs: note extension-api removal as breaking
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<T> = {
|
||||
[K in keyof T]?: T[K] extends (...args: never[]) => unknown
|
||||
@@ -39,6 +40,50 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
|
||||
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"],
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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<void>;
|
||||
resolveStorePath: (store?: string, opts?: { agentId?: string }) => string;
|
||||
loadSessionStore: (storePath: string) => Record<string, unknown>;
|
||||
saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
|
||||
resolveSessionFilePath: (
|
||||
sessionId: string,
|
||||
entry: unknown,
|
||||
opts?: { agentId?: string },
|
||||
) => string;
|
||||
DEFAULT_MODEL: string;
|
||||
DEFAULT_PROVIDER: string;
|
||||
};
|
||||
|
||||
let coreRootCache: string | null = null;
|
||||
let coreDepsPromise: Promise<CoreAgentDeps> | 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<string>();
|
||||
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<CoreAgentDeps> {
|
||||
if (coreDepsPromise) {
|
||||
return coreDepsPromise;
|
||||
}
|
||||
|
||||
coreDepsPromise = (async () => {
|
||||
return await importCoreExtensionAPI();
|
||||
})();
|
||||
|
||||
return coreDepsPromise;
|
||||
}
|
||||
export type CoreAgentDeps = OpenClawPluginApi["runtime"]["agent"];
|
||||
|
||||
@@ -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<VoiceResponseResult> {
|
||||
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<ReturnType<typeof loadCoreAgentDeps>>;
|
||||
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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<VoiceCallRuntime> {
|
||||
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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ const openAllowlistWarningCache = new Set<string>();
|
||||
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<string, string> => {
|
||||
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(),
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
36
src/plugins/runtime/runtime-agent.ts
Normal file
36
src/plugins/runtime/runtime-agent.ts
Normal file
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
3
src/types/extension-api.d.ts
vendored
3
src/types/extension-api.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
declare module "../../../dist/extensionAPI.js" {
|
||||
export const runEmbeddedPiAgent: (params: Record<string, unknown>) => Promise<unknown>;
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -119,7 +119,6 @@ function buildCoreDistEntries(): Record<string, string> {
|
||||
// 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",
|
||||
|
||||
@@ -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/**",
|
||||
|
||||
Reference in New Issue
Block a user