diff --git a/packages/core/src/tool/plugin/skill.ts b/packages/core/src/tool/plugin/skill.ts index 55fef6599090..da3ada17562f 100644 --- a/packages/core/src/tool/plugin/skill.ts +++ b/packages/core/src/tool/plugin/skill.ts @@ -1,9 +1,10 @@ export * as SkillTool from "./skill.js" -import type { Context } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" +import type { Context } from "@opencode-ai/plugin/effect/plugin" import { Effect, Schema } from "effect" import { FSUtil } from "@opencode-ai/util/fs-util" +import { Agent } from "../../agent.js" import { Skill } from "../../skill.js" import { Permission } from "../../permission.js" @@ -34,6 +35,7 @@ export const Plugin = { effect: Effect.fn("SkillTool.Plugin")(function* (ctx: Context) { const fs = yield* FSUtil.Service const skills = yield* Skill.Service + const agents = yield* Agent.Service const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => @@ -46,7 +48,13 @@ export const Plugin = { execute: (input, context) => Effect.gen(function* () { const skill = yield* skills.get(input.id) - if (!skill) return yield* unableToLoad(input.id) + if (!skill) { + if ((yield* agents.resolve(input.id)) !== undefined) + return yield* new ToolFailure({ + message: `\`${input.id}\` is an agent, not a skill. Prompt the agent as a subagent instead of loading it as a skill.`, + }) + return yield* unableToLoad(input.id) + } return yield* Effect.gen(function* () { yield* permission.assert({ action: name, diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts index ecd27383a032..7770f217fde0 100644 --- a/packages/core/test/tool-skill.test.ts +++ b/packages/core/test/tool-skill.test.ts @@ -5,6 +5,7 @@ import { Effect, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Permission } from "@opencode-ai/core/permission" +import { Agent } from "@opencode-ai/core/agent" import { AbsolutePath } from "@opencode-ai/core/schema" import { Session } from "@opencode-ai/core/session" import { Skill } from "@opencode-ai/core/skill" @@ -22,7 +23,21 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from " const skillToolNode = makeLocationNode({ name: "test/skill-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(SkillTool.Plugin)), - deps: [Tool.node, FSUtil.node, Skill.node, Permission.node], + deps: [Tool.node, FSUtil.node, Skill.node, Permission.node, Agent.node], +}) + +const agentInfo = Agent.Info.make({ + id: Agent.ID.make("my-agent"), + name: Agent.Name.make("My Agent"), + request: { settings: {}, headers: {}, body: {} }, + mode: "subagent", + hidden: false, + permissions: [], +}) + +const agentsMock = Layer.mock(Agent.Service, { + resolve: (id: string | undefined) => Effect.succeed(id === "my-agent" ? agentInfo : undefined), + list: () => Effect.succeed([agentInfo]), }) const sessionID = Session.ID.make("ses_skill_tool_test") @@ -76,6 +91,7 @@ describe("SkillTool", () => { const skillToolLayer = AppNodeBuilder.build(LayerNode.group([Tool.node, skillToolNode]), [ Permission.node.replace(permission), Skill.node.replace(skills), + Agent.node.replace(agentsMock), Image.node.replace(imagePassthrough), ]) @@ -122,6 +138,19 @@ describe("SkillTool", () => { status: "error", error: { type: "tool.execution", message: "Unable to load skill missing" }, }) + expect( + yield* executeTool(registry, { + sessionID, + ...toolIdentity, + call: { type: "tool-call", id: "call-agent-as-skill", name: "skill", input: { id: "my-agent" } }, + }), + ).toEqual({ + status: "error", + error: { + type: "tool.execution", + message: "`my-agent` is an agent, not a skill. Prompt the agent as a subagent instead of loading it as a skill.", + }, + }) deny = true expect( yield* executeTool(registry, {