import { Tool } from "./tool" import DESCRIPTION from "./task.txt" import z from "zod" import { Session } from "../session" import { Bus } from "../bus" import { MessageV2 } from "../session/message-v2" import { Identifier } from "../id/id" import { Agent } from "../agent/agent" import { SessionPrompt } from "../session/prompt" import { iife } from "@/util/iife" import { defer } from "@/util/defer" import { Config } from "../config/config" import { PermissionNext } from "@/permission/next" const parameters = z.object({ description: z.string().describe("A short (3-5 words) description of the task"), prompt: z.string().describe("The task for the agent to perform"), subagent_type: z.string().describe("The type of specialized agent to use for this task"), session_id: z.string().describe("Existing Task session to continue").optional(), command: z.string().describe("The command that triggered this task").optional(), }) export const TaskTool = Tool.define("task", async (ctx) => { const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary")) // Filter agents by permissions if agent provided const caller = ctx?.agent const accessibleAgents = caller ? agents.filter((a) => PermissionNext.evaluate("task", a.name, caller.permission).action !== "deny") : agents const description = DESCRIPTION.replace( "{agents}", accessibleAgents .map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`) .join("\n"), ) return { description, parameters, async execute(params: z.infer, ctx) { const config = await Config.get() // Skip permission check when user explicitly invoked via @ or command subtask if (!ctx.extra?.bypassAgentCheck) { await ctx.ask({ permission: "task", patterns: [params.subagent_type], always: ["*"], metadata: { description: params.description, subagent_type: params.subagent_type, }, }) } const agent = await Agent.get(params.subagent_type) if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`) const hasTaskPermission = agent.permission.some((rule) => rule.permission === "task") const session = await iife(async () => { if (params.session_id) { const found = await Session.get(params.session_id).catch(() => {}) if (found) return found } return await Session.create({ parentID: ctx.sessionID, title: params.description + ` (@${agent.name} subagent)`, permission: [ { permission: "todowrite", pattern: "*", action: "deny", }, { permission: "todoread", pattern: "*", action: "deny", }, ...(hasTaskPermission ? [] : [ { permission: "task" as const, pattern: "*" as const, action: "deny" as const, }, ]), ...(config.experimental?.primary_tools?.map((t) => ({ pattern: "*", action: "allow" as const, permission: t, })) ?? []), ], }) }) const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }) if (msg.info.role !== "assistant") throw new Error("Not an assistant message") const model = agent.model ?? { modelID: msg.info.modelID, providerID: msg.info.providerID, } ctx.metadata({ title: params.description, metadata: { sessionId: session.id, model, }, }) const messageID = Identifier.ascending("message") const parts: Record = {} const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => { if (evt.properties.part.sessionID !== session.id) return if (evt.properties.part.messageID === messageID) return if (evt.properties.part.type !== "tool") return const part = evt.properties.part parts[part.id] = { id: part.id, tool: part.tool, state: { status: part.state.status, title: part.state.status === "completed" ? part.state.title : undefined, }, } ctx.metadata({ title: params.description, metadata: { summary: Object.values(parts).sort((a, b) => a.id.localeCompare(b.id)), sessionId: session.id, model, }, }) }) function cancel() { SessionPrompt.cancel(session.id) } ctx.abort.addEventListener("abort", cancel) using _ = defer(() => ctx.abort.removeEventListener("abort", cancel)) const promptParts = await SessionPrompt.resolvePromptParts(params.prompt) const result = await SessionPrompt.prompt({ messageID, sessionID: session.id, model: { modelID: model.modelID, providerID: model.providerID, }, agent: agent.name, tools: { todowrite: false, todoread: false, ...(hasTaskPermission ? {} : { task: false }), ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), }, parts: promptParts, }) unsub() const messages = await Session.messages({ sessionID: session.id }) const summary = messages .filter((x) => x.info.role === "assistant") .flatMap((msg) => msg.parts.filter((x: any) => x.type === "tool") as MessageV2.ToolPart[]) .map((part) => ({ id: part.id, tool: part.tool, state: { status: part.state.status, title: part.state.status === "completed" ? part.state.title : undefined, }, })) const text = result.parts.findLast((x) => x.type === "text")?.text ?? "" const output = text + "\n\n" + ["", `session_id: ${session.id}`, ""].join("\n") return { title: params.description, metadata: { summary, sessionId: session.id, model, }, output, } }, } })