on main help sign in
feat(shell): add blocking wait option to job_output tool (#2189)
9ec46b8Sean Porter
internal/agent/common_test.go+3−6
@@ -182,12 +182,9 @@ func coderAgent(r *vcr.Recorder, env fakeEnv, large, small fantasy.LanguageModel
182 GeneratedWith: true,
182 GeneratedWith: true,
183 }
183 }
184
184
185− // Clear skills paths to ensure test reproducibility - user's skills
185+ // Clear some fields to avoid issues with VCR cassette matching.
186− // would be included in prompt and break VCR cassette matching.
187− cfg.Options.SkillsPaths = []string{}
186+ cfg.Options.SkillsPaths = nil
188−
187+ cfg.Options.ContextPaths = nil
189− // Clear LSP config to ensure test reproducibility - user's LSP config
190− // would be included in prompt and break VCR cassette matching.
191 cfg.LSP = nil
188 cfg.LSP = nil
192
189
193 systemPrompt, err := prompt.Build(context.TODO(), large.Provider(), large.Model(), *cfg)
190 systemPrompt, err := prompt.Build(context.TODO(), large.Provider(), large.Model(), *cfg)
internal/agent/tools/job_output.go+5
@@ -19,6 +19,7 @@ var jobOutputDescription []byte
19
19
20type JobOutputParams struct {
20type JobOutputParams struct {
21 ShellID string `json:"shell_id" description:"The ID of the background shell to retrieve output from"`
21 ShellID string `json:"shell_id" description:"The ID of the background shell to retrieve output from"`
22+ Wait bool `json:"wait" description:"If true, block until the background shell completes before returning output"`
22}
23}
23
24
24type JobOutputResponseMetadata struct {
25type JobOutputResponseMetadata struct {
@@ -44,6 +45,10 @@ func NewJobOutputTool() fantasy.AgentTool {
44 return fantasy.NewTextErrorResponse(fmt.Sprintf("background shell not found: %s", params.ShellID)), nil
45 return fantasy.NewTextErrorResponse(fmt.Sprintf("background shell not found: %s", params.ShellID)), nil
45 }
46 }
46
47
48+ if params.Wait {
49+ bgShell.WaitContext(ctx)
50+ }
51+
47 stdout, stderr, done, err := bgShell.GetOutput()
52 stdout, stderr, done, err := bgShell.GetOutput()
48
53
49 var outputParts []string
54 var outputParts []string
internal/agent/tools/job_output.md+3
@@ -4,16 +4,19 @@ Retrieves the current output from a background shell.
4- Provide the shell ID returned from a background bash execution
4- Provide the shell ID returned from a background bash execution
5- Returns the current stdout and stderr output
5- Returns the current stdout and stderr output
6- Indicates whether the shell has completed execution
6- Indicates whether the shell has completed execution
7+- Set wait=true to block until the shell completes or the request context is done
7</usage>
8</usage>
8
9
9<features>
10<features>
10- View output from running background processes
11- View output from running background processes
11- Check if background process has completed
12- Check if background process has completed
12- Get cumulative output from process start
13- Get cumulative output from process start
14+- Optionally wait for process completion (returns early on context cancel)
13</features>
15</features>
14
16
15<tips>
17<tips>
16- Use this to monitor long-running processes
18- Use this to monitor long-running processes
17- Check the 'done' status to see if process completed
19- Check the 'done' status to see if process completed
18- Can be called multiple times to view incremental output
20- Can be called multiple times to view incremental output
21+- Use wait=true when you need the final output and exit status (or current output if the request cancels)
19</tips>
22</tips>
internal/shell/background.go+9
@@ -234,3 +234,12 @@ func (bs *BackgroundShell) IsDone() bool {
234func (bs *BackgroundShell) Wait() {
234func (bs *BackgroundShell) Wait() {
235 <-bs.done
235 <-bs.done
236}
236}
237+
238+func (bs *BackgroundShell) WaitContext(ctx context.Context) bool {
239+ select {
240+ case <-bs.done:
241+ return true
242+ case <-ctx.Done():
243+ return false
244+ }
245+}
internal/shell/background_test.go+25
@@ -307,3 +307,28 @@ func TestBackgroundShellManager_KillAll_Timeout(t *testing.T) {
307 // Must return promptly after timeout, not hang for 60 seconds.
307 // Must return promptly after timeout, not hang for 60 seconds.
308 require.Less(t, elapsed, 2*time.Second)
308 require.Less(t, elapsed, 2*time.Second)
309}
309}
310+
311+func TestBackgroundShell_WaitContext_Completed(t *testing.T) {
312+ t.Parallel()
313+
314+ done := make(chan struct{})
315+ close(done)
316+
317+ bgShell := &BackgroundShell{done: done}
318+
319+ ctx, cancel := context.WithTimeout(t.Context(), time.Second)
320+ t.Cleanup(cancel)
321+
322+ require.True(t, bgShell.WaitContext(ctx))
323+}
324+
325+func TestBackgroundShell_WaitContext_Canceled(t *testing.T) {
326+ t.Parallel()
327+
328+ bgShell := &BackgroundShell{done: make(chan struct{})}
329+
330+ ctx, cancel := context.WithCancel(t.Context())
331+ cancel()
332+
333+ require.False(t, bgShell.WaitContext(ctx))
334+}