2025-06-12 21:04:34 +00:00
import { Tool } from "./tool"
import DESCRIPTION from "./task.txt"
2025-10-26 19:50:41 +00:00
import z from "zod"
2025-06-12 21:04:34 +00:00
import { Session } from "../session"
2025-07-07 19:53:43 +00:00
import { MessageV2 } from "../session/message-v2"
2025-07-13 21:22:11 +00:00
import { Identifier } from "../id/id"
2025-07-25 01:20:43 +00:00
import { Agent } from "../agent/agent"
2025-09-13 09:46:14 +00:00
import { SessionPrompt } from "../session/prompt"
2026-03-04 05:18:30 +00:00
import { SessionStatus } from "../session/status"
2025-11-19 06:17:26 +00:00
import { iife } from "@/util/iife"
2025-11-17 15:57:18 +00:00
import { defer } from "@/util/defer"
2025-12-03 17:19:43 +00:00
import { Config } from "../config/config"
2026-01-07 04:29:17 +00:00
import { PermissionNext } from "@/permission/next"
2026-01-07 19:28:13 +00:00
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" ) ,
2026-02-05 06:04:03 +00:00
task_id : z
. string ( )
. describe (
"This should only be set if you mean to resume a previous task (you can pass a prior task_id and the task will continue the same subagent session as before instead of creating a fresh one)" ,
)
. optional ( ) ,
2026-01-07 19:28:13 +00:00
command : z.string ( ) . describe ( "The command that triggered this task" ) . optional ( ) ,
2026-02-27 18:12:10 +00:00
background : z
. boolean ( )
. optional ( )
. describe ( "When true, launch the subagent in the background and return immediately" ) ,
2026-01-07 19:28:13 +00:00
} )
2026-01-07 04:29:17 +00:00
2026-02-27 18:12:10 +00:00
function output ( sessionID : string , text : string ) {
return [
` task_id: ${ sessionID } (for resuming to continue this task if needed) ` ,
"" ,
"<task_result>" ,
text ,
"</task_result>" ,
] . join ( "\n" )
}
function backgroundOutput ( sessionID : string ) {
return [
` task_id: ${ sessionID } (for polling this task with task_status) ` ,
"state: running" ,
"" ,
"<task_result>" ,
"Background task started. Continue your current work and call task_status when you need the result." ,
"</task_result>" ,
] . join ( "\n" )
}
function backgroundMessage ( input : {
sessionID : string
description : string
state : "completed" | "error"
text : string
} ) {
const tag = input . state === "completed" ? "task_result" : "task_error"
const title =
input . state === "completed"
? ` Background task completed: ${ input . description } `
: ` Background task failed: ${ input . description } `
return [ title , ` task_id: ${ input . sessionID } ` , ` state: ${ input . state } ` , ` < ${ tag } > ` , input . text , ` </ ${ tag } > ` ] . join ( "\n" )
}
function errorText ( error : unknown ) {
if ( error instanceof Error ) return error . message
return String ( error )
}
2026-03-04 05:18:30 +00:00
function resultTaskID ( input : unknown ) {
if ( ! input || typeof input !== "object" ) return
const taskID = Reflect . get ( input , "task_id" )
if ( typeof taskID === "string" ) return taskID
}
function polled ( input : { message : MessageV2.WithParts ; taskID : string } ) {
if ( input . message . info . role !== "assistant" ) return false
return input . message . parts . some ( ( part ) = > {
if ( part . type !== "tool" ) return false
if ( part . tool !== "task_status" ) return false
if ( part . state . status !== "completed" ) return false
return resultTaskID ( part . state . input ) === input . taskID
} )
}
async function latestUser ( sessionID : string ) {
const [ message ] = await Session . messages ( {
sessionID ,
limit : 1 ,
} )
if ( ! message ) return
if ( message . info . role !== "user" ) return
return message . info . id
}
async function continueParent ( input : { parentID : string ; userID : string ; taskID : string } ) {
const message =
SessionStatus . get ( input . parentID ) . type === "idle"
? undefined
: await SessionPrompt . loop ( {
sessionID : input.parentID ,
} ) . catch ( ( ) = > undefined )
if ( message && polled ( { message , taskID : input.taskID } ) ) return
if ( SessionStatus . get ( input . parentID ) . type !== "idle" ) return
if ( ( await latestUser ( input . parentID ) ) !== input . userID ) return
await SessionPrompt . loop ( {
sessionID : input.parentID ,
} )
}
2026-01-07 19:28:13 +00:00
export const TaskTool = Tool . define ( "task" , async ( ctx ) = > {
2025-08-07 20:32:12 +00:00
const agents = await Agent . list ( ) . then ( ( x ) = > x . filter ( ( a ) = > a . mode !== "primary" ) )
2026-01-07 19:28:13 +00:00
// 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
2025-08-07 20:32:12 +00:00
const description = DESCRIPTION . replace (
"{agents}" ,
2026-01-07 19:28:13 +00:00
accessibleAgents
2025-11-08 01:59:02 +00:00
. map ( ( a ) = > ` - ${ a . name } : ${ a . description ? ? "This subagent should only be called manually by the user." } ` )
2025-08-07 20:32:12 +00:00
. join ( "\n" ) ,
)
2025-07-25 01:20:43 +00:00
return {
description ,
2026-01-07 19:28:13 +00:00
parameters ,
async execute ( params : z.infer < typeof parameters > , ctx ) {
2026-01-01 22:54:11 +00:00
const config = await Config . get ( )
2026-01-07 04:29:17 +00:00
2026-01-07 19:28:13 +00:00
// Skip permission check when user explicitly invoked via @ or command subtask
if ( ! ctx . extra ? . bypassAgentCheck ) {
2026-01-06 23:05:09 +00:00
await ctx . ask ( {
permission : "task" ,
patterns : [ params . subagent_type ] ,
always : [ "*" ] ,
metadata : {
description : params.description ,
subagent_type : params.subagent_type ,
} ,
} )
}
2026-01-01 22:54:11 +00:00
2025-07-25 01:20:43 +00:00
const agent = await Agent . get ( params . subagent_type )
2025-11-08 01:59:02 +00:00
if ( ! agent ) throw new Error ( ` Unknown agent type: ${ params . subagent_type } is not a valid agent type ` )
2026-01-13 02:37:42 +00:00
const hasTaskPermission = agent . permission . some ( ( rule ) = > rule . permission === "task" )
2025-11-19 06:17:26 +00:00
const session = await iife ( async ( ) = > {
2026-02-05 06:04:03 +00:00
if ( params . task_id ) {
const found = await Session . get ( params . task_id ) . catch ( ( ) = > { } )
2025-11-19 06:17:26 +00:00
if ( found ) return found
}
return await Session . create ( {
parentID : ctx.sessionID ,
title : params.description + ` (@ ${ agent . name } subagent) ` ,
2026-01-01 22:54:11 +00:00
permission : [
{
permission : "todowrite" ,
pattern : "*" ,
action : "deny" ,
} ,
{
permission : "todoread" ,
pattern : "*" ,
action : "deny" ,
} ,
2026-01-13 02:37:42 +00:00
. . . ( hasTaskPermission
? [ ]
: [
{
permission : "task" as const ,
pattern : "*" as const ,
action : "deny" as const ,
} ,
] ) ,
2026-01-01 22:54:11 +00:00
. . . ( config . experimental ? . primary_tools ? . map ( ( t ) = > ( {
pattern : "*" ,
action : "allow" as const ,
permission : t ,
} ) ) ? ? [ ] ) ,
] ,
2025-11-19 06:17:26 +00:00
} )
2025-10-06 23:37:30 +00:00
} )
2025-11-07 19:46:58 +00:00
const msg = await MessageV2 . get ( { sessionID : ctx.sessionID , messageID : ctx.messageID } )
2025-08-15 15:16:08 +00:00
if ( msg . info . role !== "assistant" ) throw new Error ( "Not an assistant message" )
2025-10-30 17:15:21 +00:00
2026-02-27 18:12:10 +00:00
const parentModel = {
2026-01-24 05:10:40 +00:00
modelID : msg.info.modelID ,
providerID : msg.info.providerID ,
}
2026-02-27 18:12:10 +00:00
const model = agent . model ? ? parentModel
const background = params . background === true
const metadata = {
sessionId : session.id ,
model ,
. . . ( background ? { background : true } : { } ) ,
}
2026-01-24 05:10:40 +00:00
2025-10-30 17:15:21 +00:00
ctx . metadata ( {
title : params.description ,
2026-02-27 18:12:10 +00:00
metadata ,
2025-10-30 17:15:21 +00:00
} )
2026-02-27 18:12:10 +00:00
const run = async ( ) = > {
const promptParts = await SessionPrompt . resolvePromptParts ( params . prompt )
const result = await SessionPrompt . prompt ( {
messageID : Identifier.ascending ( "message" ) ,
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 ,
} )
return result . parts . findLast ( ( x ) = > x . type === "text" ) ? . text ? ? ""
}
if ( background ) {
const inject = ( state : "completed" | "error" , text : string ) = >
SessionPrompt . prompt ( {
sessionID : ctx.sessionID ,
noReply : true ,
model : {
modelID : parentModel.modelID ,
providerID : parentModel.providerID ,
} ,
agent : ctx.agent ,
parts : [
{
type : "text" ,
synthetic : true ,
text : backgroundMessage ( {
sessionID : session.id ,
description : params.description ,
state ,
text ,
} ) ,
} ,
] ,
} )
void run ( )
2026-03-04 05:18:30 +00:00
. then ( ( text ) = >
inject ( "completed" , text )
. then ( ( message ) = >
continueParent ( {
parentID : ctx.sessionID ,
userID : message.info.id ,
taskID : session.id ,
} ) ,
)
. catch ( ( ) = > { } ) ,
)
. catch ( ( error ) = >
inject ( "error" , errorText ( error ) )
. then ( ( message ) = >
continueParent ( {
parentID : ctx.sessionID ,
userID : message.info.id ,
taskID : session.id ,
} ) ,
)
. catch ( ( ) = > { } ) ,
)
2026-02-27 18:12:10 +00:00
return {
title : params.description ,
metadata ,
output : backgroundOutput ( session . id ) ,
}
}
2025-07-25 01:20:43 +00:00
2025-11-17 15:57:18 +00:00
function cancel() {
SessionPrompt . cancel ( session . id )
}
ctx . abort . addEventListener ( "abort" , cancel )
using _ = defer ( ( ) = > ctx . abort . removeEventListener ( "abort" , cancel ) )
2026-02-27 18:12:10 +00:00
const text = await run ( )
2025-11-19 06:17:26 +00:00
2025-07-25 01:20:43 +00:00
return {
2025-06-18 17:40:21 +00:00
title : params.description ,
2026-02-27 18:12:10 +00:00
metadata ,
output : output ( session . id , text ) ,
2025-07-25 01:20:43 +00:00
}
} ,
}
2025-06-12 21:04:34 +00:00
} )