@@ -10,7 +10,7 @@ import { Agent } from "../agent/agent"
1010import { deriveSubagentSessionPermission } from "../agent/subagent-permissions"
1111import type { SessionPrompt } from "../session/prompt"
1212import { Config } from "@/config/config"
13- import { Effect , Exit , Schema , Scope } from "effect"
13+ import { Effect , Exit , Schema , Scope , Semaphore } from "effect"
1414import { EffectBridge } from "@/effect/bridge"
1515import { RuntimeFlags } from "@/effect/runtime-flags"
1616import { Database } from "@opencode-ai/core/database/database"
@@ -21,6 +21,19 @@ export interface TaskPromptOps {
2121 prompt ( input : SessionPrompt . PromptInput ) : Effect . Effect < SessionV1 . WithParts >
2222}
2323
24+ function isTaskPromptOps ( value : unknown ) : value is TaskPromptOps {
25+ return (
26+ typeof value === "object" &&
27+ value !== null &&
28+ "cancel" in value &&
29+ typeof value . cancel === "function" &&
30+ "resolvePromptParts" in value &&
31+ typeof value . resolvePromptParts === "function" &&
32+ "prompt" in value &&
33+ typeof value . prompt === "function"
34+ )
35+ }
36+
2437const id = "task"
2538const BACKGROUND_DESCRIPTION = [
2639 "Background mode: background=true launches the subagent asynchronously and returns immediately." ,
@@ -88,6 +101,16 @@ export const TaskTool = Tool.define(
88101 const scope = yield * Scope . Scope
89102 const flags = yield * RuntimeFlags . Service
90103 const database = yield * Database . Service
104+ const limits = new Map < number , Semaphore . Semaphore > ( )
105+
106+ function limit ( concurrency : number | undefined ) {
107+ if ( concurrency === undefined ) return undefined
108+ const current = limits . get ( concurrency )
109+ if ( current ) return current
110+ const created = Semaphore . makeUnsafe ( concurrency )
111+ limits . set ( concurrency , created )
112+ return created
113+ }
91114
92115 const run = Effect . fn ( "TaskTool.execute" ) ( function * (
93116 params : Schema . Schema . Type < typeof Parameters > ,
@@ -194,12 +217,14 @@ export const TaskTool = Tool.define(
194217 metadata,
195218 } )
196219
197- const ops = ctx . extra ?. promptOps as TaskPromptOps
198- if ( ! ops ) return yield * Effect . fail ( new Error ( "TaskTool requires promptOps in ctx.extra" ) )
220+ const promptOps = ctx . extra ?. promptOps
221+ if ( ! isTaskPromptOps ( promptOps ) ) {
222+ return yield * Effect . fail ( new Error ( "TaskTool requires valid promptOps in ctx.extra" ) )
223+ }
199224
200- const runTask = Effect . fn ( "TaskTool.runTask" ) ( function * ( ) {
201- const parts = yield * ops . resolvePromptParts ( params . prompt )
202- const result = yield * ops . prompt ( {
225+ const executeTask = Effect . fn ( "TaskTool.runTask" ) ( function * ( ) {
226+ const parts = yield * promptOps . resolvePromptParts ( params . prompt )
227+ const result = yield * promptOps . prompt ( {
203228 messageID : MessageID . ascending ( ) ,
204229 sessionID : nextSession . id ,
205230 model : {
@@ -223,13 +248,15 @@ export const TaskTool = Tool.define(
223248 }
224249 return result . parts . findLast ( ( item ) => item . type === "text" ) ?. text ?? ""
225250 } )
251+ const semaphore = limit ( cfg . subagent_concurrency )
252+ const runTask = ( ) => ( semaphore ? semaphore . withPermit ( executeTask ( ) ) : executeTask ( ) )
226253
227254 const inject = Effect . fn ( "TaskTool.injectBackgroundResult" ) ( function * (
228255 state : "completed" | "error" ,
229256 text : string ,
230257 ) {
231258 const currentParent = yield * sessions . get ( ctx . sessionID )
232- yield * ops
259+ yield * promptOps
233260 . prompt ( {
234261 sessionID : ctx . sessionID ,
235262 agent : currentParent . agent ?? ctx . agent ,
@@ -286,14 +313,14 @@ export const TaskTool = Tool.define(
286313 type : id ,
287314 title : params . description ,
288315 metadata,
289- onPromote : Effect . all ( [
290- ctx . metadata ( {
316+ onPromote : Effect . gen ( function * ( ) {
317+ yield * ctx . metadata ( {
291318 title : params . description ,
292319 metadata : { ...metadata , background : true , jobId : nextSession . id } ,
293- } ) ,
294- notify ( nextSession . id ) ,
295- ] ) ,
296- run : runTask ( ) . pipe ( Effect . onInterrupt ( ( ) => ops . cancel ( nextSession . id ) ) ) ,
320+ } )
321+ yield * notify ( nextSession . id )
322+ } ) ,
323+ run : runTask ( ) . pipe ( Effect . onInterrupt ( ( ) => promptOps . cancel ( nextSession . id ) ) ) ,
297324 } )
298325
299326 function backgroundResult ( ) {
@@ -319,7 +346,7 @@ export const TaskTool = Tool.define(
319346 }
320347
321348 const runCancel = yield * EffectBridge . make ( )
322- const cancel = ops . cancel ( nextSession . id )
349+ const cancel = promptOps . cancel ( nextSession . id )
323350
324351 function onAbort ( ) {
325352 runCancel . fork ( cancel )
0 commit comments