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
42 changes: 23 additions & 19 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,26 @@ type Config struct {
// When nil, New uses a default in-memory history provider for local sessions.
HistoryProvider HistoryProvider

// AllowHistoryProviderConflict prevents returning an error when a configured
// HistoryProvider conflicts with service-managed history.
AllowHistoryProviderConflict bool
// ThrowOnHistoryProviderConflict controls whether a configured
// HistoryProvider conflicting with service-managed history returns an error.
// The default is true.
ThrowOnHistoryProviderConflict *bool

// SuppressHistoryProviderConflictWarning prevents logging a warning when a
// configured HistoryProvider conflicts with service-managed history.
SuppressHistoryProviderConflictWarning bool
// WarnOnHistoryProviderConflict controls whether a warning is logged when a
// configured HistoryProvider conflicts with service-managed history. The
// default is true.
WarnOnHistoryProviderConflict *bool

// KeepHistoryProviderOnConflict prevents clearing the configured HistoryProvider
// when it conflicts with service-managed history. Returning an error takes precedence.
KeepHistoryProviderOnConflict bool
// ClearOnHistoryProviderConflict controls whether the configured
// HistoryProvider is cleared when it conflicts with service-managed history.
// Returning an error takes precedence. The default is true.
ClearOnHistoryProviderConflict *bool

// ContextProviders inject and persist context around each agent run.
ContextProviders []ContextProvider

// DisableFuncAutoCall tells provider constructors not to add automatic function-tool calling middleware.
// DisableFuncAutoCall tells provider constructors not to add automatic
// function-tool calling middleware.
DisableFuncAutoCall bool

// Logger receives run, middleware, and provider diagnostics.
Expand Down Expand Up @@ -149,9 +153,9 @@ func New(prov ProviderConfig, cfg Config) *Agent {
historyProvider: historyProvider,
hasConfiguredHistory: cfg.HistoryProvider != nil,
hasDefaultHistoryProvider: hasDefaultHistoryProvider,
allowHistoryConflict: cfg.AllowHistoryProviderConflict,
suppressHistoryWarning: cfg.SuppressHistoryProviderConflictWarning,
keepHistoryOnConflict: cfg.KeepHistoryProviderOnConflict,
throwOnHistoryConflict: cfg.ThrowOnHistoryProviderConflict == nil || *cfg.ThrowOnHistoryProviderConflict,
warnOnHistoryConflict: cfg.WarnOnHistoryProviderConflict == nil || *cfg.WarnOnHistoryProviderConflict,
clearOnHistoryConflict: cfg.ClearOnHistoryProviderConflict == nil || *cfg.ClearOnHistoryProviderConflict,
providerDoesNotManageHistory: prov.ServiceDoesNotManageHistory,
contextProviders: contextProviders,
}
Expand Down Expand Up @@ -188,9 +192,9 @@ type Agent struct {
// provider is a local-session convenience and backs off for implicit per-run
// sessions and service-managed sessions.
hasDefaultHistoryProvider bool
allowHistoryConflict bool
suppressHistoryWarning bool
keepHistoryOnConflict bool
throwOnHistoryConflict bool
warnOnHistoryConflict bool
clearOnHistoryConflict bool
providerDoesNotManageHistory bool
contextProviders []ContextProvider
}
Expand Down Expand Up @@ -510,13 +514,13 @@ func (a *Agent) handleHistoryProviderConflict(ctx context.Context, provider Hist
return true, nil
}

if !a.suppressHistoryWarning && a.logger != nil {
if a.warnOnHistoryConflict && a.logger != nil {
a.logger.WarnContext(ctx, "history provider conflicts with service-managed history", slog.String("service_id", session.ServiceID()))
}
if !a.allowHistoryConflict {
if a.throwOnHistoryConflict {
return false, errors.New("only Session.ServiceID or HistoryProvider may be used, but not both; the service returned an ID indicating service-managed history while the agent has a HistoryProvider configured")
}
if !a.keepHistoryOnConflict {
if a.clearOnHistoryConflict {
a.historyCleared.Store(true)
return false, nil
}
Expand Down
30 changes: 15 additions & 15 deletions agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1617,10 +1617,10 @@ func TestAgent_Run_HistoryProvider_ClearsWhenThrowDisabledAndClearEnabled(t *tes
}
}
a := agent.New(agent.ProviderConfig{Run: runFn}, agent.Config{
ID: "test-agent",
Name: "test-agent",
HistoryProvider: historyProvider,
AllowHistoryProviderConflict: true,
ID: "test-agent",
Name: "test-agent",
HistoryProvider: historyProvider,
ThrowOnHistoryProviderConflict: new(false),
})

if _, err := a.RunText(t.Context(), "input", agent.WithSession(agenttest.CreateSession())).Collect(); err != nil {
Expand Down Expand Up @@ -1666,12 +1666,12 @@ func TestAgent_Run_HistoryProvider_KeepsReferenceButSkipsStoreWhenThrowAndClearD
}
}
a := agent.New(agent.ProviderConfig{Run: runFn}, agent.Config{
ID: "test-agent",
Name: "test-agent",
HistoryProvider: historyProvider,
AllowHistoryProviderConflict: true,
SuppressHistoryProviderConflictWarning: true,
KeepHistoryProviderOnConflict: true,
ID: "test-agent",
Name: "test-agent",
HistoryProvider: historyProvider,
ThrowOnHistoryProviderConflict: new(false),
WarnOnHistoryProviderConflict: new(false),
ClearOnHistoryProviderConflict: new(false),
})

if _, err := a.RunText(t.Context(), "input", agent.WithSession(agenttest.CreateSession())).Collect(); err != nil {
Expand Down Expand Up @@ -2298,11 +2298,11 @@ func TestAgent_Run_HistoryProvider_ConcurrentConflictClearIsRaceFree(t *testing.
}
}
a := agent.New(agent.ProviderConfig{Run: runFn}, agent.Config{
ID: "test-agent",
Name: "test-agent",
HistoryProvider: historyProvider,
AllowHistoryProviderConflict: true,
SuppressHistoryProviderConflictWarning: true,
ID: "test-agent",
Name: "test-agent",
HistoryProvider: historyProvider,
ThrowOnHistoryProviderConflict: new(false),
WarnOnHistoryProviderConflict: new(false),
})

const goroutines = 64
Expand Down
2 changes: 1 addition & 1 deletion agent/compaction/compaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ func TestSummarizationStrategy_InsertsSummaryAndPreservesRecentGroups(t *testing
Trigger: compaction.GroupsExceed(2),
Summarizer: summarizer,
MinimumPreservedGroups: new(2),
SummarizationPrompt: "summarize",
SummarizationPrompt: new("summarize"),
}

compacted, err := strategy.Compact(t.Context(), index)
Expand Down
2 changes: 1 addition & 1 deletion agent/compaction/index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func TestMessageIndex_UpdatePreservesStateFromCompactedProjection(t *testing.T)
Trigger: compaction.GroupsExceed(2),
Summarizer: compaction.SummarizerFunc(func(context.Context, []*message.Message) (string, error) { return "older context", nil }),
MinimumPreservedGroups: new(2),
SummarizationPrompt: "summarize",
SummarizationPrompt: new("summarize"),
}

compacted, err := strategy.Compact(t.Context(), index)
Expand Down
9 changes: 6 additions & 3 deletions agent/compaction/summarization.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ type SummarizationStrategy struct {
MinimumPreservedGroups *int

// SummarizationPrompt is the system prompt prepended to messages sent to Summarizer.
// When empty, a default prompt is used.
SummarizationPrompt string
// When nil, a default prompt is used.
SummarizationPrompt *string

// SummaryUnavailableMessage is used when Summarizer returns only whitespace.
// When empty, a default unavailable message is used.
Expand All @@ -87,7 +87,10 @@ func (strategy *SummarizationStrategy) Compact(ctx context.Context, index *Messa
if strategy.MinimumPreservedGroups != nil {
minimumPreservedGroups = max(*strategy.MinimumPreservedGroups, 0)
}
summarizationPrompt := cmp.Or(strategy.SummarizationPrompt, defaultSummarizationPrompt)
summarizationPrompt := defaultSummarizationPrompt
if strategy.SummarizationPrompt != nil {
summarizationPrompt = *strategy.SummarizationPrompt
}
summaryUnavailableMessage := cmp.Or(strategy.SummaryUnavailableMessage, "[Summary unavailable]")

nonSystemIncludedCount := index.IncludedNonSystemGroupCount()
Expand Down
44 changes: 21 additions & 23 deletions agent/harness/agentmode/agentmode.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ For every new substantive user request, including short factual questions, your

{available_modes}`

// Mode describes a named operating mode with a description of its behavior.
// Mode describes a named operating mode and its behavioral instructions.
type Mode struct {
Name string
Description string
Name string
Instructions string
}

// state is persisted in the session across turns.
Expand All @@ -58,18 +58,18 @@ type Config struct {
Modes []Mode

// DefaultMode is the initial mode. Must be one of the configured Modes.
// If empty, the first mode is used.
DefaultMode string
// If nil, the first mode is used.
DefaultMode *string

// Instructions overrides the default instruction template.
// Use {available_modes} and {current_mode} as placeholders.
Instructions string
Instructions *string
}

var defaultModes = []Mode{
{
Name: "plan",
Description: `Use this mode when analyzing requirements, breaking down tasks, and creating plans. This is the interactive mode — ask clarifying questions, discuss options, and get user approval before proceeding.
Instructions: `Use this mode when analyzing requirements, breaking down tasks, and creating plans. This is the interactive mode — ask clarifying questions, discuss options, and get user approval before proceeding.

Process to follow when in plan mode:
1. Analyze the request with the purpose of building a research plan.
Expand All @@ -86,7 +86,7 @@ Process to follow when in plan mode:
},
{
Name: "execute",
Description: `Determine the type of ask:
Instructions: `Determine the type of ask:
1. Simple question that doesn't require any further work to answer.
2. Any other work, including complex user request that requires a multi-step process to satisfy.

Expand All @@ -103,29 +103,24 @@ If 2. Work autonomously using your best judgment — do not ask the user questio
// New creates a new agent mode context provider.
// A zero-value Config uses defaults (plan/execute modes).
//
// Panics if the configuration is invalid (empty modes, duplicate names,
// empty mode name, or default mode not in the configured set).
// Panics if the configuration contains duplicate names, an empty mode name or
// instructions, or a default mode that is not in the configured set.
func New(cfg Config) *Provider {
modes := defaultModes
defaultMode := ""
instructions := defaultInstructions

if len(cfg.Modes) > 0 {
modes = cfg.Modes
}
if cfg.DefaultMode != "" {
defaultMode = cfg.DefaultMode
}
if cfg.Instructions != "" {
instructions = cfg.Instructions
}

if len(modes) == 0 {
panic("agentmode: at least one mode must be configured")
}

if defaultMode == "" {
defaultMode = modes[0].Name
defaultMode := modes[0].Name
if cfg.DefaultMode != nil {
defaultMode = *cfg.DefaultMode
}
instructions := defaultInstructions
if cfg.Instructions != nil {
instructions = *cfg.Instructions
}

// Validate modes: no empty names, no duplicates.
Expand All @@ -134,6 +129,9 @@ func New(cfg Config) *Provider {
if strings.TrimSpace(m.Name) == "" {
panic(fmt.Sprintf("agentmode: mode at index %d has an empty name", i))
}
if strings.TrimSpace(m.Instructions) == "" {
panic(fmt.Sprintf("agentmode: mode at index %d has empty instructions", i))
}
if _, exists := validModes[m.Name]; exists {
panic(fmt.Sprintf("agentmode: duplicate mode name %q", m.Name))
}
Expand Down Expand Up @@ -276,7 +274,7 @@ func (p *Provider) provide(ctx context.Context, invoking agent.InvokingContext)
func (p *Provider) buildInstructions(currentMode string) string {
var sb strings.Builder
for _, m := range p.modes {
fmt.Fprintf(&sb, "#### %s\n\n%s\n\n", m.Name, strings.TrimRight(m.Description, "\n"))
fmt.Fprintf(&sb, "#### %s\n\n%s\n\n", m.Name, strings.TrimRight(m.Instructions, "\n"))
}
modesText := strings.TrimRight(sb.String(), "\n")

Expand Down
43 changes: 27 additions & 16 deletions agent/harness/agentmode/agentmode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ func TestProvide_InstructionsIncludeCurrentMode(t *testing.T) {
func TestCustomModes_AreUsed(t *testing.T) {
p := agentmode.New(agentmode.Config{
Modes: []agentmode.Mode{
{Name: "draft", Description: "Draft mode"},
{Name: "review", Description: "Review mode"},
{Name: "draft", Instructions: "Draft mode"},
{Name: "review", Instructions: "Review mode"},
},
})
opts := sessionOpts()
Expand All @@ -167,8 +167,8 @@ func TestCustomModes_AreUsed(t *testing.T) {
func TestCustomModes_SetModeValidatesAgainstList(t *testing.T) {
p := agentmode.New(agentmode.Config{
Modes: []agentmode.Mode{
{Name: "draft", Description: "Draft mode"},
{Name: "review", Description: "Review mode"},
{Name: "draft", Instructions: "Draft mode"},
{Name: "review", Instructions: "Review mode"},
},
})
opts := sessionOpts()
Expand All @@ -189,10 +189,10 @@ func TestCustomModes_SetModeValidatesAgainstList(t *testing.T) {
func TestCustomDefaultMode_IsUsed(t *testing.T) {
p := agentmode.New(agentmode.Config{
Modes: []agentmode.Mode{
{Name: "draft", Description: "Draft mode"},
{Name: "review", Description: "Review mode"},
{Name: "draft", Instructions: "Draft mode"},
{Name: "review", Instructions: "Review mode"},
},
DefaultMode: "review",
DefaultMode: new("review"),
})
opts := sessionOpts()

Expand All @@ -211,9 +211,9 @@ func TestInvalidDefaultMode_Panics(t *testing.T) {
}()
agentmode.New(agentmode.Config{
Modes: []agentmode.Mode{
{Name: "plan", Description: "Plan mode"},
{Name: "plan", Instructions: "Plan mode"},
},
DefaultMode: "nonexistent",
DefaultMode: new("nonexistent"),
})
}

Expand All @@ -234,8 +234,8 @@ func TestEmptyModes_UsesDefaults(t *testing.T) {
func TestCustomModes_AppearInInstructions(t *testing.T) {
p := agentmode.New(agentmode.Config{
Modes: []agentmode.Mode{
{Name: "alpha", Description: "Alpha mode description"},
{Name: "beta", Description: "Beta mode description"},
{Name: "alpha", Instructions: "Alpha mode description"},
{Name: "beta", Instructions: "Beta mode description"},
},
})
opts := sessionOpts()
Expand All @@ -260,7 +260,7 @@ func TestCustomModes_AppearInInstructions(t *testing.T) {
}
}

// 9. AgentMode_RequiresNameAndDescription
// 9. AgentMode_RequiresNameAndInstructions
func TestEmptyModeName_Panics(t *testing.T) {
defer func() {
if r := recover(); r == nil {
Expand All @@ -269,11 +269,22 @@ func TestEmptyModeName_Panics(t *testing.T) {
}()
agentmode.New(agentmode.Config{
Modes: []agentmode.Mode{
{Name: "", Description: "No name"},
{Name: "", Instructions: "No name"},
},
})
}

func TestEmptyModeInstructions_Panics(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected panic for empty mode instructions")
}
}()
agentmode.New(agentmode.Config{
Modes: []agentmode.Mode{{Name: "plan"}},
})
}

// 10. Options_DuplicateModeNames_Throws
func TestDuplicateModeNames_Panics(t *testing.T) {
defer func() {
Expand All @@ -283,8 +294,8 @@ func TestDuplicateModeNames_Panics(t *testing.T) {
}()
agentmode.New(agentmode.Config{
Modes: []agentmode.Mode{
{Name: "plan", Description: "Plan mode"},
{Name: "plan", Description: "Duplicate"},
{Name: "plan", Instructions: "Plan mode"},
{Name: "plan", Instructions: "Duplicate"},
},
})
}
Expand Down Expand Up @@ -533,7 +544,7 @@ func TestState_PersistsAcrossInvocations(t *testing.T) {
// 24. Options_CustomInstructions_OverridesDefault
func TestCustomInstructions_OverridesDefault(t *testing.T) {
p := agentmode.New(agentmode.Config{
Instructions: "Custom instructions for mode {current_mode}",
Instructions: new("Custom instructions for mode {current_mode}"),
})
opts := sessionOpts()

Expand Down
Loading
Loading