@@ -5,11 +5,9 @@ import { Effect } from "effect"
55import { Session } from "../session"
66import { SessionID , MessageID } from "../session/schema"
77import { MessageV2 } from "../session/message-v2"
8- import { Identifier } from "../id/id"
98import { Agent } from "../agent/agent"
109import { SessionPrompt } from "../session/prompt"
1110import { iife } from "@/util/iife"
12- import { defer } from "@/util/defer"
1311import { Config } from "../config/config"
1412import { Permission } from "@/permission"
1513
@@ -32,148 +30,169 @@ export const TaskTool = Tool.defineEffect(
3230 const agent = yield * Agent . Service
3331 const config = yield * Config . Service
3432
35- return async ( ctx ) => {
36- const agents = await agent . list ( ) . pipe (
37- Effect . map ( ( x ) => x . filter ( ( a ) => a . mode !== "primary" ) ) ,
38- Effect . runPromise ,
39- )
40-
41- const caller = ctx ?. agent
42- const accessibleAgents = caller
43- ? agents . filter ( ( a ) => Permission . evaluate ( "task" , a . name , caller . permission ) . action !== "deny" )
44- : agents
45- const list = accessibleAgents . toSorted ( ( a , b ) => a . name . localeCompare ( b . name ) )
46-
47- const description = DESCRIPTION . replace (
33+ const list = Effect . fn ( "TaskTool.list" ) ( function * ( caller ?: Tool . InitContext [ "agent" ] ) {
34+ const items = yield * agent . list ( ) . pipe ( Effect . map ( ( items ) => items . filter ( ( item ) => item . mode !== "primary" ) ) )
35+ const filtered = caller
36+ ? items . filter ( ( item ) => Permission . evaluate ( "task" , item . name , caller . permission ) . action !== "deny" )
37+ : items
38+ return filtered . toSorted ( ( a , b ) => a . name . localeCompare ( b . name ) )
39+ } )
40+
41+ const desc = Effect . fn ( "TaskTool.desc" ) ( function * ( caller ?: Tool . InitContext [ "agent" ] ) {
42+ const items = yield * list ( caller )
43+ return DESCRIPTION . replace (
4844 "{agents}" ,
49- list
50- . map ( ( a ) => `- ${ a . name } : ${ a . description ?? "This subagent should only be called manually by the user." } ` )
45+ items
46+ . map (
47+ ( item ) =>
48+ `- ${ item . name } : ${ item . description ?? "This subagent should only be called manually by the user." } ` ,
49+ )
5150 . join ( "\n" ) ,
5251 )
52+ } )
5353
54- return {
55- description,
56- parameters,
57- async execute ( params : z . infer < typeof parameters > , ctx ) {
58- const cfg = await config . get ( ) . pipe ( Effect . runPromise )
59-
60- // Skip permission check when user explicitly invoked via @ or command subtask
61- if ( ! ctx . extra ?. bypassAgentCheck ) {
62- await ctx . ask ( {
63- permission : "task" ,
64- patterns : [ params . subagent_type ] ,
65- always : [ "*" ] ,
66- metadata : {
67- description : params . description ,
68- subagent_type : params . subagent_type ,
69- } ,
70- } )
71- }
54+ const run = Effect . fn ( "TaskTool.execute" ) ( function * ( params : z . infer < typeof parameters > , ctx : Tool . Context ) {
55+ const cfg = yield * config . get ( )
7256
73- const next = await agent
74- . get ( params . subagent_type )
75- . pipe ( Effect . runPromise )
76- . catch ( ( ) => undefined )
77- if ( ! next ) throw new Error ( `Unknown agent type: ${ params . subagent_type } is not a valid agent type` )
78-
79- const hasTaskPermission = next . permission . some ( ( rule ) => rule . permission === "task" )
80- const hasTodoWritePermission = next . permission . some ( ( rule ) => rule . permission === "todowrite" )
81-
82- const session = await iife ( async ( ) => {
83- if ( params . task_id ) {
84- const found = await Session . get ( SessionID . make ( params . task_id ) ) . catch ( ( ) => { } )
85- if ( found ) return found
86- }
87-
88- return await Session . create ( {
89- parentID : ctx . sessionID ,
90- title : params . description + ` (@${ next . name } subagent)` ,
91- permission : [
92- ...( hasTodoWritePermission
93- ? [ ]
94- : [
95- {
96- permission : "todowrite" as const ,
97- pattern : "*" as const ,
98- action : "deny" as const ,
99- } ,
100- ] ) ,
101- ...( hasTaskPermission
102- ? [ ]
103- : [
104- {
105- permission : "task" as const ,
106- pattern : "*" as const ,
107- action : "deny" as const ,
108- } ,
109- ] ) ,
110- ...( cfg . experimental ?. primary_tools ?. map ( ( t ) => ( {
111- pattern : "*" ,
112- action : "allow" as const ,
113- permission : t ,
114- } ) ) ?? [ ] ) ,
115- ] ,
116- } )
117- } )
118- const msg = await MessageV2 . get ( { sessionID : ctx . sessionID , messageID : ctx . messageID } )
119- if ( msg . info . role !== "assistant" ) throw new Error ( "Not an assistant message" )
120-
121- const model = next . model ?? {
122- modelID : msg . info . modelID ,
123- providerID : msg . info . providerID ,
124- }
125-
126- ctx . metadata ( {
127- title : params . description ,
57+ if ( ! ctx . extra ?. bypassAgentCheck ) {
58+ yield * Effect . promise ( ( ) =>
59+ ctx . ask ( {
60+ permission : "task" ,
61+ patterns : [ params . subagent_type ] ,
62+ always : [ "*" ] ,
12863 metadata : {
129- sessionId : session . id ,
130- model ,
64+ description : params . description ,
65+ subagent_type : params . subagent_type ,
13166 } ,
132- } )
67+ } ) ,
68+ )
69+ }
70+
71+ const next = yield * agent . get ( params . subagent_type ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( undefined ) ) )
72+ if ( ! next ) {
73+ return yield * Effect . fail ( new Error ( `Unknown agent type: ${ params . subagent_type } is not a valid agent type` ) )
74+ }
13375
134- const messageID = MessageID . ascending ( )
76+ const hasTask = next . permission . some ( ( rule ) => rule . permission === "task" )
77+ const hasTodo = next . permission . some ( ( rule ) => rule . permission === "todowrite" )
13578
136- function cancel ( ) {
137- SessionPrompt . cancel ( session . id )
79+ const session = yield * Effect . promise ( ( ) =>
80+ iife ( async ( ) => {
81+ if ( params . task_id ) {
82+ const found = await Session . get ( SessionID . make ( params . task_id ) ) . catch ( ( ) => { } )
83+ if ( found ) return found
13884 }
139- ctx . abort . addEventListener ( "abort" , cancel )
140- using _ = defer ( ( ) => ctx . abort . removeEventListener ( "abort" , cancel ) )
141- const promptParts = await SessionPrompt . resolvePromptParts ( params . prompt )
142-
143- const result = await SessionPrompt . prompt ( {
144- messageID,
145- sessionID : session . id ,
146- model : {
147- modelID : model . modelID ,
148- providerID : model . providerID ,
149- } ,
150- agent : next . name ,
151- tools : {
152- ...( hasTodoWritePermission ? { } : { todowrite : false } ) ,
153- ...( hasTaskPermission ? { } : { task : false } ) ,
154- ...Object . fromEntries ( ( cfg . experimental ?. primary_tools ?? [ ] ) . map ( ( t ) => [ t , false ] ) ) ,
155- } ,
156- parts : promptParts ,
85+
86+ return Session . create ( {
87+ parentID : ctx . sessionID ,
88+ title : params . description + ` (@${ next . name } subagent)` ,
89+ permission : [
90+ ...( hasTodo
91+ ? [ ]
92+ : [
93+ {
94+ permission : "todowrite" as const ,
95+ pattern : "*" as const ,
96+ action : "deny" as const ,
97+ } ,
98+ ] ) ,
99+ ...( hasTask
100+ ? [ ]
101+ : [
102+ {
103+ permission : "task" as const ,
104+ pattern : "*" as const ,
105+ action : "deny" as const ,
106+ } ,
107+ ] ) ,
108+ ...( cfg . experimental ?. primary_tools ?. map ( ( item ) => ( {
109+ pattern : "*" ,
110+ action : "allow" as const ,
111+ permission : item ,
112+ } ) ) ?? [ ] ) ,
113+ ] ,
157114 } )
115+ } ) ,
116+ )
158117
159- const text = result . parts . findLast ( ( x ) => x . type === "text" ) ?. text ?? ""
118+ const msg = yield * Effect . sync ( ( ) => MessageV2 . get ( { sessionID : ctx . sessionID , messageID : ctx . messageID } ) )
119+ if ( msg . info . role !== "assistant" ) return yield * Effect . fail ( new Error ( "Not an assistant message" ) )
160120
161- const output = [
162- `task_id: ${ session . id } (for resuming to continue this task if needed)` ,
163- "" ,
164- "<task_result>" ,
165- text ,
166- "</task_result>" ,
167- ] . join ( "\n" )
121+ const model = next . model ?? {
122+ modelID : msg . info . modelID ,
123+ providerID : msg . info . providerID ,
124+ }
168125
126+ ctx . metadata ( {
127+ title : params . description ,
128+ metadata : {
129+ sessionId : session . id ,
130+ model,
131+ } ,
132+ } )
133+
134+ const messageID = MessageID . ascending ( )
135+
136+ function cancel ( ) {
137+ SessionPrompt . cancel ( session . id )
138+ }
139+ return yield * Effect . acquireUseRelease (
140+ Effect . sync ( ( ) => {
141+ ctx . abort . addEventListener ( "abort" , cancel )
142+ } ) ,
143+ ( ) => Effect . promise ( ( ) => SessionPrompt . resolvePromptParts ( params . prompt ) ) ,
144+ ( ) =>
145+ Effect . sync ( ( ) => {
146+ ctx . abort . removeEventListener ( "abort" , cancel )
147+ } ) ,
148+ ) . pipe (
149+ Effect . flatMap ( ( parts ) =>
150+ Effect . promise ( ( ) =>
151+ SessionPrompt . prompt ( {
152+ messageID,
153+ sessionID : session . id ,
154+ model : {
155+ modelID : model . modelID ,
156+ providerID : model . providerID ,
157+ } ,
158+ agent : next . name ,
159+ tools : {
160+ ...( hasTodo ? { } : { todowrite : false } ) ,
161+ ...( hasTask ? { } : { task : false } ) ,
162+ ...Object . fromEntries ( ( cfg . experimental ?. primary_tools ?? [ ] ) . map ( ( item ) => [ item , false ] ) ) ,
163+ } ,
164+ parts,
165+ } ) ,
166+ ) ,
167+ ) ,
168+ Effect . map ( ( result ) => {
169+ const text = result . parts . findLast ( ( item ) => item . type === "text" ) ?. text ?? ""
169170 return {
170171 title : params . description ,
171172 metadata : {
172173 sessionId : session . id ,
173174 model,
174175 } ,
175- output,
176+ output : [
177+ `task_id: ${ session . id } (for resuming to continue this task if needed)` ,
178+ "" ,
179+ "<task_result>" ,
180+ text ,
181+ "</task_result>" ,
182+ ] . join ( "\n" ) ,
176183 }
184+ } ) ,
185+ )
186+ } )
187+
188+ return async ( ctx ) => {
189+ const description = await Effect . runPromise ( desc ( ctx ?. agent ) )
190+
191+ return {
192+ description,
193+ parameters,
194+ async execute ( params : z . infer < typeof parameters > , ctx ) {
195+ return Effect . runPromise ( run ( params , ctx ) )
177196 } ,
178197 }
179198 }
0 commit comments