Skip to content

Commit d2e7d92

Browse files
committed
Merge remote-tracking branch 'upstream/main' into inercia/aws-bedrock-provider
2 parents efc61e7 + f697a9a commit d2e7d92

35 files changed

+97
-107
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ cagent new --model dmr/ai/gemma3n:2B-F16 --max-iterations 15
258258
$ cagent new
259259

260260
------- Welcome to cagent! -------
261-
(Ctrl+C to stop the agent or exit)
261+
(Ctrl+C to stop the agent and exit)
262262

263263
What should your agent/agent team do? (describe its purpose):
264264

cmd/root/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func runHTTP(cmd *cobra.Command, args []string) error {
8080
}
8181
defer func() {
8282
for _, team := range teams {
83-
if err := team.StopToolSets(); err != nil {
83+
if err := team.StopToolSets(ctx); err != nil {
8484
slog.Error("Failed to stop tool sets", "error", err)
8585
}
8686
}

cmd/root/debug.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func debugToolsetsCommand(cmd *cobra.Command, args []string) error {
5050
}
5151

5252
slog.Info("Stopping toolsets", "agent", agentFilename)
53-
if err := team.StopToolSets(); err != nil {
53+
if err := team.StopToolSets(ctx); err != nil {
5454
slog.Error("Failed to stop tool sets", "error", err)
5555
}
5656

cmd/root/new.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func NewNewCmd() *cobra.Command {
8383
fmt.Print(blue("> "))
8484

8585
var err error
86-
prompt, err = readLine(ctx)
86+
prompt, err = readLine(ctx, os.Stdin)
8787
if err != nil {
8888
return fmt.Errorf("failed to read purpose: %w", err)
8989
}

cmd/root/run.go

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package root
22

33
import (
4-
"bufio"
54
"bytes"
65
"context"
76
"encoding/base64"
87
"fmt"
98
"io"
109
"log/slog"
1110
"os"
12-
"os/signal"
1311
"path/filepath"
1412
"sort"
1513
"strings"
@@ -189,7 +187,7 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
189187
return err
190188
}
191189
defer func() {
192-
if err := agents.StopToolSets(); err != nil {
190+
if err := agents.StopToolSets(ctx); err != nil {
193191
slog.Error("Failed to stop tool sets", "error", err)
194192
}
195193
}()
@@ -363,12 +361,21 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
363361
}
364362

365363
func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime, sess *session.Session, args []string) error {
364+
// Create a cancellable context for this agentic loop and wire Ctrl+C to cancel it
365+
ctx, cancel := context.WithCancel(ctx)
366+
367+
// Ensure telemetry is initialized and add to context so runtime can access it
368+
telemetry.EnsureGlobalTelemetryInitialized()
369+
if telemetryClient := telemetry.GetGlobalTelemetryClient(); telemetryClient != nil {
370+
ctx = telemetry.WithClient(ctx, telemetryClient)
371+
}
372+
366373
sess.Title = "Running agent"
367374
// If the last received event was an error, return it. That way the exit code
368375
// will be non-zero if the agent failed.
369376
var lastErr error
370377

371-
oneLoop := func(text string, scannerConfirmations *bufio.Scanner) error {
378+
oneLoop := func(text string, rd io.Reader) error {
372379
userInput := strings.TrimSpace(text)
373380
if userInput == "" {
374381
return nil
@@ -383,25 +390,6 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
383390
return nil
384391
}
385392

386-
// Create a cancellable context for this agentic loop and wire Ctrl+C to cancel it
387-
loopCtx, loopCancel := context.WithCancel(ctx)
388-
389-
// Ensure telemetry is initialized and add to context so runtime can access it
390-
telemetry.EnsureGlobalTelemetryInitialized()
391-
if telemetryClient := telemetry.GetGlobalTelemetryClient(); telemetryClient != nil {
392-
loopCtx = telemetry.WithClient(loopCtx, telemetryClient)
393-
}
394-
395-
sigCh := make(chan os.Signal, 1)
396-
signal.Notify(sigCh, os.Interrupt)
397-
go func() {
398-
<-sigCh
399-
// Ensure we break any inline typing output nicely
400-
fmt.Println()
401-
loopCancel()
402-
}()
403-
defer signal.Stop(sigCh)
404-
405393
// Parse for /attach commands in the message
406394
messageText, attachPath := parseAttachCommand(userInput)
407395

@@ -417,7 +405,7 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
417405
lastAgent := rt.CurrentAgent().Name()
418406
llmIsTyping := false
419407
var lastConfirmedToolCallID string
420-
for event := range rt.RunStream(loopCtx, sess) {
408+
for event := range rt.RunStream(ctx, sess) {
421409
agentName := event.GetAgentName()
422410
if agentName != "" && (firstLoop || lastAgent != agentName) {
423411
if !firstLoop {
@@ -449,9 +437,9 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
449437
fmt.Println()
450438
llmIsTyping = false
451439
}
452-
result := printToolCallWithConfirmation(e.ToolCall, scannerConfirmations)
440+
result := printToolCallWithConfirmation(ctx, e.ToolCall, rd)
453441
// If interrupted, skip resuming; the runtime will notice context cancellation and stop
454-
if loopCtx.Err() != nil {
442+
if ctx.Err() != nil {
455443
continue
456444
}
457445
lastConfirmedToolCallID = e.ToolCall.ID // Store the ID to avoid duplicate printing
@@ -466,7 +454,7 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
466454
lastConfirmedToolCallID = "" // Clear on reject since tool won't execute
467455
case ConfirmationAbort:
468456
// Stop the agent loop immediately
469-
loopCancel()
457+
cancel()
470458
continue
471459
}
472460
case *runtime.ToolCallEvent:
@@ -494,7 +482,7 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
494482
llmIsTyping = false
495483
}
496484
lowerErr := strings.ToLower(e.Error)
497-
if strings.Contains(lowerErr, "context cancel") && loopCtx.Err() != nil { // treat Ctrl+C cancellations as non-errors
485+
if strings.Contains(lowerErr, "context cancel") && ctx.Err() != nil { // treat Ctrl+C cancellations as non-errors
498486
lastErr = nil
499487
} else {
500488
lastErr = fmt.Errorf("%s", e.Error)
@@ -538,7 +526,7 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
538526
}
539527

540528
// If the loop ended due to Ctrl+C, inform the user succinctly
541-
if loopCtx.Err() != nil {
529+
if ctx.Err() != nil {
542530
fmt.Println(yellow("\n⚠️ agent stopped ⚠️"))
543531
}
544532

@@ -556,17 +544,16 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
556544
return fmt.Errorf("failed to read from stdin: %w", err)
557545
}
558546

559-
if err := oneLoop(string(buf), bufio.NewScanner(os.Stdin)); err != nil {
547+
if err := oneLoop(string(buf), os.Stdin); err != nil {
560548
return err
561549
}
562550
} else {
563-
if err := oneLoop(args[1], bufio.NewScanner(os.Stdin)); err != nil {
551+
if err := oneLoop(args[1], os.Stdin); err != nil {
564552
return err
565553
}
566554
}
567555
} else {
568556
printWelcomeMessage()
569-
scanner := bufio.NewScanner(os.Stdin)
570557
firstQuestion := true
571558
for {
572559
if !firstQuestion {
@@ -575,18 +562,15 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
575562
fmt.Print(blue("> "))
576563
firstQuestion = false
577564

578-
if !scanner.Scan() {
579-
break
565+
line, err := readLine(ctx, os.Stdin)
566+
if err != nil {
567+
return err
580568
}
581569

582-
if err := oneLoop(scanner.Text(), scanner); err != nil {
570+
if err := oneLoop(line, os.Stdin); err != nil {
583571
return err
584572
}
585573
}
586-
587-
if err := scanner.Err(); err != nil {
588-
return err
589-
}
590574
}
591575

592576
// Wrap runtime errors to prevent duplicate error messages and usage display

cmd/root/run_text_utils.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8+
"io"
89
"os"
910
"strings"
1011

@@ -39,7 +40,7 @@ const (
3940
// text utility functions
4041

4142
func printWelcomeMessage() {
42-
fmt.Printf("\n%s\n%s\n\n", blue("------- Welcome to %s! -------", bold(AppName)), white("(Ctrl+C to stop the agent or exit)"))
43+
fmt.Printf("\n%s\n%s\n\n", blue("------- Welcome to %s! -------", bold(AppName)), white("(Ctrl+C to stop the agent and exit)"))
4344
}
4445

4546
func printError(err error) {
@@ -58,7 +59,7 @@ func printToolCall(toolCall tools.ToolCall, colorFunc ...func(format string, a .
5859
fmt.Printf("\nCalling %s\n", c("%s%s", bold(toolCall.Function.Name), formatToolCallArguments(toolCall.Function.Arguments)))
5960
}
6061

61-
func printToolCallWithConfirmation(toolCall tools.ToolCall, scanner *bufio.Scanner) ConfirmationResult {
62+
func printToolCallWithConfirmation(ctx context.Context, toolCall tools.ToolCall, rd io.Reader) ConfirmationResult {
6263
fmt.Printf("\n%s\n", bold(yellow("🛠️ Tool call requires confirmation 🛠️")))
6364
printToolCall(toolCall, color.New(color.FgWhite).SprintfFunc())
6465
fmt.Printf("\n%s", bold(yellow("Can I run this tool? ([y]es/[a]ll/[n]o): ")))
@@ -97,10 +98,11 @@ func printToolCallWithConfirmation(toolCall tools.ToolCall, scanner *bufio.Scann
9798
}
9899

99100
// Fallback: line-based scanner (requires Enter)
100-
if !scanner.Scan() {
101+
text, err := readLine(ctx, rd)
102+
if err != nil {
101103
return ConfirmationReject
102104
}
103-
text := scanner.Text()
105+
104106
switch text {
105107
case "y":
106108
return ConfirmationApprove
@@ -123,7 +125,7 @@ func promptMaxIterationsContinue(ctx context.Context, maxIterations int) Confirm
123125
fmt.Printf("%s\n", white("This can happen with smaller or less capable models."))
124126
fmt.Printf("\n%s (y/n): ", blue("Do you want to continue for 10 more iterations?"))
125127

126-
response, err := readLine(ctx)
128+
response, err := readLine(ctx, os.Stdin)
127129
if err != nil {
128130
fmt.Printf("\n%s\n", red("Failed to read input, exiting..."))
129131
return ConfirmationAbort
@@ -146,7 +148,7 @@ func promptOAuthAuthorization(ctx context.Context, serverURL string) Confirmatio
146148
fmt.Printf("%s\n", white("Your browser will open automatically to complete the authorization."))
147149
fmt.Printf("\n%s (y/n): ", blue("Do you want to authorize access?"))
148150

149-
response, err := readLine(ctx)
151+
response, err := readLine(ctx, os.Stdin)
150152
if err != nil {
151153
fmt.Printf("\n%s\n", red("Failed to read input, aborting authorization..."))
152154
return ConfirmationAbort
@@ -290,15 +292,15 @@ func formatJSONValue(key string, value any) string {
290292
}
291293
}
292294

293-
func readLine(ctx context.Context) (string, error) {
295+
func readLine(ctx context.Context, rd io.Reader) (string, error) {
294296
lines := make(chan string)
295297
errs := make(chan error)
296298

297299
go func() {
298300
defer close(lines)
299301
defer close(errs)
300302

301-
reader := bufio.NewReader(os.Stdin)
303+
reader := bufio.NewReader(rd)
302304
line, err := reader.ReadString('\n')
303305
if err != nil {
304306
errs <- err

pkg/agent/agent.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func (a *Agent) Model() provider.Provider {
100100

101101
// Tools returns the tools available to this agent
102102
func (a *Agent) Tools(ctx context.Context) ([]tools.Tool, error) {
103-
if err := a.ensureToolSetsAreStarted(); err != nil {
103+
if err := a.ensureToolSetsAreStarted(ctx); err != nil {
104104
return nil, err
105105
}
106106

@@ -149,20 +149,14 @@ func (a *Agent) Commands() map[string]string {
149149
return a.commands
150150
}
151151

152-
func (a *Agent) ensureToolSetsAreStarted() error {
152+
func (a *Agent) ensureToolSetsAreStarted(ctx context.Context) error {
153153
for _, toolSet := range a.toolsets {
154154
// Skip if toolset is already started
155155
if toolSet.started.Load() {
156156
continue
157157
}
158158

159-
// The MCP toolset connection needs to persist beyond the initial HTTP request that triggered its creation.
160-
// When OAuth succeeds, subsequent agent requests should reuse the already-authenticated MCP connection.
161-
// But if the connection's underlying context is tied to the first HTTP request, it gets cancelled when that request
162-
// completes, killing the connection even though OAuth succeeded.
163-
// Use background context for starting toolsets to ensure they persist beyond request lifecycle
164-
// This is critical for OAuth flows where the toolset connection needs to remain alive after the initial HTTP request completes.
165-
if err := toolSet.Start(context.Background()); err != nil {
159+
if err := toolSet.Start(ctx); err != nil {
166160
return err
167161
}
168162

@@ -173,14 +167,14 @@ func (a *Agent) ensureToolSetsAreStarted() error {
173167
return nil
174168
}
175169

176-
func (a *Agent) StopToolSets() error {
170+
func (a *Agent) StopToolSets(ctx context.Context) error {
177171
for _, toolSet := range a.toolsets {
178172
// Only stop toolsets that are marked as started
179173
if !toolSet.started.Load() {
180174
continue
181175
}
182176

183-
if err := toolSet.Stop(); err != nil {
177+
if err := toolSet.Stop(ctx); err != nil {
184178
return fmt.Errorf("failed to stop toolset: %w", err)
185179
}
186180

pkg/codemode/codemode.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,11 @@ func (c *codeModeTool) Start(ctx context.Context) error {
117117
return nil
118118
}
119119

120-
func (c *codeModeTool) Stop() error {
120+
func (c *codeModeTool) Stop(ctx context.Context) error {
121121
var errs []error
122122

123123
for _, t := range c.toolsets {
124-
if err := t.Stop(); err != nil {
124+
if err := t.Stop(ctx); err != nil {
125125
errs = append(errs, err)
126126
}
127127
}

pkg/codemode/codemode_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,6 @@ func TestCodeModeTool_StartStop(t *testing.T) {
7979
err := tool.Start(t.Context())
8080
require.NoError(t, err)
8181

82-
err = tool.Stop()
82+
err = tool.Stop(t.Context())
8383
require.NoError(t, err)
8484
}

pkg/creator/agent.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ func (f *fsToolset) Start(ctx context.Context) error {
4040
return f.inner.Start(ctx)
4141
}
4242

43-
func (f *fsToolset) Stop() error {
44-
return f.inner.Stop()
43+
func (f *fsToolset) Stop(ctx context.Context) error {
44+
return f.inner.Stop(ctx)
4545
}
4646

4747
func (f *fsToolset) SetElicitationHandler(tools.ElicitationHandler) {

0 commit comments

Comments
 (0)