refactor(plugin-sdk): add managed task flow runtime

This commit is contained in:
Peter Steinberger
2026-04-28 05:32:23 +01:00
parent d987e153fe
commit b60eb1711a
16 changed files with 35 additions and 23 deletions

View File

@@ -725,18 +725,18 @@ canonical replacement.
</Accordion>
<Accordion title="runtime.tasks.flow → runtime.tasks.flows">
<Accordion title="runtime.tasks.flow → runtime.tasks.managedFlows">
**Old**: `runtime.tasks.flow` (singular) returned a live task-flow accessor.
**New**: `runtime.tasks.flows` (plural) returns DTO-based TaskFlow access,
which is import-safe and does not require the full task runtime to be
loaded.
**New**: `runtime.tasks.managedFlows` keeps the managed TaskFlow mutation
runtime for plugins that create, update, cancel, or run child tasks from a
flow. Use `runtime.tasks.flows` when the plugin only needs DTO-based reads.
```typescript
// Before
const flow = api.runtime.tasks.flow(ctx);
const flow = api.runtime.tasks.flow.fromToolContext(ctx);
// After
const flows = api.runtime.tasks.flows(ctx);
const flow = api.runtime.tasks.managedFlows.fromToolContext(ctx);
```
</Accordion>

View File

@@ -179,11 +179,11 @@ Internal OpenClaw runtime code has the same direction: load config once at the C
Inside the Gateway this runtime is in-process. In plugin CLI commands it calls the configured Gateway over RPC, so commands such as `openclaw googlemeet recover-tab` can inspect paired nodes from the terminal. Node commands still go through normal Gateway node pairing, command allowlists, and node-local command handling.
</Accordion>
<Accordion title="api.runtime.taskFlow">
<Accordion title="api.runtime.tasks.managedFlows">
Bind a Task Flow runtime to an existing OpenClaw session key or trusted tool context, then create and manage Task Flows without passing an owner on every call.
```typescript
const taskFlow = api.runtime.taskFlow.fromToolContext(ctx);
const taskFlow = api.runtime.tasks.managedFlows.fromToolContext(ctx);
const created = taskFlow.createManaged({
controllerId: "my-plugin/review-batch",

View File

@@ -89,7 +89,7 @@ The plugin applies:
- Request body size and timeout guards
- Fixed-window rate limiting
- In-flight request limiting
- Owner-bound TaskFlow access through `api.runtime.taskFlow.bindSession(...)`
- Owner-bound TaskFlow access through `api.runtime.tasks.managedFlows.bindSession(...)`
## Request format

View File

@@ -13,8 +13,8 @@ export default definePluginEntry({
return null;
}
const taskFlow =
api.runtime?.taskFlow && ctx.sessionKey
? api.runtime.taskFlow.fromToolContext(ctx)
api.runtime?.tasks.managedFlows && ctx.sessionKey
? api.runtime.tasks.managedFlows.fromToolContext(ctx)
: undefined;
return createLobsterTool(api, { taskFlow }) as AnyAgentTool;
}) as OpenClawPluginToolFactory,

View File

@@ -12,7 +12,7 @@ type JsonLike =
};
type BoundTaskFlow = ReturnType<
NonNullable<OpenClawPluginApi["runtime"]>["taskFlow"]["bindSession"]
NonNullable<OpenClawPluginApi["runtime"]>["tasks"]["managedFlows"]["bindSession"]
>;
type FlowRecord = ReturnType<BoundTaskFlow["createManaged"]>;

View File

@@ -13,7 +13,7 @@ import {
} from "./lobster-taskflow.js";
type BoundTaskFlow = ReturnType<
NonNullable<OpenClawPluginApi["runtime"]>["taskFlow"]["bindSession"]
NonNullable<OpenClawPluginApi["runtime"]>["tasks"]["managedFlows"]["bindSession"]
>;
type JsonLike =

View File

@@ -2,7 +2,7 @@ import { vi } from "vitest";
import type { OpenClawPluginApi } from "../runtime-api.js";
export type BoundTaskFlow = ReturnType<
NonNullable<OpenClawPluginApi["runtime"]>["taskFlow"]["bindSession"]
NonNullable<OpenClawPluginApi["runtime"]>["tasks"]["managedFlows"]["bindSession"]
>;
export function createFakeTaskFlow(overrides?: Partial<BoundTaskFlow>): BoundTaskFlow {

View File

@@ -14,8 +14,10 @@ function createApi(params?: {
source: "test",
pluginConfig: params?.pluginConfig ?? {},
runtime: {
taskFlow: {
bindSession: vi.fn(({ sessionKey }: { sessionKey: string }) => ({ sessionKey })),
tasks: {
managedFlows: {
bindSession: vi.fn(({ sessionKey }: { sessionKey: string }) => ({ sessionKey })),
},
},
} as unknown as OpenClawPluginApi["runtime"],
registerHttpRoute: params?.registerHttpRoute ?? vi.fn(),

View File

@@ -17,7 +17,7 @@ function registerWebhookRoutes(api: OpenClawPluginApi): void {
});
for (const route of routes) {
const taskFlow = api.runtime.taskFlow.bindSession({
const taskFlow = api.runtime.tasks.managedFlows.bindSession({
sessionKey: route.sessionKey,
});
const target: TaskFlowWebhookTarget = {

View File

@@ -18,7 +18,7 @@ import {
} from "../runtime-api.js";
import type { WebhookSecretInput } from "./config.js";
type BoundTaskFlowRuntime = ReturnType<PluginRuntime["taskFlow"]["bindSession"]>;
type BoundTaskFlowRuntime = ReturnType<PluginRuntime["tasks"]["managedFlows"]["bindSession"]>;
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };

View File

@@ -73,10 +73,10 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
const taskFlow = {
bindSession: vi.fn(
createTaskFlowSessionMock,
) as unknown as PluginRuntime["taskFlow"]["bindSession"],
) as unknown as PluginRuntime["tasks"]["managedFlows"]["bindSession"],
fromToolContext: vi.fn(
createTaskFlowSessionMock,
) as unknown as PluginRuntime["taskFlow"]["fromToolContext"],
) as unknown as PluginRuntime["tasks"]["managedFlows"]["fromToolContext"],
};
const base: PluginRuntime = {
version: "1.0.0-test",
@@ -468,6 +468,7 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
bindSession: vi.fn(),
fromToolContext: vi.fn(),
} as PluginRuntime["tasks"]["flows"],
managedFlows: taskFlow,
flow: taskFlow,
},
taskFlow,

View File

@@ -720,7 +720,8 @@ export const PLUGIN_COMPAT_RECORDS = [
deprecated: "2026-04-26",
warningStarts: "2026-04-26",
removeAfter: "2026-07-26",
replacement: "`api.runtime.tasks.flows`",
replacement:
"`api.runtime.tasks.managedFlows` for managed mutations or `api.runtime.tasks.flows` for DTO reads",
docsPath: "/plugins/sdk-runtime",
surfaces: ["api.runtime.taskFlow", "api.runtime.tasks.flow"],
diagnostics: ["plugin runtime compatibility warning"],

View File

@@ -191,7 +191,7 @@ describe("plugin runtime command execution", () => {
},
},
{
name: "exposes canonical runtime.tasks.runs and runtime.tasks.flows while keeping legacy TaskFlow aliases",
name: "exposes canonical runtime.tasks task runtimes while keeping legacy TaskFlow aliases",
assert: (runtime: ReturnType<typeof createPluginRuntime>) => {
expectFunctionKeys(runtime.tasks.runs as Record<string, unknown>, [
"bindSession",
@@ -201,11 +201,16 @@ describe("plugin runtime command execution", () => {
"bindSession",
"fromToolContext",
]);
expectFunctionKeys(runtime.tasks.managedFlows as Record<string, unknown>, [
"bindSession",
"fromToolContext",
]);
expectFunctionKeys(runtime.tasks.flow as Record<string, unknown>, [
"bindSession",
"fromToolContext",
]);
expect(runtime.taskFlow).toBe(runtime.tasks.flow);
expect(runtime.tasks.managedFlows).toBe(runtime.tasks.flow);
expect(runtime.taskFlow).toBe(runtime.tasks.managedFlows);
},
},
{

View File

@@ -217,6 +217,7 @@ export function createRuntimeTasks(params: {
return {
runs: createRuntimeTaskRuns(),
flows: createRuntimeTaskFlows(),
managedFlows: params.legacyTaskFlow,
flow: params.legacyTaskFlow,
};
}

View File

@@ -64,6 +64,7 @@ export type PluginRuntimeTaskFlows = {
export type PluginRuntimeTasks = {
runs: PluginRuntimeTaskRuns;
flows: PluginRuntimeTaskFlows;
managedFlows: PluginRuntimeTaskFlow;
/** @deprecated Use runtime.tasks.flows for DTO-based TaskFlow access. */
flow: PluginRuntimeTaskFlow;
};

View File

@@ -231,6 +231,7 @@ export type PluginRuntimeCore = {
tasks: {
runs: PluginRuntimeTaskRuns;
flows: PluginRuntimeTaskFlows;
managedFlows: import("./runtime-taskflow.types.js").PluginRuntimeTaskFlow;
/** @deprecated Use runtime.tasks.flows for DTO-based TaskFlow access. */
flow: import("./runtime-taskflow.types.js").PluginRuntimeTaskFlow;
};