mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-13 16:32:54 +00:00
refactor: rename skill workshop agent tool
This commit is contained in:
@@ -185,9 +185,9 @@ scans them, verifies their hashes before apply, and writes them beside the
|
||||
active `SKILL.md` only after the proposal is applied.
|
||||
|
||||
Agents can create, revise, list, and inspect pending proposals through the
|
||||
`skill_research` tool when they identify reusable work. If the user explicitly
|
||||
`skill_workshop` tool when they identify reusable work. If the user explicitly
|
||||
asks to approve/use/apply, reject, or quarantine a specific proposal,
|
||||
`skill_research` can also perform that proposal lifecycle action through the
|
||||
`skill_workshop` can also perform that proposal lifecycle action through the
|
||||
same Skill Workshop safeguards.
|
||||
|
||||
## Related
|
||||
|
||||
@@ -96,8 +96,8 @@ For how skills are loaded and prioritized, see [Skills](/tools/skills).
|
||||
|
||||
## Propose before applying
|
||||
|
||||
For agent-generated or research-derived procedures, use a Skill Workshop
|
||||
proposal instead of writing `SKILL.md` directly:
|
||||
For agent-generated procedures, use a Skill Workshop proposal instead of
|
||||
writing `SKILL.md` directly:
|
||||
|
||||
```bash
|
||||
openclaw skills workshop propose-create \
|
||||
|
||||
@@ -169,7 +169,7 @@ openclaw skills workshop reject <proposal-id>
|
||||
openclaw skills workshop quarantine <proposal-id>
|
||||
```
|
||||
|
||||
Agents can draft proposals through the `skill_research` tool when they identify
|
||||
Agents can draft proposals through the `skill_workshop` tool when they identify
|
||||
work worth reusing and can revise pending proposals during review. When the
|
||||
user explicitly asks to approve/use/apply, reject, or quarantine a specific
|
||||
proposal, the tool can perform that lifecycle action through Skill Workshop
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("SkillsProposalInspectResultSchema", () => {
|
||||
schema: "openclaw.skill-workshop.proposal.v1",
|
||||
createdAt: "2026-05-30T00:00:00.000Z",
|
||||
updatedAt: "2026-05-30T00:00:00.000Z",
|
||||
createdBy: "skill-research",
|
||||
createdBy: "skill-workshop",
|
||||
proposedVersion: "v1",
|
||||
draftFile: "PROPOSAL.md",
|
||||
target: {
|
||||
|
||||
@@ -496,7 +496,6 @@ const SkillProposalScanStateSchema = Type.Union([
|
||||
Type.Literal("quarantined"),
|
||||
]);
|
||||
const SkillProposalSourceSchema = Type.Union([
|
||||
Type.Literal("skill-research"),
|
||||
Type.Literal("skill-workshop"),
|
||||
Type.Literal("cli"),
|
||||
Type.Literal("gateway"),
|
||||
|
||||
@@ -313,15 +313,15 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
|
||||
expect(runBeforeToolCallMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires approval before skill_research applies a proposal", async () => {
|
||||
it("requires approval before skill_workshop applies a proposal", async () => {
|
||||
(hookRunner.hasHooks as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
||||
mockCallGatewayTool.mockResolvedValueOnce({
|
||||
id: "skill-research-approval",
|
||||
id: "skill-workshop-approval",
|
||||
decision: PluginApprovalResolutions.ALLOW_ONCE,
|
||||
});
|
||||
|
||||
const result = await runBeforeToolCallHook({
|
||||
toolName: "skill_research",
|
||||
toolName: "skill_workshop",
|
||||
params: { action: "apply", proposal_id: "weather-20260530-a1b2c3d4e5" },
|
||||
toolCallId: "call-skill-apply",
|
||||
ctx: {
|
||||
@@ -342,7 +342,7 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
|
||||
params: { action: "apply", proposal_id: "weather-20260530-a1b2c3d4e5" },
|
||||
approvalResolution: PluginApprovalResolutions.ALLOW_ONCE,
|
||||
});
|
||||
const approvalCall = requireApprovalRequestCall("skill_research approval request");
|
||||
const approvalCall = requireApprovalRequestCall("skill_workshop approval request");
|
||||
expect(approvalCall.request.pluginId).toBeUndefined();
|
||||
expect(approvalCall.request.title).toBe("Apply workspace skill proposal");
|
||||
expect(approvalCall.request.description).toBe(
|
||||
@@ -350,16 +350,16 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
|
||||
);
|
||||
expect(approvalCall.request.severity).toBe("warning");
|
||||
expect(approvalCall.request.allowedDecisions).toEqual(["allow-once", "deny"]);
|
||||
expect(approvalCall.request.toolName).toBe("skill_research");
|
||||
expect(approvalCall.request.toolName).toBe("skill_workshop");
|
||||
expect(approvalCall.request.toolCallId).toBe("call-skill-apply");
|
||||
expect(runBeforeToolCallMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not require skill_research lifecycle approval in auto mode", async () => {
|
||||
it("does not require skill_workshop lifecycle approval in auto mode", async () => {
|
||||
(hookRunner.hasHooks as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
||||
|
||||
const result = await runBeforeToolCallHook({
|
||||
toolName: "skill_research",
|
||||
toolName: "skill_workshop",
|
||||
params: { action: "reject", proposal_id: "weather-20260530-a1b2c3d4e5" },
|
||||
ctx: {
|
||||
config: {
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
resolveSkillTelemetrySourceValue,
|
||||
} from "../skills/loading/source.js";
|
||||
import type { SkillSnapshot, SkillTelemetrySource } from "../skills/types.js";
|
||||
import { resolveSkillResearchToolApproval } from "../skills/workshop/policy.js";
|
||||
import { resolveSkillWorkshopToolApproval } from "../skills/workshop/policy.js";
|
||||
import { isPlainObject } from "../utils.js";
|
||||
import { adjustedParamsByToolCallId } from "./agent-tools.before-tool-call.state.js";
|
||||
import { copyChannelAgentToolMeta, getChannelAgentToolMeta } from "./channel-tools.js";
|
||||
@@ -839,7 +839,7 @@ export async function runBeforeToolCallHook(args: {
|
||||
const hasBeforeToolCallHooks = hookRunner?.hasHooks("before_tool_call") === true;
|
||||
const shouldRunTrustedPolicies = hasTrustedToolPolicies();
|
||||
const normalizedParams = isPlainObject(params) ? params : {};
|
||||
const corePolicyResult = resolveSkillResearchToolApproval({
|
||||
const corePolicyResult = resolveSkillWorkshopToolApproval({
|
||||
toolName,
|
||||
toolParams: normalizedParams,
|
||||
...(args.ctx?.config ? { config: args.ctx.config } : {}),
|
||||
|
||||
@@ -1050,12 +1050,12 @@ describe("createOpenClawCodingTools", () => {
|
||||
expect(toolNameList(tools)).toContain("heartbeat_respond");
|
||||
});
|
||||
|
||||
it("keeps skill_research available under the coding profile", () => {
|
||||
it("keeps skill_workshop available under the coding profile", () => {
|
||||
const tools = createOpenClawCodingTools({
|
||||
config: { tools: { profile: "coding" } },
|
||||
});
|
||||
|
||||
expect(toolNameList(tools)).toContain("skill_research");
|
||||
expect(toolNameList(tools)).toContain("skill_workshop");
|
||||
});
|
||||
|
||||
it("can keep message available when a cron route needs it under a provider coding profile", () => {
|
||||
|
||||
@@ -302,11 +302,11 @@ describe("resolveEmbeddedAttemptToolConstructionPlan", () => {
|
||||
},
|
||||
);
|
||||
expectConstructionPlan(
|
||||
resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["skill_research"] }),
|
||||
resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["skill_workshop"] }),
|
||||
{
|
||||
constructTools: true,
|
||||
includeCoreTools: true,
|
||||
runtimeToolAllowlist: ["skill_research"],
|
||||
runtimeToolAllowlist: ["skill_workshop"],
|
||||
coding: {
|
||||
includeBaseCodingTools: false,
|
||||
includeShellTools: false,
|
||||
|
||||
@@ -36,7 +36,7 @@ const OPENCLAW_TOOL_FACTORY_NAMES = new Set([
|
||||
"sessions_send",
|
||||
"sessions_spawn",
|
||||
"sessions_yield",
|
||||
"skill_research",
|
||||
"skill_workshop",
|
||||
"create_goal",
|
||||
"subagents",
|
||||
"tts",
|
||||
|
||||
@@ -56,7 +56,7 @@ import { createSessionsListTool } from "./tools/sessions-list-tool.js";
|
||||
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
|
||||
import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
||||
import { createSessionsYieldTool } from "./tools/sessions-yield-tool.js";
|
||||
import { createSkillResearchTool } from "./tools/skill-research-tool.js";
|
||||
import { createSkillWorkshopTool } from "./tools/skill-workshop-tool.js";
|
||||
import { createSubagentsTool } from "./tools/subagents-tool.js";
|
||||
import { createTranscriptsTool } from "./tools/transcripts-tool.js";
|
||||
import { createTtsTool } from "./tools/tts-tool.js";
|
||||
@@ -445,7 +445,7 @@ export function createOpenClawTools(
|
||||
...(options?.sandboxed
|
||||
? []
|
||||
: [
|
||||
createSkillResearchTool({
|
||||
createSkillWorkshopTool({
|
||||
workspaceDir,
|
||||
config: resolvedConfig,
|
||||
agentId: sessionAgentId,
|
||||
|
||||
@@ -683,24 +683,24 @@ describe("buildAgentSystemPrompt", () => {
|
||||
expect(prompt).toContain("If several apply, choose the most specific.");
|
||||
});
|
||||
|
||||
it("instructs models to use skill_research only when the tool is available", () => {
|
||||
it("instructs models to use skill_workshop only when the tool is available", () => {
|
||||
const withoutTool = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
toolNames: ["read"],
|
||||
});
|
||||
expect(withoutTool).not.toContain("## Skill Research");
|
||||
expect(withoutTool).not.toContain("use `skill_research`");
|
||||
expect(withoutTool).not.toContain("## Skill Workshop");
|
||||
expect(withoutTool).not.toContain("use `skill_workshop`");
|
||||
|
||||
const withTool = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
toolNames: ["read", "skill_research"],
|
||||
toolNames: ["read", "skill_workshop"],
|
||||
});
|
||||
expect(withTool).toContain(
|
||||
"- skill_research: Create, update, revise, list, inspect, apply, reject, or quarantine Skill Workshop proposals",
|
||||
"- skill_workshop: Create, update, revise, list, inspect, apply, reject, or quarantine Skill Workshop proposals",
|
||||
);
|
||||
expect(withTool).toContain("## Skill Research");
|
||||
expect(withTool).toContain("## Skill Workshop");
|
||||
expect(withTool).toContain(
|
||||
"Use `skill_research` when the user wants to capture, create, draft, formalize, improve, update, or revise a reusable skill, playbook, workflow, procedure, or durable instruction.",
|
||||
"Use `skill_workshop` when the user wants to create, update, revise, list, inspect, apply, reject, or quarantine a reusable skill, Skill Workshop proposal, playbook, workflow, procedure, or durable instruction.",
|
||||
);
|
||||
expect(withTool).toContain(
|
||||
"Treat a request as durable when it should be saved, repeated, proposed, installed later, shared as a skill, or used as a standing workflow instead of answered once in chat.",
|
||||
@@ -716,11 +716,12 @@ describe("buildAgentSystemPrompt", () => {
|
||||
expect(withTool).toContain(
|
||||
"Use `action=apply`, `action=reject`, or `action=quarantine` only after the user explicitly asks to approve/use/apply, reject, or quarantine a specific proposal.",
|
||||
);
|
||||
expect(withTool).toContain("Generated skills are pending proposals by default.");
|
||||
expect(withTool).toContain(
|
||||
"Do not apply, reject, or quarantine proposals manually with filesystem operations or shell commands.",
|
||||
);
|
||||
expect(withTool).toContain(
|
||||
"You may gather context first, but the durable proposal write or lifecycle change must use `skill_research`.",
|
||||
"You may gather context first, but the durable proposal write or lifecycle change must use `skill_workshop`.",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -750,7 +750,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
"On-demand list/status visibility for sub-agent runs in this requester session; do not use for wait loops",
|
||||
session_status:
|
||||
"Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
|
||||
skill_research:
|
||||
skill_workshop:
|
||||
"Create, update, revise, list, inspect, apply, reject, or quarantine Skill Workshop proposals",
|
||||
image: "Analyze an image with the configured image model",
|
||||
image_generate: "Generate images with the configured image-generation model",
|
||||
@@ -782,7 +782,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
"sessions_yield",
|
||||
"subagents",
|
||||
"session_status",
|
||||
"skill_research",
|
||||
"skill_workshop",
|
||||
"image",
|
||||
"image_generate",
|
||||
];
|
||||
@@ -923,20 +923,20 @@ export function buildAgentSystemPrompt(params: {
|
||||
skillsPrompt,
|
||||
readToolName,
|
||||
});
|
||||
const skillResearchSection = availableTools.has("skill_research")
|
||||
const skillWorkshopSection = availableTools.has("skill_workshop")
|
||||
? [
|
||||
"## Skill Research",
|
||||
"Use `skill_research` when the user wants to capture, create, draft, formalize, improve, update, or revise a reusable skill, playbook, workflow, procedure, or durable instruction.",
|
||||
"## Skill Workshop",
|
||||
"Use `skill_workshop` when the user wants to create, update, revise, list, inspect, apply, reject, or quarantine a reusable skill, Skill Workshop proposal, playbook, workflow, procedure, or durable instruction.",
|
||||
"Treat a request as durable when it should be saved, repeated, proposed, installed later, shared as a skill, or used as a standing workflow instead of answered once in chat.",
|
||||
"Do not create or change skill proposal files manually with `write`, `edit`, `exec`, shell commands, or direct filesystem operations. The final proposal artifact must go through `skill_research`.",
|
||||
"Do not create or change skill proposal files manually with `write`, `edit`, `exec`, shell commands, or direct filesystem operations. The final proposal artifact must go through `skill_workshop`.",
|
||||
"Use `action=create` for a new skill, `action=update` for an existing approved/live skill, and `action=revise` for an existing pending proposal.",
|
||||
"For `action=revise`, pass `proposal_id` when known. If it is not known, pass the proposal or skill name in `name` so `skill_research` can resolve the pending proposal or return candidates.",
|
||||
"For `action=revise`, pass `proposal_id` when known. If it is not known, pass the proposal or skill name in `name` so `skill_workshop` can resolve the pending proposal or return candidates.",
|
||||
"Use `action=list` or `action=inspect` only when you need to find or read pending proposals before revising. Do not use filesystem search for proposal discovery.",
|
||||
"If the user names an existing skill, proposal, or workflow, inspect it first when available.",
|
||||
"Generated or researched skills are pending proposals by default. Do not apply, install, approve, enable, or write into live skills unless the user explicitly asks for that separate action.",
|
||||
"Generated skills are pending proposals by default. Do not apply, install, approve, enable, or write into live skills unless the user explicitly asks for that separate action.",
|
||||
"Use `action=apply`, `action=reject`, or `action=quarantine` only after the user explicitly asks to approve/use/apply, reject, or quarantine a specific proposal. Pass `proposal_id`; if it is not known, use `action=list` or `action=inspect` first.",
|
||||
"Do not apply, reject, or quarantine proposals manually with filesystem operations or shell commands. Proposal lifecycle changes must use `skill_research`.",
|
||||
"You may gather context first, but the durable proposal write or lifecycle change must use `skill_research`.",
|
||||
"Do not apply, reject, or quarantine proposals manually with filesystem operations or shell commands. Proposal lifecycle changes must use `skill_workshop`.",
|
||||
"You may gather context first, but the durable proposal write or lifecycle change must use `skill_workshop`.",
|
||||
"",
|
||||
]
|
||||
: [];
|
||||
@@ -1102,7 +1102,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
"`restart`, not stop+start.",
|
||||
"",
|
||||
...skillsSection,
|
||||
...skillResearchSection,
|
||||
...skillWorkshopSection,
|
||||
...memorySection,
|
||||
hasGateway && !isMinimal ? "## OpenClaw Self-Update" : "",
|
||||
hasGateway && !isMinimal
|
||||
|
||||
@@ -38,7 +38,7 @@ const coreTools = [
|
||||
stubActionTool("sessions_spawn", ["spawn", "handoff"]),
|
||||
stubActionTool("subagents", ["list", "show"]),
|
||||
stubActionTool("session_status", ["get", "show"]),
|
||||
stubTool("skill_research"),
|
||||
stubTool("skill_workshop"),
|
||||
stubActionTool("browser", ["status", "snapshot"]),
|
||||
stubTool("tts"),
|
||||
stubTool("image_generate"),
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("tool-catalog", () => {
|
||||
"create_goal",
|
||||
"update_goal",
|
||||
"update_plan",
|
||||
"skill_research",
|
||||
"skill_workshop",
|
||||
"image",
|
||||
"image_generate",
|
||||
"music_generate",
|
||||
|
||||
@@ -293,8 +293,8 @@ const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [
|
||||
includeInOpenClawGroup: true,
|
||||
},
|
||||
{
|
||||
id: "skill_research",
|
||||
label: "skill_research",
|
||||
id: "skill_workshop",
|
||||
label: "skill_workshop",
|
||||
description:
|
||||
"Create, update, revise, list, inspect, apply, reject, or quarantine Skill Workshop proposals",
|
||||
sectionId: "agents",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { captureEnv } from "../../test-utils/env.js";
|
||||
import { createTrackedTempDirs } from "../../test-utils/tracked-temp-dirs.js";
|
||||
import { createOpenClawTools } from "../openclaw-tools.js";
|
||||
import { createSkillResearchTool } from "./skill-research-tool.js";
|
||||
import { createSkillWorkshopTool } from "./skill-workshop-tool.js";
|
||||
|
||||
const tempDirs = createTrackedTempDirs();
|
||||
let envSnapshot: ReturnType<typeof captureEnv>;
|
||||
@@ -12,7 +12,7 @@ let stateDir = "";
|
||||
|
||||
beforeEach(async () => {
|
||||
envSnapshot = captureEnv(["OPENCLAW_STATE_DIR"]);
|
||||
stateDir = await tempDirs.make("openclaw-skill-research-state-");
|
||||
stateDir = await tempDirs.make("openclaw-skill-workshop-state-");
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
});
|
||||
|
||||
@@ -21,19 +21,19 @@ afterEach(async () => {
|
||||
await tempDirs.cleanup();
|
||||
});
|
||||
|
||||
describe("skill_research tool", () => {
|
||||
describe("skill_workshop tool", () => {
|
||||
it("is exposed in the OpenClaw tool set", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-research-tool-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-workshop-tool-");
|
||||
const tools = createOpenClawTools({
|
||||
workspaceDir,
|
||||
config: {},
|
||||
disablePluginTools: true,
|
||||
});
|
||||
expect(tools.some((tool) => tool.name === "skill_research")).toBe(true);
|
||||
expect(tools.some((tool) => tool.name === "skill_workshop")).toBe(true);
|
||||
});
|
||||
|
||||
it("stays exposed when autonomous proposal capture is disabled", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-research-tool-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-workshop-tool-");
|
||||
const tools = createOpenClawTools({
|
||||
workspaceDir,
|
||||
config: {
|
||||
@@ -47,11 +47,11 @@ describe("skill_research tool", () => {
|
||||
},
|
||||
disablePluginTools: true,
|
||||
});
|
||||
expect(tools.some((tool) => tool.name === "skill_research")).toBe(true);
|
||||
expect(tools.some((tool) => tool.name === "skill_workshop")).toBe(true);
|
||||
});
|
||||
|
||||
it("is not exposed from sandboxed OpenClaw tool sets", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-research-tool-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-workshop-tool-");
|
||||
const tools = createOpenClawTools({
|
||||
workspaceDir,
|
||||
config: {},
|
||||
@@ -59,12 +59,12 @@ describe("skill_research tool", () => {
|
||||
sandboxed: true,
|
||||
});
|
||||
|
||||
expect(tools.some((tool) => tool.name === "skill_research")).toBe(false);
|
||||
expect(tools.some((tool) => tool.name === "skill_workshop")).toBe(false);
|
||||
});
|
||||
|
||||
it("creates pending skill proposals without applying them", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-research-tool-");
|
||||
const tool = createSkillResearchTool({ workspaceDir, config: {}, agentId: "main" });
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-workshop-tool-");
|
||||
const tool = createSkillWorkshopTool({ workspaceDir, config: {}, agentId: "main" });
|
||||
|
||||
const result = await tool.execute("call-1", {
|
||||
action: "create",
|
||||
@@ -206,8 +206,8 @@ describe("skill_research tool", () => {
|
||||
});
|
||||
|
||||
it("applies, rejects, and quarantines proposals through the workshop service", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-research-tool-");
|
||||
const tool = createSkillResearchTool({ workspaceDir, config: {}, agentId: "main" });
|
||||
const workspaceDir = await tempDirs.make("openclaw-skill-workshop-tool-");
|
||||
const tool = createSkillWorkshopTool({ workspaceDir, config: {}, agentId: "main" });
|
||||
|
||||
const created = await tool.execute("call-1", {
|
||||
action: "create",
|
||||
@@ -307,14 +307,14 @@ describe("skill_research tool", () => {
|
||||
});
|
||||
|
||||
it("scopes proposal discovery to the tool workspace", async () => {
|
||||
const firstWorkspaceDir = await tempDirs.make("openclaw-skill-research-tool-first-");
|
||||
const secondWorkspaceDir = await tempDirs.make("openclaw-skill-research-tool-second-");
|
||||
const firstTool = createSkillResearchTool({
|
||||
const firstWorkspaceDir = await tempDirs.make("openclaw-skill-workshop-tool-first-");
|
||||
const secondWorkspaceDir = await tempDirs.make("openclaw-skill-workshop-tool-second-");
|
||||
const firstTool = createSkillWorkshopTool({
|
||||
workspaceDir: firstWorkspaceDir,
|
||||
config: {},
|
||||
agentId: "main",
|
||||
});
|
||||
const secondTool = createSkillResearchTool({
|
||||
const secondTool = createSkillWorkshopTool({
|
||||
workspaceDir: secondWorkspaceDir,
|
||||
config: {},
|
||||
agentId: "main",
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
type AnyAgentTool,
|
||||
} from "./common.js";
|
||||
|
||||
const SKILL_RESEARCH_ACTIONS = [
|
||||
const SKILL_WORKSHOP_ACTIONS = [
|
||||
"create",
|
||||
"update",
|
||||
"revise",
|
||||
@@ -45,9 +45,9 @@ const SKILL_PROPOSAL_STATUSES = [
|
||||
"stale",
|
||||
] as const satisfies readonly SkillProposalStatus[];
|
||||
|
||||
const SkillResearchToolSchema = Type.Object(
|
||||
const SkillWorkshopToolSchema = Type.Object(
|
||||
{
|
||||
action: stringEnum(SKILL_RESEARCH_ACTIONS, {
|
||||
action: stringEnum(SKILL_WORKSHOP_ACTIONS, {
|
||||
description:
|
||||
"create for a new skill proposal, update for an existing skill, revise for a pending proposal, list or inspect proposals for proposal discovery, apply/reject/quarantine for explicit proposal lifecycle actions.",
|
||||
}),
|
||||
@@ -103,7 +103,7 @@ const SkillResearchToolSchema = Type.Object(
|
||||
{ description: "Optional support files to store with the proposal." },
|
||||
),
|
||||
),
|
||||
goal: Type.Optional(Type.String({ description: "Research or improvement goal." })),
|
||||
goal: Type.Optional(Type.String({ description: "Proposal or improvement goal." })),
|
||||
evidence: Type.Optional(Type.String({ description: "Short evidence or notes." })),
|
||||
reason: Type.Optional(
|
||||
Type.String({
|
||||
@@ -114,20 +114,20 @@ const SkillResearchToolSchema = Type.Object(
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export type SkillResearchToolOptions = {
|
||||
export type SkillWorkshopToolOptions = {
|
||||
workspaceDir: string;
|
||||
config?: OpenClawConfig;
|
||||
agentId?: string;
|
||||
};
|
||||
|
||||
export function createSkillResearchTool(options: SkillResearchToolOptions): AnyAgentTool {
|
||||
export function createSkillWorkshopTool(options: SkillWorkshopToolOptions): AnyAgentTool {
|
||||
return {
|
||||
label: "Skill Research",
|
||||
name: "skill_research",
|
||||
label: "Skill Workshop",
|
||||
name: "skill_workshop",
|
||||
displaySummary: "Propose a reusable skill",
|
||||
description:
|
||||
"Create, update, revise, list, inspect, apply, reject, or quarantine Skill Workshop proposals when reusable procedures should be captured, improved, or explicitly approved.",
|
||||
parameters: SkillResearchToolSchema,
|
||||
parameters: SkillWorkshopToolSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = asToolParamsRecord(args);
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
@@ -205,7 +205,7 @@ export function createSkillResearchTool(options: SkillResearchToolOptions): AnyA
|
||||
description: readStringParam(params, "description", { required: true }),
|
||||
content: proposalContent,
|
||||
supportFiles,
|
||||
createdBy: "skill-research",
|
||||
createdBy: "skill-workshop",
|
||||
goal,
|
||||
evidence,
|
||||
});
|
||||
@@ -220,7 +220,7 @@ export function createSkillResearchTool(options: SkillResearchToolOptions): AnyA
|
||||
}),
|
||||
content: proposalContent,
|
||||
supportFiles,
|
||||
createdBy: "skill-research",
|
||||
createdBy: "skill-workshop",
|
||||
goal,
|
||||
evidence,
|
||||
});
|
||||
@@ -242,7 +242,7 @@ export function createSkillResearchTool(options: SkillResearchToolOptions): AnyA
|
||||
evidence,
|
||||
});
|
||||
} else {
|
||||
throw new ToolInputError(`action must be one of ${SKILL_RESEARCH_ACTIONS.join(", ")}`);
|
||||
throw new ToolInputError(`action must be one of ${SKILL_WORKSHOP_ACTIONS.join(", ")}`);
|
||||
}
|
||||
|
||||
return proposalResult(proposal);
|
||||
@@ -552,7 +552,7 @@ export function registerSkillsCli(program: Command) {
|
||||
"--proposal-dir <path>",
|
||||
"Path to proposal directory with PROPOSAL.md and UTF-8 text support files",
|
||||
)
|
||||
.option("--goal <text>", "Research or improvement goal")
|
||||
.option("--goal <text>", "Proposal or improvement goal")
|
||||
.option("--evidence <text>", "Evidence or notes for the proposal")
|
||||
.option("--json", "Output as JSON", false)
|
||||
.action(
|
||||
@@ -603,7 +603,7 @@ export function registerSkillsCli(program: Command) {
|
||||
"--proposal-dir <path>",
|
||||
"Path to proposal directory with PROPOSAL.md and UTF-8 text support files",
|
||||
)
|
||||
.option("--goal <text>", "Research or improvement goal")
|
||||
.option("--goal <text>", "Proposal or improvement goal")
|
||||
.option("--evidence <text>", "Evidence or notes for the proposal")
|
||||
.option("--json", "Output as JSON", false)
|
||||
.action(
|
||||
|
||||
@@ -3,19 +3,19 @@ import type { PluginHookBeforeToolCallResult } from "../../plugins/types.js";
|
||||
import { asNullableRecord } from "../../shared/record-coerce.js";
|
||||
import { resolveSkillWorkshopConfig } from "./config.js";
|
||||
|
||||
const SKILL_RESEARCH_LIFECYCLE_ACTIONS = new Set(["apply", "reject", "quarantine"]);
|
||||
const SKILL_WORKSHOP_LIFECYCLE_ACTIONS = new Set(["apply", "reject", "quarantine"]);
|
||||
|
||||
type SkillResearchLifecycleAction = "apply" | "reject" | "quarantine";
|
||||
type SkillWorkshopLifecycleAction = "apply" | "reject" | "quarantine";
|
||||
|
||||
function readLifecycleAction(params: unknown): SkillResearchLifecycleAction | undefined {
|
||||
function readLifecycleAction(params: unknown): SkillWorkshopLifecycleAction | undefined {
|
||||
const action = asNullableRecord(params)?.action;
|
||||
if (typeof action !== "string" || !SKILL_RESEARCH_LIFECYCLE_ACTIONS.has(action)) {
|
||||
if (typeof action !== "string" || !SKILL_WORKSHOP_LIFECYCLE_ACTIONS.has(action)) {
|
||||
return undefined;
|
||||
}
|
||||
return action as SkillResearchLifecycleAction;
|
||||
return action as SkillWorkshopLifecycleAction;
|
||||
}
|
||||
|
||||
function lifecycleApprovalText(action: SkillResearchLifecycleAction): {
|
||||
function lifecycleApprovalText(action: SkillWorkshopLifecycleAction): {
|
||||
title: string;
|
||||
description: string;
|
||||
severity: "info" | "warning";
|
||||
@@ -41,12 +41,12 @@ function lifecycleApprovalText(action: SkillResearchLifecycleAction): {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSkillResearchToolApproval(params: {
|
||||
export function resolveSkillWorkshopToolApproval(params: {
|
||||
toolName: string;
|
||||
toolParams: unknown;
|
||||
config?: OpenClawConfig;
|
||||
}): PluginHookBeforeToolCallResult | undefined {
|
||||
if (params.toolName !== "skill_research") {
|
||||
if (params.toolName !== "skill_workshop") {
|
||||
return undefined;
|
||||
}
|
||||
const action = readLifecycleAction(params.toolParams);
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("skill workshop proposals", () => {
|
||||
content: "export function parseWeather(value) { return value; }\n",
|
||||
},
|
||||
],
|
||||
createdBy: "skill-research",
|
||||
createdBy: "skill-workshop",
|
||||
goal: "Reuse weather lookup steps",
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export const SKILL_WORKSHOP_ROLLBACK_SCHEMA = "openclaw.skill-workshop.rollback.
|
||||
export type SkillProposalKind = "create" | "update";
|
||||
export type SkillProposalStatus = "pending" | "applied" | "rejected" | "quarantined" | "stale";
|
||||
export type SkillProposalScannerState = "pending" | "clean" | "failed" | "quarantined";
|
||||
export type SkillProposalSource = "skill-research" | "skill-workshop" | "cli" | "gateway";
|
||||
export type SkillProposalSource = "skill-workshop" | "cli" | "gateway";
|
||||
|
||||
export type SkillProposalScan = {
|
||||
state: SkillProposalScannerState;
|
||||
|
||||
Reference in New Issue
Block a user