Skip to content

Commit ba150ae

Browse files
committed
Move maxIteration to session
Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
1 parent 60d4343 commit ba150ae

11 files changed

Lines changed: 74 additions & 32 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,10 @@ export GOOGLE_API_KEY=your_api_key_here # if anthropic and openai keys are n
219219
`--max-tokens` can be specified to override the context limit used.
220220
When using DMR, the default is 16k to limit memory usage. With all other providers the default is 64k
221221

222-
Example of provider, model and context size overriding:
222+
`--max-iterations` can be specified to override how many times the agent is allowed to loop when doing tool calling etc.
223+
When using DMR, the default is set to 20 (small local models have the highest chance of getting confused and looping endlessly). For all other providers, the default is 0 (unlimited).
224+
225+
Example of provider, model, context size and max iterations overriding:
223226

224227
```sh
225228
# Use GPT-5 via OpenAI
@@ -230,6 +233,9 @@ cagent new --model dmr/ai/gemma3-qat:12B
230233
231234
# Override the max_tokens used during generation, default is 64k, 16k when using the dmr provider
232235
cagent new --model openai/gpt-5-mini --max-tokens 32000
236+
237+
# Override max_iterations to limit how much the model can loop autonomously
238+
cagent new --model dmr/ai/gemma3n:2B-F16 --max-iterations 15
233239
```
234240

235241
---

