Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions agent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@
"description": "Maximum consecutive identical tool calls before the agent is terminated. Prevents degenerate loops. 0 uses the default of 5.",
"minimum": 0
},
"max_old_tool_call_tokens": {
"type": "integer",
"description": "Maximum number of tokens to keep from old tool call arguments and results. Older tool calls beyond this budget will have their content replaced with a placeholder. Tokens are approximated as len/4. Set to -1 to disable truncation (unlimited tool content). Default: 40000.",
"minimum": -1
},
"num_history_items": {
"type": "integer",
"description": "Number of history items to keep",
Expand Down
15 changes: 9 additions & 6 deletions cmd/root/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"

"github.com/docker/docker-agent/pkg/agent"
"github.com/docker/docker-agent/pkg/app"
"github.com/docker/docker-agent/pkg/cli"
"github.com/docker/docker-agent/pkg/config"
Expand Down Expand Up @@ -339,7 +340,7 @@ func (f *runExecFlags) createRemoteRuntimeAndSession(ctx context.Context, origin
func (f *runExecFlags) createLocalRuntimeAndSession(ctx context.Context, loadResult *teamloader.LoadResult) (runtime.Runtime, *session.Session, error) {
t := loadResult.Team

agent, err := t.Agent(f.agentName)
agt, err := t.Agent(f.agentName)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -404,7 +405,7 @@ func (f *runExecFlags) createLocalRuntimeAndSession(ctx context.Context, loadRes
slog.Debug("Loaded existing session", "session_id", resolvedID, "session_ref", f.sessionID, "agent", f.agentName)
} else {
wd, _ := os.Getwd()
sess = session.New(f.buildSessionOpts(agent.MaxIterations(), wd)...)
sess = session.New(f.buildSessionOpts(agt, wd)...)
// Session is stored lazily on first UpdateSession call (when content is added)
// This avoids creating empty sessions in the database
slog.Debug("Using local runtime", "agent", f.agentName)
Expand Down Expand Up @@ -494,9 +495,11 @@ func (f *runExecFlags) buildAppOpts(args []string) ([]app.Opt, error) {
// buildSessionOpts returns the canonical set of session options derived from
// CLI flags and agent configuration. Both the initial session and spawned
// sessions use this method so their options never drift apart.
func (f *runExecFlags) buildSessionOpts(maxIterations int, workingDir string) []session.Opt {
func (f *runExecFlags) buildSessionOpts(agt *agent.Agent, workingDir string) []session.Opt {
return []session.Opt{
session.WithMaxIterations(maxIterations),
session.WithMaxIterations(agt.MaxIterations()),
session.WithMaxConsecutiveToolCalls(agt.MaxConsecutiveToolCalls()),
Copy link
Contributor Author

@gtardif gtardif Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI @derekmisler I noticed the WithMaxConsecutiveToolCalls was not propagated properly (had the same issue initially for WithMaxOldToolCallTokens, I added the propagation here. We didn't noticed because we only use the default value I suppose)

session.WithMaxOldToolCallTokens(agt.MaxOldToolCallTokens()),
session.WithToolsApproved(f.autoApprove),
session.WithHideToolResults(f.hideToolResults),
session.WithWorkingDir(workingDir),
Expand All @@ -517,7 +520,7 @@ func (f *runExecFlags) createSessionSpawner(agentSource config.Source, sessStore
}

team := loadResult.Team
agent, err := team.Agent(f.agentName)
agt, err := team.Agent(f.agentName)
if err != nil {
return nil, nil, nil, err
}
Expand All @@ -543,7 +546,7 @@ func (f *runExecFlags) createSessionSpawner(agentSource config.Source, sessStore
}

// Create a new session
newSess := session.New(f.buildSessionOpts(agent.MaxIterations(), workingDir)...)
newSess := session.New(f.buildSessionOpts(agt, workingDir)...)

// Create cleanup function
cleanup := func() {
Expand Down
1 change: 1 addition & 0 deletions pkg/a2a/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func runDockerAgent(ctx agent.InvocationContext, t *team.Team, agentName string,
session.WithUserMessage(message),
session.WithMaxIterations(a.MaxIterations()),
session.WithMaxConsecutiveToolCalls(a.MaxConsecutiveToolCalls()),
session.WithMaxOldToolCallTokens(a.MaxOldToolCallTokens()),
session.WithToolsApproved(true),
)

Expand Down
1 change: 1 addition & 0 deletions pkg/acp/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func (a *Agent) NewSession(ctx context.Context, params acp.NewSessionRequest) (a
sessOpts := []session.Opt{
session.WithMaxIterations(rootAgent.MaxIterations()),
session.WithMaxConsecutiveToolCalls(rootAgent.MaxConsecutiveToolCalls()),
session.WithMaxOldToolCallTokens(rootAgent.MaxOldToolCallTokens()),
}
if workingDir != "" {
sessOpts = append(sessOpts, session.WithWorkingDir(workingDir))
Expand Down
5 changes: 5 additions & 0 deletions pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Agent struct {
addDescriptionParameter bool
maxIterations int
maxConsecutiveToolCalls int
maxOldToolCallTokens int
numHistoryItems int
addPromptFiles []string
tools []tools.Tool
Expand Down Expand Up @@ -81,6 +82,10 @@ func (a *Agent) MaxConsecutiveToolCalls() int {
return a.maxConsecutiveToolCalls
}

func (a *Agent) MaxOldToolCallTokens() int {
return a.maxOldToolCallTokens
}

func (a *Agent) NumHistoryItems() int {
return a.numHistoryItems
}
Expand Down
9 changes: 9 additions & 0 deletions pkg/agent/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ func WithMaxConsecutiveToolCalls(n int) Opt {
}
}

// WithMaxOldToolCallTokens sets the maximum token budget for old tool call content.
// Set to -1 to disable truncation (unlimited tool content).
// Set to 0 to use the default (40000).
func WithMaxOldToolCallTokens(n int) Opt {
return func(a *Agent) {
a.maxOldToolCallTokens = n
}
}

func WithNumHistoryItems(numHistoryItems int) Opt {
return func(a *Agent) {
a.numHistoryItems = numHistoryItems
Expand Down
1 change: 1 addition & 0 deletions pkg/config/latest/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ type AgentConfig struct {
AddDescriptionParameter bool `json:"add_description_parameter,omitempty"`
MaxIterations int `json:"max_iterations,omitempty"`
MaxConsecutiveToolCalls int `json:"max_consecutive_tool_calls,omitempty"`
MaxOldToolCallTokens int `json:"max_old_tool_call_tokens,omitempty"`
NumHistoryItems int `json:"num_history_items,omitempty"`
AddPromptFiles []string `json:"add_prompt_files,omitempty" yaml:"add_prompt_files,omitempty"`
Commands types.Commands `json:"commands,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions pkg/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func CreateToolHandler(t *team.Team, agentName string) func(context.Context, *mc
session.WithTitle("MCP tool call"),
session.WithMaxIterations(ag.MaxIterations()),
session.WithMaxConsecutiveToolCalls(ag.MaxConsecutiveToolCalls()),
session.WithMaxOldToolCallTokens(ag.MaxOldToolCallTokens()),
session.WithUserMessage(input.Message),
session.WithToolsApproved(true),
)
Expand Down
1 change: 1 addition & 0 deletions pkg/runtime/agent_delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func newSubSession(parent *session.Session, cfg SubSessionConfig, childAgent *ag
session.WithImplicitUserMessage(userMsg),
session.WithMaxIterations(childAgent.MaxIterations()),
session.WithMaxConsecutiveToolCalls(childAgent.MaxConsecutiveToolCalls()),
session.WithMaxOldToolCallTokens(childAgent.MaxOldToolCallTokens()),
session.WithTitle(cfg.Title),
session.WithToolsApproved(cfg.ToolsApproved),
session.WithSendUserMessage(false),
Expand Down
2 changes: 2 additions & 0 deletions pkg/server/session_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func (sm *SessionManager) CreateSession(ctx context.Context, sessionTemplate *se
opts = append(opts,
session.WithMaxIterations(sessionTemplate.MaxIterations),
session.WithMaxConsecutiveToolCalls(sessionTemplate.MaxConsecutiveToolCalls),
session.WithMaxOldToolCallTokens(sessionTemplate.MaxOldToolCallTokens),
session.WithToolsApproved(sessionTemplate.ToolsApproved),
)

Expand Down Expand Up @@ -347,6 +348,7 @@ func (sm *SessionManager) runtimeForSession(ctx context.Context, sess *session.S
}
sess.MaxIterations = agent.MaxIterations()
sess.MaxConsecutiveToolCalls = agent.MaxConsecutiveToolCalls()
sess.MaxOldToolCallTokens = agent.MaxOldToolCallTokens()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Session-specific MaxOldToolCallTokens may be overwritten

The new line sess.MaxOldToolCallTokens = agent.MaxOldToolCallTokens() follows the same pattern as MaxIterations and MaxConsecutiveToolCalls on lines 349-350, which unconditionally overwrites session-specific values with agent configuration when runtimeForSession is called.

Impact: If a session is created with a specific MaxOldToolCallTokens value (via CreateSession at lines 77-80), that value will be lost when the session is loaded or when a runtime is created for it.

Note: This pattern was already established for MaxIterations and MaxConsecutiveToolCalls, so this PR is just maintaining consistency. However, if the intention is for session-specific overrides to persist across reloads, this behavior may be unexpected.

Recommendation: Consider whether session-specific settings should persist, or document that sessions always use current agent configuration.


opts := []runtime.Opt{
runtime.WithCurrentAgent(currentAgent),
Expand Down
30 changes: 27 additions & 3 deletions pkg/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import (
)

const (
// MaxToolCallTokens is the maximum number of tokens to keep from tool call
// DefaultMaxOldToolCallTokens is the default maximum number of tokens to keep from tool call
// arguments and results. Older tool calls beyond this budget will have their
// content replaced with a placeholder. Tokens are approximated as len/4.
MaxToolCallTokens = 40000
DefaultMaxOldToolCallTokens = 40000

// toolContentPlaceholder is the text used to replace truncated tool content
toolContentPlaceholder = "[content truncated]"
Expand Down Expand Up @@ -94,6 +94,13 @@ type Session struct {
// repeatedly issues the same call without making progress. Default: 5.
MaxConsecutiveToolCalls int `json:"max_consecutive_tool_calls,omitempty"`

// MaxOldToolCallTokens is the maximum number of tokens to keep from old tool call
// arguments and results. Older tool calls beyond this budget will have their
// content replaced with a placeholder. Tokens are approximated as len/4.
// Set to -1 to disable truncation (unlimited tool content).
// Default: 40000 (when not configured or set to 0).
MaxOldToolCallTokens int `json:"max_old_tool_call_tokens,omitempty"`

// Starred indicates if this session has been starred by the user
Starred bool `json:"starred"`

Expand Down Expand Up @@ -442,6 +449,15 @@ func WithMaxConsecutiveToolCalls(n int) Opt {
}
}

// WithMaxOldToolCallTokens sets the maximum token budget for old tool call content.
// Set to -1 to disable truncation (unlimited tool content).
// Set to 0 to use the default (40000).
func WithMaxOldToolCallTokens(n int) Opt {
return func(s *Session) {
s.MaxOldToolCallTokens = n
}
}

func WithWorkingDir(workingDir string) Opt {
return func(s *Session) {
s.WorkingDir = workingDir
Expand Down Expand Up @@ -781,7 +797,15 @@ func (s *Session) GetMessages(a *agent.Agent) []chat.Message {
messages = trimMessages(messages, maxItems)
}

messages = truncateOldToolContent(messages, MaxToolCallTokens)
// Use configured max tokens or fall back to default constant if zero or unset.
// -1 means unlimited (no truncation).
maxOldToolCallTokens := s.MaxOldToolCallTokens
if maxOldToolCallTokens == 0 {
maxOldToolCallTokens = DefaultMaxOldToolCallTokens
}
if maxOldToolCallTokens > 0 { // If maxOldToolCallTokens is -1, skip truncation (unlimited)
messages = truncateOldToolContent(messages, maxOldToolCallTokens)
}

systemCount := 0
conversationCount := 0
Expand Down
1 change: 1 addition & 0 deletions pkg/teamloader/teamloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ func LoadWithConfig(ctx context.Context, agentSource config.Source, runConfig *c
agent.WithAddPromptFiles(promptFiles),
agent.WithMaxIterations(agentConfig.MaxIterations),
agent.WithMaxConsecutiveToolCalls(agentConfig.MaxConsecutiveToolCalls),
agent.WithMaxOldToolCallTokens(agentConfig.MaxOldToolCallTokens),
agent.WithNumHistoryItems(agentConfig.NumHistoryItems),
agent.WithCommands(expander.ExpandCommands(ctx, agentConfig.Commands)),
agent.WithHooks(config.MergeHooks(agentConfig.Hooks, cliHooks)),
Expand Down
Loading