openclaw-swarm-layer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/src/cli/context.d.ts +18 -0
- package/dist/src/cli/context.js +60 -0
- package/dist/src/cli/output.d.ts +1 -0
- package/dist/src/cli/output.js +9 -0
- package/dist/src/cli/register-swarm-cli.d.ts +6 -0
- package/dist/src/cli/register-swarm-cli.js +130 -0
- package/dist/src/cli/swarm-doctor.d.ts +40 -0
- package/dist/src/cli/swarm-doctor.js +69 -0
- package/dist/src/cli/swarm-init.d.ts +9 -0
- package/dist/src/cli/swarm-init.js +10 -0
- package/dist/src/cli/swarm-plan.d.ts +13 -0
- package/dist/src/cli/swarm-plan.js +39 -0
- package/dist/src/cli/swarm-report.d.ts +9 -0
- package/dist/src/cli/swarm-report.js +14 -0
- package/dist/src/cli/swarm-review.d.ts +8 -0
- package/dist/src/cli/swarm-review.js +34 -0
- package/dist/src/cli/swarm-run.d.ts +7 -0
- package/dist/src/cli/swarm-run.js +39 -0
- package/dist/src/cli/swarm-session-cancel.d.ts +6 -0
- package/dist/src/cli/swarm-session-cancel.js +64 -0
- package/dist/src/cli/swarm-session-cleanup.d.ts +16 -0
- package/dist/src/cli/swarm-session-cleanup.js +34 -0
- package/dist/src/cli/swarm-session-close.d.ts +6 -0
- package/dist/src/cli/swarm-session-close.js +53 -0
- package/dist/src/cli/swarm-session-followup.d.ts +7 -0
- package/dist/src/cli/swarm-session-followup.js +63 -0
- package/dist/src/cli/swarm-session-inspect.d.ts +5 -0
- package/dist/src/cli/swarm-session-inspect.js +12 -0
- package/dist/src/cli/swarm-session-list.d.ts +4 -0
- package/dist/src/cli/swarm-session-list.js +19 -0
- package/dist/src/cli/swarm-session-status.d.ts +5 -0
- package/dist/src/cli/swarm-session-status.js +85 -0
- package/dist/src/cli/swarm-session-steer.d.ts +6 -0
- package/dist/src/cli/swarm-session-steer.js +40 -0
- package/dist/src/cli/swarm-status.d.ts +81 -0
- package/dist/src/cli/swarm-status.js +56 -0
- package/dist/src/config.d.ts +159 -0
- package/dist/src/config.js +292 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +24 -0
- package/dist/src/lib/json-file.d.ts +5 -0
- package/dist/src/lib/json-file.js +42 -0
- package/dist/src/lib/paths.d.ts +25 -0
- package/dist/src/lib/paths.js +41 -0
- package/dist/src/planning/planner.d.ts +3 -0
- package/dist/src/planning/planner.js +39 -0
- package/dist/src/planning/task-graph.d.ts +8 -0
- package/dist/src/planning/task-graph.js +59 -0
- package/dist/src/reporting/obsidian-journal.d.ts +7 -0
- package/dist/src/reporting/obsidian-journal.js +126 -0
- package/dist/src/reporting/operator-summary.d.ts +32 -0
- package/dist/src/reporting/operator-summary.js +124 -0
- package/dist/src/reporting/reporter.d.ts +10 -0
- package/dist/src/reporting/reporter.js +128 -0
- package/dist/src/review/review-gate.d.ts +15 -0
- package/dist/src/review/review-gate.js +116 -0
- package/dist/src/runtime/acp-mapping.d.ts +23 -0
- package/dist/src/runtime/acp-mapping.js +50 -0
- package/dist/src/runtime/acp-runner.d.ts +11 -0
- package/dist/src/runtime/acp-runner.js +83 -0
- package/dist/src/runtime/bridge-errors.d.ts +8 -0
- package/dist/src/runtime/bridge-errors.js +59 -0
- package/dist/src/runtime/bridge-manifest.d.ts +30 -0
- package/dist/src/runtime/bridge-manifest.js +87 -0
- package/dist/src/runtime/bridge-openclaw-session-adapter.d.ts +48 -0
- package/dist/src/runtime/bridge-openclaw-session-adapter.js +142 -0
- package/dist/src/runtime/bridge-openclaw-subagent-adapter.d.ts +33 -0
- package/dist/src/runtime/bridge-openclaw-subagent-adapter.js +149 -0
- package/dist/src/runtime/manual-runner.d.ts +9 -0
- package/dist/src/runtime/manual-runner.js +53 -0
- package/dist/src/runtime/openclaw-exec-bridge.d.ts +211 -0
- package/dist/src/runtime/openclaw-exec-bridge.js +498 -0
- package/dist/src/runtime/openclaw-session-adapter.d.ts +48 -0
- package/dist/src/runtime/openclaw-session-adapter.js +14 -0
- package/dist/src/runtime/openclaw-subagent-adapter.d.ts +42 -0
- package/dist/src/runtime/openclaw-subagent-adapter.js +11 -0
- package/dist/src/runtime/public-api-seams.d.ts +23 -0
- package/dist/src/runtime/public-api-seams.js +79 -0
- package/dist/src/runtime/real-openclaw-session-adapter.d.ts +83 -0
- package/dist/src/runtime/real-openclaw-session-adapter.js +91 -0
- package/dist/src/runtime/retry-engine.d.ts +7 -0
- package/dist/src/runtime/retry-engine.js +29 -0
- package/dist/src/runtime/runner-registry.d.ts +6 -0
- package/dist/src/runtime/runner-registry.js +25 -0
- package/dist/src/runtime/session-sync.d.ts +9 -0
- package/dist/src/runtime/session-sync.js +165 -0
- package/dist/src/runtime/subagent-mapping.d.ts +9 -0
- package/dist/src/runtime/subagent-mapping.js +31 -0
- package/dist/src/runtime/subagent-runner.d.ts +9 -0
- package/dist/src/runtime/subagent-runner.js +63 -0
- package/dist/src/runtime/task-runner.d.ts +38 -0
- package/dist/src/runtime/task-runner.js +1 -0
- package/dist/src/schemas/run.schema.json +51 -0
- package/dist/src/schemas/spec.schema.json +30 -0
- package/dist/src/schemas/task.schema.json +48 -0
- package/dist/src/schemas/workflow-state.schema.json +46 -0
- package/dist/src/services/orchestrator.d.ts +47 -0
- package/dist/src/services/orchestrator.js +224 -0
- package/dist/src/session/session-lifecycle.d.ts +6 -0
- package/dist/src/session/session-lifecycle.js +84 -0
- package/dist/src/session/session-selector.d.ts +12 -0
- package/dist/src/session/session-selector.js +72 -0
- package/dist/src/session/session-store.d.ts +14 -0
- package/dist/src/session/session-store.js +84 -0
- package/dist/src/spec/spec-importer.d.ts +4 -0
- package/dist/src/spec/spec-importer.js +80 -0
- package/dist/src/state/state-store.d.ts +22 -0
- package/dist/src/state/state-store.js +187 -0
- package/dist/src/tools/index.d.ts +2 -0
- package/dist/src/tools/index.js +116 -0
- package/dist/src/types.d.ts +151 -0
- package/dist/src/types.js +1 -0
- package/dist/src/workspace/workspace-manager.d.ts +8 -0
- package/dist/src/workspace/workspace-manager.js +18 -0
- package/openclaw.plugin.json +121 -0
- package/package.json +62 -0
- package/scripts/openclaw-exec-bridge.mjs +4 -0
- package/skills/swarm-layer/SKILL.md +358 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const ACP_PUBLIC_REPLACEMENT_EXPORT = "getAcpSessionManager";
|
|
2
|
+
export const SUBAGENT_PUBLIC_REPLACEMENT_EXPORT = "spawnSubagentDirect";
|
|
3
|
+
export function buildMigrationChecklist(plan) {
|
|
4
|
+
const steps = [
|
|
5
|
+
"Run `openclaw swarm doctor --json` before changing bridge or public API integration code.",
|
|
6
|
+
];
|
|
7
|
+
for (const item of plan) {
|
|
8
|
+
if (item.available) {
|
|
9
|
+
steps.push(`[${item.runner}] Replace ${item.currentImplementation} with ${item.targetImplementation}. Update modules: ${item.affectedModules.join(", ")}.`);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
steps.push(`[${item.runner}] Keep the current bridge path until the public export ${item.publicExport} is available.`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
steps.push("After any replacement, rerun unit tests, e2e regressions, and at least one live smoke before relaxing bridge guards.");
|
|
16
|
+
return steps;
|
|
17
|
+
}
|
|
18
|
+
export async function detectPublicApiAvailability(sdkLoader = async () => (await import("openclaw/plugin-sdk"))) {
|
|
19
|
+
const sdk = await sdkLoader();
|
|
20
|
+
const acpControlPlaneExport = typeof sdk[ACP_PUBLIC_REPLACEMENT_EXPORT] === "function";
|
|
21
|
+
const subagentSpawnExport = typeof sdk[SUBAGENT_PUBLIC_REPLACEMENT_EXPORT] === "function";
|
|
22
|
+
const readyReplacementPoints = [];
|
|
23
|
+
const notes = [];
|
|
24
|
+
if (!acpControlPlaneExport) {
|
|
25
|
+
notes.push(`Top-level public plugin SDK does not expose ${ACP_PUBLIC_REPLACEMENT_EXPORT}().`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
readyReplacementPoints.push(`acp:${ACP_PUBLIC_REPLACEMENT_EXPORT}`);
|
|
29
|
+
notes.push(`Top-level public plugin SDK exposes ${ACP_PUBLIC_REPLACEMENT_EXPORT}(); ACP bridge replacement is now technically possible.`);
|
|
30
|
+
}
|
|
31
|
+
if (!subagentSpawnExport) {
|
|
32
|
+
notes.push(`Top-level public plugin SDK does not expose ${SUBAGENT_PUBLIC_REPLACEMENT_EXPORT}().`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
readyReplacementPoints.push(`subagent:${SUBAGENT_PUBLIC_REPLACEMENT_EXPORT}`);
|
|
36
|
+
notes.push(`Top-level public plugin SDK exposes ${SUBAGENT_PUBLIC_REPLACEMENT_EXPORT}(); subagent bridge replacement is now technically possible.`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
acpControlPlaneExport,
|
|
40
|
+
subagentSpawnExport,
|
|
41
|
+
readyReplacementPoints,
|
|
42
|
+
notes,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function buildReplacementPlan(availability) {
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
runner: "acp",
|
|
49
|
+
publicExport: ACP_PUBLIC_REPLACEMENT_EXPORT,
|
|
50
|
+
available: availability.acpControlPlaneExport,
|
|
51
|
+
status: availability.acpControlPlaneExport ? "ready" : "blocked",
|
|
52
|
+
currentImplementation: "bridge-openclaw-session-adapter -> openclaw-exec-bridge",
|
|
53
|
+
targetImplementation: "real-openclaw-session-adapter via top-level public plugin-sdk export",
|
|
54
|
+
affectedModules: [
|
|
55
|
+
"src/runtime/bridge-openclaw-session-adapter.ts",
|
|
56
|
+
"src/runtime/openclaw-exec-bridge.ts",
|
|
57
|
+
"src/runtime/real-openclaw-session-adapter.ts",
|
|
58
|
+
],
|
|
59
|
+
nextStep: availability.acpControlPlaneExport
|
|
60
|
+
? "Prototype replacing the ACP bridge control-plane path with the public export."
|
|
61
|
+
: "Keep using the bridge-backed ACP adapter until a public control-plane export is available.",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
runner: "subagent",
|
|
65
|
+
publicExport: SUBAGENT_PUBLIC_REPLACEMENT_EXPORT,
|
|
66
|
+
available: availability.subagentSpawnExport,
|
|
67
|
+
status: availability.subagentSpawnExport ? "ready" : "blocked",
|
|
68
|
+
currentImplementation: "bridge-openclaw-subagent-adapter -> openclaw-exec-bridge patched helpers",
|
|
69
|
+
targetImplementation: "public subagent spawn helper from top-level plugin-sdk export",
|
|
70
|
+
affectedModules: [
|
|
71
|
+
"src/runtime/bridge-openclaw-subagent-adapter.ts",
|
|
72
|
+
"src/runtime/openclaw-exec-bridge.ts",
|
|
73
|
+
],
|
|
74
|
+
nextStep: availability.subagentSpawnExport
|
|
75
|
+
? "Prototype replacing the subagent bridge spawn path with the public export."
|
|
76
|
+
: "Keep using the bridge-backed subagent adapter until a public spawn export is available.",
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { SwarmPluginConfig } from "../config.js";
|
|
2
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { OpenClawSessionAdapter, AcpAcceptedSession, AcpSessionStatus } from "./openclaw-session-adapter.js";
|
|
4
|
+
import type { AcpSpawnParams } from "./acp-mapping.js";
|
|
5
|
+
type AcpManager = {
|
|
6
|
+
initializeSession(input: {
|
|
7
|
+
cfg: unknown;
|
|
8
|
+
sessionKey: string;
|
|
9
|
+
agent: string;
|
|
10
|
+
mode: "persistent" | "oneshot";
|
|
11
|
+
cwd?: string;
|
|
12
|
+
backendId?: string;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
handle: {
|
|
15
|
+
sessionKey: string;
|
|
16
|
+
backend: string;
|
|
17
|
+
backendSessionId?: string;
|
|
18
|
+
agentSessionId?: string;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
runTurn(input: {
|
|
22
|
+
cfg: unknown;
|
|
23
|
+
sessionKey: string;
|
|
24
|
+
text: string;
|
|
25
|
+
mode: "prompt" | "steer";
|
|
26
|
+
requestId: string;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
getSessionStatus(input: {
|
|
29
|
+
cfg: unknown;
|
|
30
|
+
sessionKey: string;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
sessionKey: string;
|
|
33
|
+
backend: string;
|
|
34
|
+
state: "idle" | "running" | "error";
|
|
35
|
+
identity?: {
|
|
36
|
+
acpxSessionId?: string;
|
|
37
|
+
agentSessionId?: string;
|
|
38
|
+
};
|
|
39
|
+
runtimeStatus?: {
|
|
40
|
+
backendSessionId?: string;
|
|
41
|
+
agentSessionId?: string;
|
|
42
|
+
summary?: string;
|
|
43
|
+
};
|
|
44
|
+
lastError?: string;
|
|
45
|
+
}>;
|
|
46
|
+
cancelSession(input: {
|
|
47
|
+
cfg: unknown;
|
|
48
|
+
sessionKey: string;
|
|
49
|
+
reason?: string;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
closeSession(input: {
|
|
52
|
+
cfg: unknown;
|
|
53
|
+
sessionKey: string;
|
|
54
|
+
reason: string;
|
|
55
|
+
}): Promise<{
|
|
56
|
+
runtimeClosed: boolean;
|
|
57
|
+
runtimeNotice?: string;
|
|
58
|
+
}>;
|
|
59
|
+
};
|
|
60
|
+
type SdkLike = {
|
|
61
|
+
getAcpSessionManager?: () => AcpManager;
|
|
62
|
+
};
|
|
63
|
+
export declare class ExperimentalRealOpenClawSessionAdapter implements OpenClawSessionAdapter {
|
|
64
|
+
private readonly runtime;
|
|
65
|
+
private readonly config;
|
|
66
|
+
private readonly sdkLoader;
|
|
67
|
+
constructor(runtime: Pick<PluginRuntime, "config">, config: Pick<SwarmPluginConfig, "acp">, sdkLoader?: () => Promise<SdkLike>);
|
|
68
|
+
private getManager;
|
|
69
|
+
spawnAcpSession(params: AcpSpawnParams): Promise<AcpAcceptedSession>;
|
|
70
|
+
getAcpSessionStatus(sessionKey: string): Promise<AcpSessionStatus>;
|
|
71
|
+
cancelAcpSession(sessionKey: string, reason?: string): Promise<{
|
|
72
|
+
sessionKey: string;
|
|
73
|
+
cancelledAt?: string;
|
|
74
|
+
message?: string;
|
|
75
|
+
}>;
|
|
76
|
+
closeAcpSession(sessionKey: string, reason?: string): Promise<{
|
|
77
|
+
sessionKey: string;
|
|
78
|
+
closedAt?: string;
|
|
79
|
+
message?: string;
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
82
|
+
export declare function createSessionAdapter(runtime: Pick<PluginRuntime, "config"> | undefined, config: Pick<SwarmPluginConfig, "acp">): OpenClawSessionAdapter | null;
|
|
83
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
function mapManagerState(status) {
|
|
3
|
+
if (status.state === "running") {
|
|
4
|
+
return "running";
|
|
5
|
+
}
|
|
6
|
+
if (status.state === "error") {
|
|
7
|
+
return "failed";
|
|
8
|
+
}
|
|
9
|
+
return "completed";
|
|
10
|
+
}
|
|
11
|
+
export class ExperimentalRealOpenClawSessionAdapter {
|
|
12
|
+
runtime;
|
|
13
|
+
config;
|
|
14
|
+
sdkLoader;
|
|
15
|
+
constructor(runtime, config, sdkLoader = async () => (await import("openclaw/plugin-sdk"))) {
|
|
16
|
+
this.runtime = runtime;
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.sdkLoader = sdkLoader;
|
|
19
|
+
}
|
|
20
|
+
async getManager() {
|
|
21
|
+
if (!this.config.acp.experimentalControlPlaneAdapter) {
|
|
22
|
+
throw new Error("Experimental ACP control-plane adapter is disabled in plugin config");
|
|
23
|
+
}
|
|
24
|
+
const sdk = await this.sdkLoader();
|
|
25
|
+
const manager = sdk.getAcpSessionManager?.();
|
|
26
|
+
if (!manager) {
|
|
27
|
+
throw new Error("OpenClaw plugin-sdk does not expose getAcpSessionManager at runtime; real ACP adapter remains blocked on an upstream public control-plane export");
|
|
28
|
+
}
|
|
29
|
+
const cfg = this.runtime.config.loadConfig();
|
|
30
|
+
return { manager, cfg };
|
|
31
|
+
}
|
|
32
|
+
async spawnAcpSession(params) {
|
|
33
|
+
const { manager, cfg } = await this.getManager();
|
|
34
|
+
const sessionKey = `agent:${params.agentId}:acp:${randomUUID()}`;
|
|
35
|
+
const initialized = await manager.initializeSession({
|
|
36
|
+
cfg,
|
|
37
|
+
sessionKey,
|
|
38
|
+
agent: params.agentId,
|
|
39
|
+
mode: params.mode === "session" ? "persistent" : "oneshot",
|
|
40
|
+
cwd: params.cwd,
|
|
41
|
+
backendId: this.config.acp.backendId,
|
|
42
|
+
});
|
|
43
|
+
void manager.runTurn({
|
|
44
|
+
cfg,
|
|
45
|
+
sessionKey,
|
|
46
|
+
text: params.task,
|
|
47
|
+
mode: "prompt",
|
|
48
|
+
requestId: randomUUID(),
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
sessionKey,
|
|
52
|
+
backend: initialized.handle.backend,
|
|
53
|
+
backendSessionId: initialized.handle.backendSessionId,
|
|
54
|
+
agentSessionId: initialized.handle.agentSessionId,
|
|
55
|
+
acceptedAt: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async getAcpSessionStatus(sessionKey) {
|
|
59
|
+
const { manager, cfg } = await this.getManager();
|
|
60
|
+
const status = await manager.getSessionStatus({ cfg, sessionKey });
|
|
61
|
+
return {
|
|
62
|
+
sessionKey,
|
|
63
|
+
state: mapManagerState(status),
|
|
64
|
+
backend: status.backend,
|
|
65
|
+
backendSessionId: status.runtimeStatus?.backendSessionId ?? status.identity?.acpxSessionId,
|
|
66
|
+
agentSessionId: status.runtimeStatus?.agentSessionId ?? status.identity?.agentSessionId,
|
|
67
|
+
checkedAt: new Date().toISOString(),
|
|
68
|
+
message: status.lastError ?? status.runtimeStatus?.summary,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async cancelAcpSession(sessionKey, reason) {
|
|
72
|
+
const { manager, cfg } = await this.getManager();
|
|
73
|
+
await manager.cancelSession({ cfg, sessionKey, reason });
|
|
74
|
+
return { sessionKey, cancelledAt: new Date().toISOString(), message: reason };
|
|
75
|
+
}
|
|
76
|
+
async closeAcpSession(sessionKey, reason) {
|
|
77
|
+
const { manager, cfg } = await this.getManager();
|
|
78
|
+
const closed = await manager.closeSession({ cfg, sessionKey, reason: reason ?? "closed by swarm layer" });
|
|
79
|
+
return {
|
|
80
|
+
sessionKey,
|
|
81
|
+
closedAt: new Date().toISOString(),
|
|
82
|
+
message: closed.runtimeNotice ?? reason,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function createSessionAdapter(runtime, config) {
|
|
87
|
+
if (!runtime || !config.acp.experimentalControlPlaneAdapter) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return new ExperimentalRealOpenClawSessionAdapter(runtime, config);
|
|
91
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RetryHistoryEntry, RetryPolicy, RunRecord, TaskNode } from "../types.js";
|
|
2
|
+
export declare function isRetryableStatus(status: RunRecord["status"], policy: RetryPolicy): boolean;
|
|
3
|
+
export declare function shouldRetry(task: TaskNode, runRecord: RunRecord): {
|
|
4
|
+
retry: boolean;
|
|
5
|
+
reason: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function appendRetryHistory(runRecord: RunRecord): RetryHistoryEntry[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const DEFAULT_RETRYABLE_STATUSES = ["failed", "timed_out"];
|
|
2
|
+
export function isRetryableStatus(status, policy) {
|
|
3
|
+
const retryOn = policy.retryOn.length > 0 ? policy.retryOn : DEFAULT_RETRYABLE_STATUSES;
|
|
4
|
+
return retryOn.includes(status);
|
|
5
|
+
}
|
|
6
|
+
export function shouldRetry(task, runRecord) {
|
|
7
|
+
const policy = task.runner.retryPolicy;
|
|
8
|
+
if (!policy) {
|
|
9
|
+
return { retry: false, reason: "no retry policy configured" };
|
|
10
|
+
}
|
|
11
|
+
if (!isRetryableStatus(runRecord.status, policy)) {
|
|
12
|
+
return { retry: false, reason: `status ${runRecord.status} is not retryable` };
|
|
13
|
+
}
|
|
14
|
+
const pastAttempts = runRecord.retryHistory?.length ?? 0;
|
|
15
|
+
const currentAttempt = pastAttempts + 1;
|
|
16
|
+
if (currentAttempt >= policy.maxAttempts) {
|
|
17
|
+
return { retry: false, reason: `exhausted ${policy.maxAttempts} attempts` };
|
|
18
|
+
}
|
|
19
|
+
return { retry: true, reason: `attempt ${currentAttempt + 1} of ${policy.maxAttempts}` };
|
|
20
|
+
}
|
|
21
|
+
export function appendRetryHistory(runRecord) {
|
|
22
|
+
const entry = {
|
|
23
|
+
attempt: runRecord.attempt,
|
|
24
|
+
runId: runRecord.runId,
|
|
25
|
+
status: runRecord.status,
|
|
26
|
+
at: runRecord.endedAt ?? runRecord.startedAt,
|
|
27
|
+
};
|
|
28
|
+
return [...(runRecord.retryHistory ?? []), entry];
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AcpRunner } from "./acp-runner.js";
|
|
2
|
+
import { ManualRunner } from "./manual-runner.js";
|
|
3
|
+
import { UnsupportedOpenClawSessionAdapter } from "./openclaw-session-adapter.js";
|
|
4
|
+
import { UnsupportedOpenClawSubagentAdapter } from "./openclaw-subagent-adapter.js";
|
|
5
|
+
import { SubagentRunner } from "./subagent-runner.js";
|
|
6
|
+
export class RunnerRegistry {
|
|
7
|
+
runners = new Map();
|
|
8
|
+
constructor(runners) {
|
|
9
|
+
const defaults = runners ?? [
|
|
10
|
+
new ManualRunner(),
|
|
11
|
+
new AcpRunner(undefined, new UnsupportedOpenClawSessionAdapter()),
|
|
12
|
+
new SubagentRunner(new UnsupportedOpenClawSubagentAdapter()),
|
|
13
|
+
];
|
|
14
|
+
defaults.forEach((runner) => {
|
|
15
|
+
this.runners.set(runner.kind, runner);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
resolve(kind) {
|
|
19
|
+
const runner = this.runners.get(kind);
|
|
20
|
+
if (!runner) {
|
|
21
|
+
throw new Error(`Unknown runner: ${kind}`);
|
|
22
|
+
}
|
|
23
|
+
return runner;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RunRecord } from "../types.js";
|
|
2
|
+
import type { AcpSessionStatus } from "./openclaw-session-adapter.js";
|
|
3
|
+
import type { SubagentRunStatus } from "./openclaw-subagent-adapter.js";
|
|
4
|
+
export type SessionSyncResult<TRemoteStatus = AcpSessionStatus | SubagentRunStatus> = {
|
|
5
|
+
runRecord: RunRecord;
|
|
6
|
+
remoteStatus: TRemoteStatus;
|
|
7
|
+
};
|
|
8
|
+
export declare function syncAcpRunRecord(runRecord: RunRecord, remoteStatus: AcpSessionStatus): SessionSyncResult<AcpSessionStatus>;
|
|
9
|
+
export declare function syncSubagentRunRecord(runRecord: RunRecord, remoteStatus: SubagentRunStatus): SessionSyncResult<SubagentRunStatus>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
function parseAcpSummary(message) {
|
|
2
|
+
if (!message) {
|
|
3
|
+
return {};
|
|
4
|
+
}
|
|
5
|
+
const match = (pattern) => message.match(pattern)?.[1];
|
|
6
|
+
return {
|
|
7
|
+
rawStatus: match(/status=([^\s]+)/),
|
|
8
|
+
recordId: match(/acpxRecordId=([^\s]+)/),
|
|
9
|
+
sessionId: match(/acpxSessionId=([^\s]+)/),
|
|
10
|
+
pid: match(/pid=([^\s]+)/),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function buildAcpSummary(remoteStatus, fallback) {
|
|
14
|
+
const parsed = parseAcpSummary(remoteStatus.message);
|
|
15
|
+
const details = [
|
|
16
|
+
parsed.rawStatus ? `status=${parsed.rawStatus}` : null,
|
|
17
|
+
parsed.recordId ? `record=${parsed.recordId}` : null,
|
|
18
|
+
parsed.sessionId ? `session=${parsed.sessionId}` : null,
|
|
19
|
+
parsed.pid ? `pid=${parsed.pid}` : null,
|
|
20
|
+
]
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(", ");
|
|
23
|
+
const detailText = details ? ` (${details})` : "";
|
|
24
|
+
if (remoteStatus.state === "completed") {
|
|
25
|
+
return `Completed: ACP session finished${detailText}`;
|
|
26
|
+
}
|
|
27
|
+
if (remoteStatus.state === "running") {
|
|
28
|
+
return `Running: ACP session still active${detailText}`;
|
|
29
|
+
}
|
|
30
|
+
if (remoteStatus.state === "accepted") {
|
|
31
|
+
return `Accepted: ACP session accepted${detailText}`;
|
|
32
|
+
}
|
|
33
|
+
if (remoteStatus.state === "cancelled") {
|
|
34
|
+
return `Cancelled: ACP session stopped${detailText}`;
|
|
35
|
+
}
|
|
36
|
+
if (remoteStatus.state === "timed_out") {
|
|
37
|
+
return `Timed out: ACP session exceeded allowed time${detailText}`;
|
|
38
|
+
}
|
|
39
|
+
if (remoteStatus.state === "failed") {
|
|
40
|
+
return remoteStatus.message ? `Failed: ${remoteStatus.message}` : fallback;
|
|
41
|
+
}
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
function buildSubagentSummary(remoteStatus, fallback) {
|
|
45
|
+
const normalizedOutput = remoteStatus.outputText?.trim();
|
|
46
|
+
const normalizedMessage = remoteStatus.message?.trim();
|
|
47
|
+
if (remoteStatus.state === "completed") {
|
|
48
|
+
if (normalizedOutput) {
|
|
49
|
+
return `Completed: ${normalizedOutput.replace(/\s+/g, " ").trim()}`;
|
|
50
|
+
}
|
|
51
|
+
if (normalizedMessage) {
|
|
52
|
+
return `Completed: ${normalizedMessage}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (remoteStatus.state === "running") {
|
|
56
|
+
return normalizedMessage ? `Running: ${normalizedMessage}` : fallback;
|
|
57
|
+
}
|
|
58
|
+
if (remoteStatus.state === "accepted") {
|
|
59
|
+
return normalizedMessage ? `Accepted: ${normalizedMessage}` : fallback;
|
|
60
|
+
}
|
|
61
|
+
if (remoteStatus.state === "cancelled") {
|
|
62
|
+
return normalizedMessage ? `Cancelled: ${normalizedMessage}` : fallback;
|
|
63
|
+
}
|
|
64
|
+
if (remoteStatus.state === "failed") {
|
|
65
|
+
return normalizedMessage ? `Failed: ${normalizedMessage}` : fallback;
|
|
66
|
+
}
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
function appendStatusEvent(runRecord, remoteStatus) {
|
|
70
|
+
const events = [...(runRecord.events ?? [])];
|
|
71
|
+
events.push({
|
|
72
|
+
at: remoteStatus.checkedAt ?? new Date().toISOString(),
|
|
73
|
+
type: "status_polled",
|
|
74
|
+
detail: {
|
|
75
|
+
state: remoteStatus.state,
|
|
76
|
+
message: remoteStatus.message,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (remoteStatus.state === "completed") {
|
|
80
|
+
events.push({ at: remoteStatus.checkedAt ?? new Date().toISOString(), type: "done" });
|
|
81
|
+
}
|
|
82
|
+
else if (remoteStatus.state === "failed") {
|
|
83
|
+
events.push({ at: remoteStatus.checkedAt ?? new Date().toISOString(), type: "error", detail: { message: remoteStatus.message } });
|
|
84
|
+
}
|
|
85
|
+
else if (remoteStatus.state === "cancelled") {
|
|
86
|
+
events.push({ at: remoteStatus.checkedAt ?? new Date().toISOString(), type: "cancelled" });
|
|
87
|
+
}
|
|
88
|
+
else if (remoteStatus.state === "timed_out") {
|
|
89
|
+
events.push({ at: remoteStatus.checkedAt ?? new Date().toISOString(), type: "timeout" });
|
|
90
|
+
}
|
|
91
|
+
return events;
|
|
92
|
+
}
|
|
93
|
+
function summarizeRemoteStatus(remoteStatus, fallback) {
|
|
94
|
+
const labelByState = {
|
|
95
|
+
planned: "Planned",
|
|
96
|
+
accepted: "Accepted",
|
|
97
|
+
running: "Running",
|
|
98
|
+
completed: "Completed",
|
|
99
|
+
failed: "Failed",
|
|
100
|
+
cancelled: "Cancelled",
|
|
101
|
+
timed_out: "Timed out",
|
|
102
|
+
};
|
|
103
|
+
const normalizedOutput = remoteStatus.outputText?.trim();
|
|
104
|
+
if (normalizedOutput) {
|
|
105
|
+
const singleLine = normalizedOutput.replace(/\s+/g, " ").trim();
|
|
106
|
+
const summary = singleLine.length > 240 ? `${singleLine.slice(0, 237)}...` : singleLine;
|
|
107
|
+
return `${labelByState[remoteStatus.state]}: ${summary}`;
|
|
108
|
+
}
|
|
109
|
+
const normalizedMessage = remoteStatus.message?.trim();
|
|
110
|
+
if (normalizedMessage) {
|
|
111
|
+
return `${labelByState[remoteStatus.state]}: ${normalizedMessage}`;
|
|
112
|
+
}
|
|
113
|
+
return fallback ? `${labelByState[remoteStatus.state]}: ${fallback}` : fallback;
|
|
114
|
+
}
|
|
115
|
+
function applyTerminalFields(runRecord, remoteStatus, runtime) {
|
|
116
|
+
const nextStatus = remoteStatus.state;
|
|
117
|
+
return {
|
|
118
|
+
...runRecord,
|
|
119
|
+
status: nextStatus,
|
|
120
|
+
endedAt: nextStatus === "completed" || nextStatus === "failed" || nextStatus === "cancelled" || nextStatus === "timed_out"
|
|
121
|
+
? remoteStatus.checkedAt ?? new Date().toISOString()
|
|
122
|
+
: runRecord.endedAt,
|
|
123
|
+
resultSummary: summarizeRemoteStatus(remoteStatus, runRecord.resultSummary),
|
|
124
|
+
sessionRef: {
|
|
125
|
+
...runRecord.sessionRef,
|
|
126
|
+
runtime,
|
|
127
|
+
sessionKey: runRecord.sessionRef?.sessionKey,
|
|
128
|
+
},
|
|
129
|
+
events: appendStatusEvent(runRecord, remoteStatus),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export function syncAcpRunRecord(runRecord, remoteStatus) {
|
|
133
|
+
if (runRecord.runner.type !== "acp") {
|
|
134
|
+
throw new Error("Only ACP run records can be synced");
|
|
135
|
+
}
|
|
136
|
+
const synced = {
|
|
137
|
+
...applyTerminalFields(runRecord, remoteStatus, "acp"),
|
|
138
|
+
resultSummary: buildAcpSummary(remoteStatus, runRecord.resultSummary),
|
|
139
|
+
sessionRef: {
|
|
140
|
+
...runRecord.sessionRef,
|
|
141
|
+
runtime: "acp",
|
|
142
|
+
sessionKey: remoteStatus.sessionKey,
|
|
143
|
+
backend: remoteStatus.backend ?? runRecord.sessionRef?.backend,
|
|
144
|
+
backendSessionId: remoteStatus.backendSessionId ?? runRecord.sessionRef?.backendSessionId,
|
|
145
|
+
agentSessionId: remoteStatus.agentSessionId ?? runRecord.sessionRef?.agentSessionId,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
runRecord: synced,
|
|
150
|
+
remoteStatus,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function syncSubagentRunRecord(runRecord, remoteStatus) {
|
|
154
|
+
if (runRecord.runner.type !== "subagent") {
|
|
155
|
+
throw new Error("Only subagent run records can be synced");
|
|
156
|
+
}
|
|
157
|
+
const synced = {
|
|
158
|
+
...applyTerminalFields(runRecord, remoteStatus, "subagent"),
|
|
159
|
+
resultSummary: buildSubagentSummary(remoteStatus, runRecord.resultSummary),
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
runRecord: synced,
|
|
163
|
+
remoteStatus,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TaskNode, WorkflowState } from "../types.js";
|
|
2
|
+
import type { SubagentSpawnParams } from "./openclaw-subagent-adapter.js";
|
|
3
|
+
export type SubagentPreflightResult = {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
errors: string[];
|
|
6
|
+
warnings: string[];
|
|
7
|
+
};
|
|
8
|
+
export declare function preflightSubagentTask(task: TaskNode): SubagentPreflightResult;
|
|
9
|
+
export declare function buildSubagentSpawnParams(task: TaskNode, workflow: WorkflowState): SubagentSpawnParams;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function preflightSubagentTask(task) {
|
|
2
|
+
const errors = [];
|
|
3
|
+
const warnings = [];
|
|
4
|
+
const mode = task.runner.mode ?? "run";
|
|
5
|
+
const thread = Boolean(task.runner.threadRequested);
|
|
6
|
+
if (mode === "session" && !thread) {
|
|
7
|
+
errors.push('subagent mode "session" requires threadRequested=true');
|
|
8
|
+
}
|
|
9
|
+
if (task.runner.persistentSession) {
|
|
10
|
+
warnings.push("persistentSession is ignored for subagent runner");
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
ok: errors.length === 0,
|
|
14
|
+
errors,
|
|
15
|
+
warnings,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function buildSubagentSpawnParams(task, workflow) {
|
|
19
|
+
const preflight = preflightSubagentTask(task);
|
|
20
|
+
if (!preflight.ok) {
|
|
21
|
+
throw new Error(`Subagent preflight failed: ${preflight.errors.join("; ")}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
task: task.description || task.title,
|
|
25
|
+
label: `${task.taskId}:${task.title}`,
|
|
26
|
+
agentId: task.runner.agentId,
|
|
27
|
+
mode: task.runner.mode ?? "run",
|
|
28
|
+
thread: Boolean(task.runner.threadRequested),
|
|
29
|
+
runTimeoutSeconds: task.runner.timeoutSeconds,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type OpenClawSubagentAdapter } from "./openclaw-subagent-adapter.js";
|
|
2
|
+
import type { RunnerPlan, RunnerPlanInput, RunnerRunInput, RunnerRunResult, TaskRunner } from "./task-runner.js";
|
|
3
|
+
export declare class SubagentRunner implements TaskRunner {
|
|
4
|
+
private readonly subagentAdapter;
|
|
5
|
+
readonly kind: "subagent";
|
|
6
|
+
constructor(subagentAdapter?: OpenClawSubagentAdapter);
|
|
7
|
+
plan(input: RunnerPlanInput): Promise<RunnerPlan>;
|
|
8
|
+
run(input: RunnerRunInput): Promise<RunnerRunResult>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { buildSubagentSpawnParams } from "./subagent-mapping.js";
|
|
2
|
+
import { UnsupportedOpenClawSubagentAdapter } from "./openclaw-subagent-adapter.js";
|
|
3
|
+
export class SubagentRunner {
|
|
4
|
+
subagentAdapter;
|
|
5
|
+
kind = "subagent";
|
|
6
|
+
constructor(subagentAdapter = new UnsupportedOpenClawSubagentAdapter()) {
|
|
7
|
+
this.subagentAdapter = subagentAdapter;
|
|
8
|
+
}
|
|
9
|
+
async plan(input) {
|
|
10
|
+
const params = buildSubagentSpawnParams(input.task, input.workflow);
|
|
11
|
+
return {
|
|
12
|
+
runnable: true,
|
|
13
|
+
summary: `subagent runner is prepared for task ${input.task.taskId} in ${params.mode} mode`,
|
|
14
|
+
workspacePath: input.projectRoot,
|
|
15
|
+
nextStatus: "running",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async run(input) {
|
|
19
|
+
const params = buildSubagentSpawnParams(input.task, input.workflow);
|
|
20
|
+
const requestedAt = new Date().toISOString();
|
|
21
|
+
const accepted = await this.subagentAdapter.spawnSubagent(params);
|
|
22
|
+
const acceptedAt = accepted.acceptedAt ?? new Date().toISOString();
|
|
23
|
+
return {
|
|
24
|
+
accepted: true,
|
|
25
|
+
nextTaskStatus: "running",
|
|
26
|
+
runRecord: {
|
|
27
|
+
runId: accepted.runId,
|
|
28
|
+
taskId: input.task.taskId,
|
|
29
|
+
attempt: 1,
|
|
30
|
+
status: "accepted",
|
|
31
|
+
runner: { type: "subagent" },
|
|
32
|
+
workspacePath: input.projectRoot,
|
|
33
|
+
startedAt: requestedAt,
|
|
34
|
+
promptSummary: params.task,
|
|
35
|
+
resultSummary: accepted.note ?? `Subagent accepted: ${accepted.childSessionKey}`,
|
|
36
|
+
artifacts: [],
|
|
37
|
+
sessionRef: {
|
|
38
|
+
runtime: "subagent",
|
|
39
|
+
sessionKey: accepted.childSessionKey,
|
|
40
|
+
},
|
|
41
|
+
events: [
|
|
42
|
+
{
|
|
43
|
+
at: requestedAt,
|
|
44
|
+
type: "spawn_requested",
|
|
45
|
+
detail: {
|
|
46
|
+
mode: params.mode,
|
|
47
|
+
thread: params.thread,
|
|
48
|
+
agentId: params.agentId,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
at: acceptedAt,
|
|
53
|
+
type: "spawn_accepted",
|
|
54
|
+
detail: {
|
|
55
|
+
childSessionKey: accepted.childSessionKey,
|
|
56
|
+
runId: accepted.runId,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RunRecord, SessionRecord, TaskNode, WorkflowState } from "../types.js";
|
|
2
|
+
export type RunnerPlanInput = {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
task: TaskNode;
|
|
5
|
+
workflow: WorkflowState;
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type RunnerPlan = {
|
|
9
|
+
runnable: boolean;
|
|
10
|
+
summary: string;
|
|
11
|
+
workspacePath: string;
|
|
12
|
+
nextStatus: TaskNode["status"];
|
|
13
|
+
};
|
|
14
|
+
export type RunnerRunInput = {
|
|
15
|
+
projectRoot: string;
|
|
16
|
+
task: TaskNode;
|
|
17
|
+
workflow: WorkflowState;
|
|
18
|
+
reusedSession?: SessionRecord;
|
|
19
|
+
};
|
|
20
|
+
export type RunnerRunResult = {
|
|
21
|
+
accepted: boolean;
|
|
22
|
+
runRecord: RunRecord;
|
|
23
|
+
nextTaskStatus: TaskNode["status"];
|
|
24
|
+
};
|
|
25
|
+
export type RunnerSyncInput = {
|
|
26
|
+
projectRoot: string;
|
|
27
|
+
task: TaskNode;
|
|
28
|
+
runRecord: RunRecord;
|
|
29
|
+
};
|
|
30
|
+
export type RunnerSyncResult = {
|
|
31
|
+
runRecord: RunRecord;
|
|
32
|
+
};
|
|
33
|
+
export interface TaskRunner {
|
|
34
|
+
kind: "manual" | "acp" | "subagent";
|
|
35
|
+
plan(input: RunnerPlanInput): Promise<RunnerPlan>;
|
|
36
|
+
run(input: RunnerRunInput): Promise<RunnerRunResult>;
|
|
37
|
+
sync?(input: RunnerSyncInput): Promise<RunnerSyncResult>;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|