cmd/root/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
349349
lastAgent := rt.CurrentAgent().Name()
350350
llmIsTyping := false
351351
var lastConfirmedToolCallID string
352-
for event := range rt.RunStream(loopCtx, sess, 0) {
352+
for event := range rt.RunStream(loopCtx, sess) {
353353
if event.GetAgentName() != "" && (firstLoop || lastAgent != event.GetAgentName()) {
354354
if !firstLoop {
355355
if llmIsTyping {

examples/golibrary/stream/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func main() {
4444

4545
sess := session.New(session.WithUserMessage("", "How are you doing?"))
4646

47-
events := rt.RunStream(ctx, sess, 0)
47+
events := rt.RunStream(ctx, sess)
4848
for event := range events {
4949
switch e := event.(type) {
5050
case *runtime.AgentChoiceEvent:

pkg/app/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func (a *App) Run(ctx context.Context, message string) {
5858

5959
// User message
6060
a.session.AddMessage(session.UserMessage(a.agentFilename, message))
61-
for event := range a.runtime.RunStream(ctx, a.session, 0) {
61+
for event := range a.runtime.RunStream(ctx, a.session) {
6262
a.events <- event
6363
}
6464
}()

pkg/creator/agent.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,11 @@ func StreamCreateAgent(ctx context.Context, baseDir, prompt string, runConfig co
232232
return nil, nil, fmt.Errorf("failed to create runtime: %w", err)
233233
}
234234

235-
sess := session.New(session.WithUserMessage("", prompt))
235+
sess := session.New(
236+
session.WithUserMessage("", prompt),
237+
session.WithMaxIterations(maxIterations),
238+
)
236239
sess.ToolsApproved = true
237240

238-
return rt.RunStream(ctx, sess, maxIterations), rt, nil
241+
return rt.RunStream(ctx, sess), rt, nil
239242
}

pkg/runtime/remote_runtime.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,8 @@ func (r *RemoteRuntime) CurrentAgent() *agent.Agent {
7272
return agent.New(r.currentAgent, fmt.Sprintf("Remote agent: %s", r.currentAgent))
7373
}
7474

75-
// RunStream starts the agent's interaction loop with an optional iteration limit
76-
// If maxIterations is 0, there is no limit
77-
func (r *RemoteRuntime) RunStream(ctx context.Context, sess *session.Session, maxIterations int) <-chan Event {
75+
// RunStream starts the agent's interaction loop and returns a channel of events
76+
func (r *RemoteRuntime) RunStream(ctx context.Context, sess *session.Session) <-chan Event {
7877
slog.Debug("Starting remote runtime stream", "agent", r.currentAgent, "session_id", r.sessionID)
7978
events := make(chan Event, 128)
8079

@@ -126,7 +125,7 @@ func (r *RemoteRuntime) RunStream(ctx context.Context, sess *session.Session, ma
126125

127126
// Run starts the agent's interaction loop and returns the final messages
128127
func (r *RemoteRuntime) Run(ctx context.Context, sess *session.Session) ([]session.Message, error) {
129-
eventsChan := r.RunStream(ctx, sess, 0)
128+
eventsChan := r.RunStream(ctx, sess)
130129

131130
for event := range eventsChan {
132131
if errEvent, ok := event.(*ErrorEvent); ok {

pkg/runtime/runtime.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ type Runtime interface {
4040
// CurrentAgent returns the currently active agent
4141
CurrentAgent() *agent.Agent
4242
// RunStream starts the agent's interaction loop and returns a channel of events
43-
RunStream(ctx context.Context, sess *session.Session, maxIterations int) <-chan Event
43+
RunStream(ctx context.Context, sess *session.Session) <-chan Event
4444
// Run starts the agent's interaction loop and returns the final messages
4545
Run(ctx context.Context, sess *session.Session) ([]session.Message, error)
4646
// Resume allows resuming execution after user confirmation
@@ -168,9 +168,8 @@ func (r *runtime) finalizeEventChannel(ctx context.Context, sess *session.Sessio
168168
}
169169
}
170170

171-
// RunStream starts the agent's interaction loop with an optional iteration limit
172-
// If maxIterations is 0, there is no limit
173-
func (r *runtime) RunStream(ctx context.Context, sess *session.Session, maxIterations int) <-chan Event {
171+
// RunStream starts the agent's interaction loop and returns a channel of events
172+
func (r *runtime) RunStream(ctx context.Context, sess *session.Session) <-chan Event {
174173
slog.Debug("Starting runtime stream", "agent", r.currentAgent, "session_id", sess.ID)
175174
events := make(chan Event, 128)
176175

@@ -208,18 +207,20 @@ func (r *runtime) RunStream(ctx context.Context, sess *session.Session, maxItera
208207
}
209208

210209
iteration := 0
210+
// Use a runtime copy of maxIterations so we don't modify the session's persistent config
211+
runtimeMaxIterations := sess.MaxIterations
211212
for {
212213
// Check iteration limit
213-
if maxIterations > 0 && iteration >= maxIterations {
214-
slog.Debug("Maximum iterations reached", "agent", a.Name(), "iterations", iteration, "max", maxIterations)
215-
events <- MaxIterationsReached(maxIterations)
214+
if runtimeMaxIterations > 0 && iteration >= runtimeMaxIterations {
215+
slog.Debug("Maximum iterations reached", "agent", a.Name(), "iterations", iteration, "max", runtimeMaxIterations)
216+
events <- MaxIterationsReached(runtimeMaxIterations)
216217

217218
// Wait for user decision
218219
select {
219220
case resumeType := <-r.resumeChan:
220221
if resumeType == ResumeTypeApprove {
221222
slog.Debug("User chose to continue after max iterations", "agent", a.Name())
222-
maxIterations = iteration + 10
223+
runtimeMaxIterations = iteration + 10
223224
} else {
224225
slog.Debug("User chose to exit after max iterations", "agent", a.Name())
225226
return
@@ -425,7 +426,7 @@ func (r *runtime) ResumeCodeReceived(_ context.Context, code string) error {
425426

426427
// Run starts the agent's interaction loop
427428
func (r *runtime) Run(ctx context.Context, sess *session.Session) ([]session.Message, error) {
428-
eventsChan := r.RunStream(ctx, sess, 0)
429+
eventsChan := r.RunStream(ctx, sess)
429430

430431
for event := range eventsChan {
431432
if errEvent, ok := event.(*ErrorEvent); ok {
@@ -839,12 +840,16 @@ func (r *runtime) handleTaskTransfer(ctx context.Context, sess *session.Session,
839840
}
840841

841842
slog.Debug("Creating new session with parent session", "parent_session_id", sess.ID, "tools_approved", sess.ToolsApproved)
842-
s := session.New(session.WithSystemMessage(memberAgentTask), session.WithUserMessage("", "Follow the default instructions"))
843+
s := session.New(
844+
session.WithSystemMessage(memberAgentTask),
845+
session.WithUserMessage("", "Follow the default instructions"),
846+
session.WithMaxIterations(sess.MaxIterations),
847+
)
843848
s.SendUserMessage = false
844849
s.Title = "Transferred task"
845850
s.ToolsApproved = sess.ToolsApproved
846851

847-
for event := range r.RunStream(ctx, s, 0) {
852+
for event := range r.RunStream(ctx, s) {
848853
evts <- event
849854
if errEvent, ok := event.(*ErrorEvent); ok {
850855
span.RecordError(fmt.Errorf("%s", errEvent.Error))

pkg/server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,7 @@ func (s *Server) runAgent(c echo.Context) error {
883883
c.Response().Header().Set("Connection", "keep-alive")
884884
c.Response().WriteHeader(http.StatusOK)
885885

886-
streamChan := rt.RunStream(c.Request().Context(), sess, 0)
886+
streamChan := rt.RunStream(c.Request().Context(), sess)
887887
for event := range streamChan {
888888
data, err := json.Marshal(event)
889889
if err != nil {

pkg/session/migrations.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,13 @@ func getAllMigrations() []Migration {
193193
UpSQL: `ALTER TABLE sessions ADD COLUMN send_user_message BOOLEAN DEFAULT 1`,
194194
DownSQL: `ALTER TABLE sessions DROP COLUMN send_user_message`,
195195
},
196+
{
197+
ID: 7,
198+
Name: "007_add_max_iterations_column",
199+
Description: "Add max_iterations column to sessions table",
200+
UpSQL: `ALTER TABLE sessions ADD COLUMN max_iterations INTEGER DEFAULT 0`,
201+
DownSQL: `ALTER TABLE sessions DROP COLUMN max_iterations`,
202+
},
196203
// Add more migrations here as needed
197204
}
198205
}

pkg/session/session.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ type Session struct {
5858
// SendUserMessage is a flag to indicate if the user message should be sent
5959
SendUserMessage bool
6060

61+
// MaxIterations is the maximum number of agentic loop iterations to prevent infinite loops
62+
// If 0, there is no limit
63+
MaxIterations int `json:"max_iterations"`
64+
6165
InputTokens int `json:"input_tokens"`
6266
OutputTokens int `json:"output_tokens"`
6367
Cost float64 `json:"cost"`
@@ -165,6 +169,12 @@ func WithSystemMessage(content string) Opt {
165169
}
166170
}
167171

172+
func WithMaxIterations(maxIterations int) Opt {
173+
return func(s *Session) {
174+
s.MaxIterations = maxIterations
175+
}
176+
}
177+
168178
// New creates a new agent session
169179
func New(opts ...Opt) *Session {
170180
sessionID := uuid.New().String()

0 commit comments

Comments
 (0)