2026-04-16 03:56:54 +00:00
import * as Tool from "./tool"
2025-06-12 21:04:34 +00:00
import DESCRIPTION from "./task.txt"
2026-05-14 16:40:15 +00:00
import { ToolJsonSchema } from "./json-schema"
2026-05-31 01:08:38 +00:00
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
2026-05-14 16:40:15 +00:00
import { BackgroundJob } from "@/background/job"
2026-04-27 18:33:33 +00:00
import { Session } from "@/session/session"
2026-03-11 23:30:17 +00:00
import { SessionID , MessageID } from "../session/schema"
2025-07-07 19:53:43 +00:00
import { MessageV2 } from "../session/message-v2"
2025-07-25 01:20:43 +00:00
import { Agent } from "../agent/agent"
2026-05-09 23:51:55 +00:00
import { deriveSubagentSessionPermission } from "../agent/subagent-permissions"
2026-04-10 23:36:13 +00:00
import type { SessionPrompt } from "../session/prompt"
2026-04-27 18:33:33 +00:00
import { Config } from "@/config/config"
2026-05-25 21:09:56 +00:00
import { Cause , Effect , Exit , Schema , Scope } from "effect"
2026-05-05 02:36:06 +00:00
import { EffectBridge } from "@/effect/bridge"
2026-05-14 16:40:15 +00:00
import { RuntimeFlags } from "@/effect/runtime-flags"
2026-05-31 01:08:38 +00:00
import { Database } from "@opencode-ai/core/database/database"
2026-01-07 04:29:17 +00:00
2026-04-10 23:36:13 +00:00
export interface TaskPromptOps {
2026-05-05 02:36:06 +00:00
cancel ( sessionID : SessionID ) : Effect . Effect < void >
2026-04-11 02:57:47 +00:00
resolvePromptParts ( template : string ) : Effect . Effect < SessionPrompt.PromptInput [ " parts " ] >
2026-05-31 01:08:38 +00:00
prompt ( input : SessionPrompt.PromptInput ) : Effect . Effect < SessionLegacy.WithParts >
2026-04-10 23:36:13 +00:00
}
2026-04-08 23:02:19 +00:00
const id = "task"
2026-05-16 13:48:15 +00:00
const BACKGROUND_DESCRIPTION = [
"" ,
"" ,
[
2026-05-25 21:09:56 +00:00
"Background mode: background=true launches the subagent asynchronously and returns immediately." ,
"Foreground is the default; use it when you need the result before continuing." ,
"Use background only for independent work that can run while you continue elsewhere." ,
"You will be notified automatically when it finishes." ,
2026-05-16 13:48:15 +00:00
] . join ( " " ) ,
] . join ( "\n" )
2026-04-08 23:02:19 +00:00
2026-05-25 21:09:56 +00:00
const BaseParameterFields = {
2026-05-14 16:40:15 +00:00
description : Schema.String.annotate ( { description : "A short (3-5 words) description of the task" } ) ,
prompt : Schema.String.annotate ( { description : "The task for the agent to perform" } ) ,
subagent_type : Schema.String.annotate ( { description : "The type of specialized agent to use for this task" } ) ,
task_id : Schema.optional ( Schema . String ) . annotate ( {
description :
"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)" ,
} ) ,
command : Schema.optional ( Schema . String ) . annotate ( { description : "The command that triggered this task" } ) ,
2026-05-25 21:09:56 +00:00
}
const BaseParameters = Schema . Struct ( BaseParameterFields )
2026-05-14 16:40:15 +00:00
2026-04-23 20:09:34 +00:00
export const Parameters = Schema . Struct ( {
2026-05-25 21:09:56 +00:00
. . . BaseParameterFields ,
2026-05-14 16:40:15 +00:00
background : Schema.optional ( Schema . Boolean ) . annotate ( {
2026-05-25 21:09:56 +00:00
description : "Run the agent in the background. You will be notified when it completes." ,
2026-05-14 16:40:15 +00:00
} ) ,
2026-04-08 23:02:19 +00:00
} )
2026-05-14 16:40:15 +00:00
function output ( sessionID : SessionID , text : string ) {
2026-05-25 21:11:17 +00:00
return [ ` <task id=" ${ sessionID } " state="completed"> ` , "<task_result>" , text , "</task_result>" , "</task>" ] . join ( "\n" )
2026-05-14 16:40:15 +00:00
}
function backgroundOutput ( sessionID : SessionID ) {
return [
2026-05-25 21:09:56 +00:00
` <task id=" ${ sessionID } " state="running"> ` ,
"<summary>Background task started</summary>" ,
2026-05-14 16:40:15 +00:00
"<task_result>" ,
2026-05-25 21:09:56 +00:00
"Background task started. You will be notified automatically when it finishes; do not poll for progress." ,
"Do not duplicate its work. Continue only with non-overlapping work, or stop if there is nothing else useful to do." ,
2026-05-14 16:40:15 +00:00
"</task_result>" ,
2026-05-25 21:09:56 +00:00
"</task>" ,
2026-05-14 16:40:15 +00:00
] . join ( "\n" )
}
2026-05-14 16:42:18 +00:00
function backgroundMessage ( input : {
sessionID : SessionID
description : string
state : "completed" | "error"
text : string
} ) {
2026-05-14 16:40:15 +00:00
const tag = input . state === "completed" ? "task_result" : "task_error"
const title =
input . state === "completed"
? ` Background task completed: ${ input . description } `
: ` Background task failed: ${ input . description } `
2026-05-25 21:09:56 +00:00
return [
` <task id=" ${ input . sessionID } " state=" ${ input . state } "> ` ,
` <summary> ${ title } </summary> ` ,
` < ${ tag } > ` ,
input . text ,
` </ ${ tag } > ` ,
"</task>" ,
] . join ( "\n" )
2026-05-14 16:40:15 +00:00
}
function errorText ( error : unknown ) {
if ( error instanceof Error ) return error . message
return String ( error )
}
2026-04-11 02:36:02 +00:00
export const TaskTool = Tool . define (
2026-04-08 23:02:19 +00:00
id ,
Effect . gen ( function * ( ) {
const agent = yield * Agent . Service
2026-05-14 16:40:15 +00:00
const background = yield * BackgroundJob . Service
2026-04-08 23:02:19 +00:00
const config = yield * Config . Service
2026-04-11 03:18:30 +00:00
const sessions = yield * Session . Service
2026-05-14 16:40:15 +00:00
const scope = yield * Scope . Scope
const flags = yield * RuntimeFlags . Service
2026-05-31 01:08:38 +00:00
const database = yield * Database . Service
2026-04-08 23:02:19 +00:00
2026-04-23 20:10:56 +00:00
const run = Effect . fn ( "TaskTool.execute" ) ( function * (
params : Schema.Schema.Type < typeof Parameters > ,
ctx : Tool.Context ,
) {
2026-04-08 23:02:19 +00:00
const cfg = yield * config . get ( )
2026-05-14 16:40:15 +00:00
const runInBackground = params . background === true
if ( runInBackground && ! flags . experimentalBackgroundSubagents ) {
2026-05-14 16:42:18 +00:00
return yield * Effect . fail (
new Error ( "Background subagents require OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true" ) ,
)
2026-05-14 16:40:15 +00:00
}
2026-01-07 04:29:17 +00:00
2026-01-07 19:28:13 +00:00
if ( ! ctx . extra ? . bypassAgentCheck ) {
2026-04-11 02:36:02 +00:00
yield * ctx . ask ( {
permission : id ,
patterns : [ params . subagent_type ] ,
always : [ "*" ] ,
metadata : {
description : params.description ,
subagent_type : params.subagent_type ,
} ,
} )
2026-01-06 23:05:09 +00:00
}
2026-01-01 22:54:11 +00:00
2026-04-08 23:02:19 +00:00
const next = yield * agent . get ( params . subagent_type )
if ( ! next ) {
return yield * Effect . fail ( new Error ( ` Unknown agent type: ${ params . subagent_type } is not a valid agent type ` ) )
}
2025-10-30 17:15:21 +00:00
2026-05-25 21:09:56 +00:00
const session = params . task_id
? yield * sessions . get ( SessionID . make ( params . task_id ) ) . pipe ( Effect . catchCause ( ( ) = > Effect . succeed ( undefined ) ) )
2026-04-08 23:02:19 +00:00
: undefined
2026-04-30 03:06:29 +00:00
const parent = yield * sessions . get ( ctx . sessionID )
2026-05-09 23:51:55 +00:00
const parentAgent = parent . agent
? yield * agent . get ( parent . agent ) . pipe ( Effect . catchCause ( ( ) = > Effect . succeed ( undefined ) ) )
: undefined
2026-04-08 23:02:19 +00:00
const nextSession =
session ? ?
2026-04-11 03:18:30 +00:00
( yield * sessions . create ( {
parentID : ctx.sessionID ,
title : params.description + ` (@ ${ next . name } subagent) ` ,
permission : [
2026-05-09 23:51:55 +00:00
. . . deriveSubagentSessionPermission ( {
parentSessionPermission : parent.permission ? ? [ ] ,
parentAgent ,
subagent : next ,
} ) ,
2026-04-11 03:18:30 +00:00
. . . ( cfg . experimental ? . primary_tools ? . map ( ( item ) = > ( {
pattern : "*" ,
action : "allow" as const ,
permission : item ,
} ) ) ? ? [ ] ) ,
] ,
} ) )
2026-04-08 23:02:19 +00:00
2026-05-31 01:08:38 +00:00
const msg = yield * MessageV2 . get ( { sessionID : ctx.sessionID , messageID : ctx.messageID } ) . pipe (
Effect . provideService ( Database . Service , database ) ,
Effect . orDie ,
)
2026-04-08 23:02:19 +00:00
if ( msg . info . role !== "assistant" ) return yield * Effect . fail ( new Error ( "Not an assistant message" ) )
const model = next . model ? ? {
2026-01-24 05:10:40 +00:00
modelID : msg.info.modelID ,
providerID : msg.info.providerID ,
}
2026-05-14 16:40:15 +00:00
const metadata = {
parentSessionId : ctx.sessionID ,
sessionId : nextSession.id ,
model ,
. . . ( runInBackground ? { background : true } : { } ) ,
}
2026-01-24 05:10:40 +00:00
2026-04-11 03:12:04 +00:00
yield * ctx . metadata ( {
2025-10-30 17:15:21 +00:00
title : params.description ,
2026-05-14 16:40:15 +00:00
metadata ,
2025-10-30 17:15:21 +00:00
} )
2026-04-10 23:36:13 +00:00
const ops = ctx . extra ? . promptOps as TaskPromptOps
if ( ! ops ) return yield * Effect . fail ( new Error ( "TaskTool requires promptOps in ctx.extra" ) )
2026-05-14 16:40:15 +00:00
const runTask = Effect . fn ( "TaskTool.runTask" ) ( function * ( ) {
const parts = yield * ops . resolvePromptParts ( params . prompt )
const result = yield * ops . prompt ( {
messageID : MessageID.ascending ( ) ,
sessionID : nextSession.id ,
model : {
modelID : model.modelID ,
providerID : model.providerID ,
} ,
agent : next.name ,
tools : {
. . . ( next . permission . some ( ( rule ) = > rule . permission === "todowrite" ) ? { } : { todowrite : false } ) ,
. . . ( next . permission . some ( ( rule ) = > rule . permission === id ) ? { } : { task : false } ) ,
. . . Object . fromEntries ( ( cfg . experimental ? . primary_tools ? ? [ ] ) . map ( ( item ) = > [ item , false ] ) ) ,
} ,
parts ,
} )
return result . parts . findLast ( ( item ) = > item . type === "text" ) ? . text ? ? ""
} )
2026-05-14 16:42:18 +00:00
const inject = Effect . fn ( "TaskTool.injectBackgroundResult" ) ( function * (
state : "completed" | "error" ,
text : string ,
) {
2026-05-14 16:40:15 +00:00
const currentParent = yield * sessions . get ( ctx . sessionID )
2026-05-25 21:09:56 +00:00
yield * ops
. prompt ( {
sessionID : ctx.sessionID ,
agent : currentParent.agent ? ? ctx . agent ,
parts : [
{
type : "text" ,
synthetic : true ,
text : backgroundMessage ( {
sessionID : nextSession.id ,
description : params.description ,
state ,
text ,
} ) ,
} ,
] ,
} )
. pipe ( Effect . ignore , Effect . forkIn ( scope , { startImmediately : true } ) )
2026-05-14 16:40:15 +00:00
} )
const existing = yield * background . get ( nextSession . id )
if ( existing ? . status === "running" ) {
2026-05-25 21:09:56 +00:00
return yield * Effect . fail ( new Error ( ` Task ${ nextSession . id } is already running. ` ) )
2026-05-14 16:40:15 +00:00
}
if ( runInBackground ) {
const info = yield * background . start ( {
id : nextSession.id ,
type : id ,
title : params.description ,
metadata ,
run : runTask ( ) . pipe (
Effect . tap ( ( text ) = > inject ( "completed" , text ) . pipe ( Effect . ignore ) ) ,
Effect . catchCause ( ( cause ) = >
( Cause . hasInterruptsOnly ( cause )
? Effect . void
: inject ( "error" , errorText ( Cause . squash ( cause ) ) ) . pipe ( Effect . ignore )
) . pipe ( Effect . andThen ( Effect . failCause ( cause ) ) ) ,
) ,
) ,
} )
return {
title : params.description ,
metadata : {
. . . metadata ,
jobId : info.id ,
} ,
output : backgroundOutput ( nextSession . id ) ,
}
}
2026-05-25 21:09:56 +00:00
const runCancel = yield * EffectBridge . make ( )
2026-05-05 02:36:06 +00:00
const cancel = ops . cancel ( nextSession . id )
2025-07-25 01:20:43 +00:00
2026-05-05 02:36:06 +00:00
function onAbort() {
runCancel . fork ( cancel )
2025-11-17 15:57:18 +00:00
}
2025-11-19 06:17:26 +00:00
2026-04-08 23:02:19 +00:00
return yield * Effect . acquireUseRelease (
Effect . sync ( ( ) = > {
2026-05-05 02:36:06 +00:00
ctx . abort . addEventListener ( "abort" , onAbort )
2026-04-08 23:02:19 +00:00
} ) ,
( ) = >
Effect . gen ( function * ( ) {
2026-05-14 16:40:15 +00:00
const text = yield * runTask ( )
2026-04-08 23:02:19 +00:00
return {
title : params.description ,
2026-05-14 16:40:15 +00:00
metadata ,
output : output ( nextSession . id , text ) ,
2026-04-08 23:02:19 +00:00
}
} ) ,
2026-05-05 02:36:06 +00:00
( _ , exit ) = >
Effect . gen ( function * ( ) {
if ( Exit . hasInterrupts ( exit ) ) yield * cancel
} ) . pipe (
Effect . ensuring (
Effect . sync ( ( ) = > {
ctx . abort . removeEventListener ( "abort" , onAbort )
} ) ,
) ,
) ,
2026-04-08 23:02:19 +00:00
)
} )
return {
2026-05-16 13:48:15 +00:00
description : flags.experimentalBackgroundSubagents ? DESCRIPTION + BACKGROUND_DESCRIPTION : DESCRIPTION ,
2026-04-23 20:09:34 +00:00
parameters : Parameters ,
2026-05-14 16:40:15 +00:00
jsonSchema : flags.experimentalBackgroundSubagents ? undefined : ToolJsonSchema . fromSchema ( BaseParameters ) ,
2026-04-23 20:10:56 +00:00
execute : ( params : Schema.Schema.Type < typeof Parameters > , ctx : Tool.Context ) = >
run ( params , ctx ) . pipe ( Effect . orDie ) ,
2026-04-08 23:02:19 +00:00
}
} ) ,
)