@@ -19,7 +19,7 @@ export const MAX_TIMEOUT_MS = 10 * 60 * 1_000
1919export const MAX_CAPTURE_BYTES = 1024 * 1024
2020
2121const BACKGROUND_STARTED =
22- "The command is running in the background. You will be notified automatically when it completes. DO NOT sleep, poll, or proactively check on its progress ."
22+ "The command has not completed; it is now running in the background ."
2323
2424export const Input = Schema . Struct ( {
2525 command : Schema . String . annotate ( { description : "Shell command string to execute" } ) ,
@@ -185,75 +185,80 @@ export const Plugin = {
185185 return yield * Effect . fail ( new Error ( `Working directory is not a directory: ${ target . canonical } ` ) )
186186
187187 const timeout = input . timeout ?? DEFAULT_TIMEOUT_MS
188+ const info = yield * shell . create ( {
189+ command : input . command ,
190+ cwd : target . canonical ,
191+ timeout,
192+ metadata : { sessionID : context . sessionID } ,
193+ } )
188194
189- if ( input . background === true ) {
190- const background = yield * shell . create ( {
191- command : input . command ,
192- cwd : target . canonical ,
193- timeout,
194- metadata : { sessionID : context . sessionID } ,
195- } )
196- const run = Effect . fn ( "ShellTool.run" ) ( function * ( ) {
197- return yield * Effect . gen ( function * ( ) {
198- const final = yield * shell . wait ( background . id )
199- const page = yield * shell . output ( background . id , { limit : MAX_CAPTURE_BYTES } )
195+ const settleShell = Effect . fn ( "ShellTool.settleShell" ) ( function * ( ) {
196+ const final = yield * shell . wait ( info . id )
197+ const page = yield * shell . output ( info . id , { limit : MAX_CAPTURE_BYTES } )
200198
201- if ( final . status === "timeout" )
202- return `Command exceeded timeout of ${ timeout } ms. Retry with a larger timeout if the command is expected to take longer.`
199+ if ( final . status === "timeout" ) {
200+ return {
201+ exit : final . exit ,
202+ output : `Command exceeded timeout of ${ timeout } ms. Retry with a larger timeout if the command is expected to take longer.` ,
203+ truncated : false ,
204+ timeout : true ,
205+ status : "completed" as const ,
206+ }
207+ }
203208
204- const truncated = page . size > page . cursor
205- const body = page . output || "(no output)"
206- const notice = truncated ? `\n\n[output truncated; full output saved to: ${ final . file } ]` : ""
207- return `${ body } ${ notice } `
208- } ) . pipe ( Effect . onInterrupt ( ( ) => shell . remove ( background . id ) . pipe ( Effect . ignore ) ) )
209- } )
209+ const truncated = page . size > page . cursor
210+ const body = page . output || "(no output)"
211+ const notice = truncated ? `\n\n[output truncated; full output saved to: ${ final . file } ]` : ""
212+ return {
213+ exit : final . exit ,
214+ output : `${ body } ${ notice } ` ,
215+ truncated,
216+ status : "completed" as const ,
217+ }
218+ } )
210219
211- const info = yield * runtime . job . start ( {
212- id : context . toolCallID ,
213- type : name ,
214- title : input . command ,
215- metadata : { sessionID : context . sessionID } ,
216- run : run ( ) ,
217- } )
218- yield * runtime . job . background ( info . id )
220+ const run = settleShell ( ) . pipe (
221+ Effect . map ( ( output ) => output . output ) ,
222+ Effect . onInterrupt ( ( ) => shell . remove ( info . id ) . pipe ( Effect . ignore ) ) ,
223+ )
224+ const job = yield * runtime . job . start ( {
225+ id : context . toolCallID ,
226+ type : name ,
227+ title : input . command ,
228+ metadata : { sessionID : context . sessionID , shellID : info . id } ,
229+ run,
230+ } )
231+
232+ if ( input . background === true ) {
233+ yield * runtime . job . background ( job . id )
219234 yield * notifyWhenDone ( context . sessionID , context . toolCallID , input . command )
220235 return {
221236 output : BACKGROUND_STARTED ,
222- shellID : background . id ,
237+ shellID : info . id ,
223238 truncated : false ,
224239 status : "running" as const ,
225240 ...( warnings . length ? { warnings } : { } ) ,
226241 }
227242 }
228243
229- const info = yield * shell . create ( {
230- command : input . command ,
231- cwd : target . canonical ,
232- timeout,
233- metadata : { sessionID : context . sessionID } ,
234- } )
235- const final = yield * shell . wait ( info . id )
236- const page = yield * shell . output ( info . id , { limit : MAX_CAPTURE_BYTES } )
237-
238- if ( final . status === "timeout" ) {
244+ const result = yield * runtime . job . block ( { id : job . id , sessionID : context . sessionID } ) . pipe (
245+ Effect . onInterrupt ( ( ) => runtime . job . cancel ( job . id ) . pipe ( Effect . ignore ) ) ,
246+ )
247+ if ( result ?. type === "backgrounded" ) {
248+ yield * notifyWhenDone ( context . sessionID , context . toolCallID , input . command )
239249 return {
240- exit : final . exit ,
241- output : `Command exceeded timeout of ${ timeout } ms. Retry with a larger timeout if the command is expected to take longer.` ,
250+ output : BACKGROUND_STARTED ,
251+ shellID : info . id ,
242252 truncated : false ,
243- timeout : true ,
244- status : "completed" as const ,
253+ status : "running" as const ,
245254 ...( warnings . length ? { warnings } : { } ) ,
246255 }
247256 }
257+ if ( result ?. info . status === "error" ) return yield * Effect . fail ( new Error ( result . info . error ?? "Command failed" ) )
258+ if ( result ?. info . status === "cancelled" ) return yield * Effect . fail ( new Error ( "Command cancelled" ) )
248259
249- const truncated = page . size > page . cursor
250- const body = page . output || "(no output)"
251- const notice = truncated ? `\n\n[output truncated; full output saved to: ${ final . file } ]` : ""
252260 return {
253- exit : final . exit ,
254- output : `${ body } ${ notice } ` ,
255- truncated,
256- status : "completed" as const ,
261+ ...( yield * settleShell ( ) ) ,
257262 ...( warnings . length ? { warnings } : { } ) ,
258263 }
259264 } ) . pipe ( Effect . mapError ( ( ) => new ToolFailure ( { message : `Unable to execute command: ${ input . command } ` } ) ) ) ,
0 commit comments