mirror of https://github.com/openclaw/openclaw.git
183 lines
6.1 KiB
TypeScript
183 lines
6.1 KiB
TypeScript
import { Type } from "@sinclair/typebox";
|
|
import { loadConfig } from "../../config/config.js";
|
|
import { optionalStringEnum } from "../schema/typebox.js";
|
|
import {
|
|
buildSubagentList,
|
|
DEFAULT_RECENT_MINUTES,
|
|
isActiveSubagentRun,
|
|
killAllControlledSubagentRuns,
|
|
killControlledSubagentRun,
|
|
listControlledSubagentRuns,
|
|
MAX_RECENT_MINUTES,
|
|
MAX_STEER_MESSAGE_CHARS,
|
|
resolveControlledSubagentTarget,
|
|
resolveSubagentController,
|
|
steerControlledSubagentRun,
|
|
createPendingDescendantCounter,
|
|
} from "../subagent-control.js";
|
|
import type { AnyAgentTool } from "./common.js";
|
|
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
|
|
|
|
const SUBAGENT_ACTIONS = ["list", "kill", "steer"] as const;
|
|
type SubagentAction = (typeof SUBAGENT_ACTIONS)[number];
|
|
|
|
const SubagentsToolSchema = Type.Object({
|
|
action: optionalStringEnum(SUBAGENT_ACTIONS),
|
|
target: Type.Optional(Type.String()),
|
|
message: Type.Optional(Type.String()),
|
|
recentMinutes: Type.Optional(Type.Number({ minimum: 1 })),
|
|
});
|
|
|
|
export function createSubagentsTool(opts?: { agentSessionKey?: string }): AnyAgentTool {
|
|
return {
|
|
label: "Subagents",
|
|
name: "subagents",
|
|
description:
|
|
"List, kill, or steer spawned sub-agents for this requester session. Use this for sub-agent orchestration.",
|
|
parameters: SubagentsToolSchema,
|
|
execute: async (_toolCallId, args) => {
|
|
const params = args as Record<string, unknown>;
|
|
const action = (readStringParam(params, "action") ?? "list") as SubagentAction;
|
|
const cfg = loadConfig();
|
|
const controller = resolveSubagentController({
|
|
cfg,
|
|
agentSessionKey: opts?.agentSessionKey,
|
|
});
|
|
const runs = listControlledSubagentRuns(controller.controllerSessionKey);
|
|
const recentMinutesRaw = readNumberParam(params, "recentMinutes");
|
|
const recentMinutes = recentMinutesRaw
|
|
? Math.max(1, Math.min(MAX_RECENT_MINUTES, Math.floor(recentMinutesRaw)))
|
|
: DEFAULT_RECENT_MINUTES;
|
|
const pendingDescendantCount = createPendingDescendantCounter();
|
|
const isActive = (entry: (typeof runs)[number]) =>
|
|
isActiveSubagentRun(entry, pendingDescendantCount);
|
|
|
|
if (action === "list") {
|
|
const list = buildSubagentList({
|
|
cfg,
|
|
runs,
|
|
recentMinutes,
|
|
});
|
|
return jsonResult({
|
|
status: "ok",
|
|
action: "list",
|
|
requesterSessionKey: controller.controllerSessionKey,
|
|
callerSessionKey: controller.callerSessionKey,
|
|
callerIsSubagent: controller.callerIsSubagent,
|
|
total: list.total,
|
|
active: list.active.map(({ line: _line, ...view }) => view),
|
|
recent: list.recent.map(({ line: _line, ...view }) => view),
|
|
text: list.text,
|
|
});
|
|
}
|
|
|
|
if (action === "kill") {
|
|
const target = readStringParam(params, "target", { required: true });
|
|
if (target === "all" || target === "*") {
|
|
const result = await killAllControlledSubagentRuns({
|
|
cfg,
|
|
controller,
|
|
runs,
|
|
});
|
|
if (result.status === "forbidden") {
|
|
return jsonResult({
|
|
status: "forbidden",
|
|
action: "kill",
|
|
target: "all",
|
|
error: result.error,
|
|
});
|
|
}
|
|
return jsonResult({
|
|
status: "ok",
|
|
action: "kill",
|
|
target: "all",
|
|
killed: result.killed,
|
|
labels: result.labels,
|
|
text:
|
|
result.killed > 0
|
|
? `killed ${result.killed} subagent${result.killed === 1 ? "" : "s"}.`
|
|
: "no running subagents to kill.",
|
|
});
|
|
}
|
|
const resolved = resolveControlledSubagentTarget(runs, target, {
|
|
recentMinutes,
|
|
isActive,
|
|
});
|
|
if (!resolved.entry) {
|
|
return jsonResult({
|
|
status: "error",
|
|
action: "kill",
|
|
target,
|
|
error: resolved.error ?? "Unknown subagent target.",
|
|
});
|
|
}
|
|
const result = await killControlledSubagentRun({
|
|
cfg,
|
|
controller,
|
|
entry: resolved.entry,
|
|
});
|
|
return jsonResult({
|
|
status: result.status,
|
|
action: "kill",
|
|
target,
|
|
runId: result.runId,
|
|
sessionKey: result.sessionKey,
|
|
label: result.label,
|
|
cascadeKilled: "cascadeKilled" in result ? result.cascadeKilled : undefined,
|
|
cascadeLabels: "cascadeLabels" in result ? result.cascadeLabels : undefined,
|
|
error: "error" in result ? result.error : undefined,
|
|
text: result.text,
|
|
});
|
|
}
|
|
|
|
if (action === "steer") {
|
|
const target = readStringParam(params, "target", { required: true });
|
|
const message = readStringParam(params, "message", { required: true });
|
|
if (message.length > MAX_STEER_MESSAGE_CHARS) {
|
|
return jsonResult({
|
|
status: "error",
|
|
action: "steer",
|
|
target,
|
|
error: `Message too long (${message.length} chars, max ${MAX_STEER_MESSAGE_CHARS}).`,
|
|
});
|
|
}
|
|
const resolved = resolveControlledSubagentTarget(runs, target, {
|
|
recentMinutes,
|
|
isActive,
|
|
});
|
|
if (!resolved.entry) {
|
|
return jsonResult({
|
|
status: "error",
|
|
action: "steer",
|
|
target,
|
|
error: resolved.error ?? "Unknown subagent target.",
|
|
});
|
|
}
|
|
const result = await steerControlledSubagentRun({
|
|
cfg,
|
|
controller,
|
|
entry: resolved.entry,
|
|
message,
|
|
});
|
|
return jsonResult({
|
|
status: result.status,
|
|
action: "steer",
|
|
target,
|
|
runId: result.runId,
|
|
sessionKey: result.sessionKey,
|
|
sessionId: result.sessionId,
|
|
mode: "mode" in result ? result.mode : undefined,
|
|
label: "label" in result ? result.label : undefined,
|
|
error: "error" in result ? result.error : undefined,
|
|
text: result.text,
|
|
});
|
|
}
|
|
|
|
return jsonResult({
|
|
status: "error",
|
|
error: "Unsupported action.",
|
|
});
|
|
},
|
|
};
|
|
}
|