Files
fusion/plugins/fusion-plugin-openclaw-runtime/src/runtime-adapter.ts
Fusion 836ca6807a feat(FN-3717): add OpenClaw MCP bridge (+5 more)
Commits merged:
- docs(FN-3717): update openclaw runtime and settings docs
- test(FN-3717): fix openclaw engine test typing
- test(FN-3717): strengthen openclaw bridge coverage
- feat(FN-3717): complete Step 2 — wire engine and bundle delivery
- fix(FN-3717): address openclaw bridge review feedback
- feat(FN-3717): complete Step 1 — add OpenClaw MCP bridge

Files changed:
.changeset/fn-3717-openclaw-tool-bridge.md         |  5 ++
 docs/settings-reference.md                         | 28 +++++---
 packages/cli/src/__tests__/bundle-output.test.ts   |  9 +++
 packages/cli/tsup.config.ts                        |  7 ++
 .../src/__tests__/openclaw-runtime-e2e.test.ts     | 28 ++++++++
 .../__tests__/openclaw-runtime-integration.test.ts | 10 +++
 plugins/fusion-plugin-openclaw-runtime/README.md   | 13 ++++
 .../src/__tests__/cli-spawn.test.ts                | 82 +++++++++++++++++++---
 .../src/__tests__/mcp-config.test.ts               | 45 ++++++++++++
 .../src/__tests__/runtime-adapter.test.ts          | 46 ++++++++++++
 .../fusion-plugin-openclaw-runtime/src/index.ts    |  5 ++
 .../src/mcp-config.ts                              | 60 ++++++++++++++++
 .../src/mcp-schema-server.cjs                      | 59 ++++++++++++++++
 .../src/pi-module.ts                               | 52 ++++++++++++--
 .../src/runtime-adapter.ts                         | 26 +++++++
 .../fusion-plugin-openclaw-runtime/src/types.ts    |  4 ++
 16 files changed, 457 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-3717
2026-05-07 18:00:18 -07:00

99 lines
3.0 KiB
TypeScript

/**
* OpenClaw Runtime Adapter — drives the local `openclaw` CLI as a subprocess.
*
* Each call to `promptWithFallback` invokes
* `openclaw --no-color agent --local --json --session-id <uuid> --message <prompt>`
* (and `--model`, `--thinking`, `--timeout`, `--agent` if configured),
* parses the JSON document on stdout, and forwards visible/reasoning text
* via the session callbacks.
*
* Session continuity: the UUID minted on session create is reused on every
* subsequent prompt as `--session-id`, so openclaw resumes the same agent
* conversation server-side.
*/
import type {
AgentRuntime,
AgentRuntimeOptions,
AgentSessionResult,
CliConfig,
GatewaySession,
} from "./types.js";
import {
configureOpenClawMcpServer,
createCliSession,
describeCliModel,
promptCli,
resolveCliConfig,
} from "./pi-module.js";
import { toolsToMcpToolDefs, writeOpenClawMcpBridgeFiles, type ToolLike } from "./mcp-config.js";
import { randomUUID } from "node:crypto";
export class OpenClawRuntimeAdapter implements AgentRuntime {
readonly id = "openclaw";
readonly name = "OpenClaw Runtime";
private readonly config: CliConfig;
constructor(settings?: Partial<CliConfig> | Record<string, unknown>) {
this.config = resolveCliConfig(settings as Record<string, unknown> | undefined);
}
async createSession(options: AgentRuntimeOptions): Promise<AgentSessionResult> {
const contextTools = [
...(Array.isArray(options.tools) ? (options.tools as ToolLike[]) : []),
...(Array.isArray(options.customTools) ? (options.customTools as ToolLike[]) : []),
];
const toolDefs = toolsToMcpToolDefs(contextTools);
let mcpProfile: string | undefined;
let mcpConfigPath: string | undefined;
if (toolDefs.length > 0) {
const bridge = writeOpenClawMcpBridgeFiles(toolDefs, randomUUID());
mcpProfile = `fusion-${randomUUID()}`;
mcpConfigPath = bridge.serverConfigPath;
await configureOpenClawMcpServer({
binaryPath: this.config.binaryPath,
profile: mcpProfile,
serverName: bridge.serverName,
serverConfigPath: bridge.serverConfigPath,
});
}
const session = createCliSession({
systemPrompt: options.systemPrompt,
agentId: this.config.agentId,
mcpProfile,
mcpConfigPath,
callbacks: {
onText: options.onText,
onThinking: options.onThinking,
onToolStart: options.onToolStart,
onToolEnd: options.onToolEnd,
},
});
return { session, sessionFile: undefined };
}
async promptWithFallback(
session: GatewaySession,
prompt: string,
options?: unknown,
): Promise<void> {
const overrideCallbacks = (options ?? undefined) as
| Parameters<typeof promptCli>[3]
| undefined;
await promptCli(session, prompt, this.config, overrideCallbacks);
}
describeModel(session: GatewaySession): string {
return describeCliModel(session);
}
async dispose(_session: GatewaySession): Promise<void> {
// No persistent resources — each prompt spawns a fresh subprocess.
}
}