| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- 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<typeof parameters>, 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<string, { id: string; tool: string; state: { status: string; title?: string } }> = {}
- 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" + ["<task_metadata>", `session_id: ${session.id}`, "</task_metadata>"].join("\n")
- return {
- title: params.description,
- metadata: {
- summary,
- sessionId: session.id,
- model,
- },
- output,
- }
- },
- }
- })
|