项目文件夹

文件
2026-07-13 12:49:10 +08:00

881 行
27 KiB
Go

/*
* Copyright 2025 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Package planexecute implements a plan–execute–replan style agent.
package planexecute
import (
"context"
"encoding/json"
"fmt"
"runtime/debug"
"strings"
"github.com/bytedance/sonic"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/prompt"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/internal/safe"
"github.com/cloudwego/eino/schema"
)
func init() {
schema.RegisterName[*defaultPlan]("_eino_adk_plan_execute_default_plan")
schema.RegisterName[ExecutedStep]("_eino_adk_plan_execute_executed_step")
schema.RegisterName[[]ExecutedStep]("_eino_adk_plan_execute_executed_steps")
}
// Plan represents an execution plan with a sequence of actionable steps.
// It supports JSON serialization and deserialization while providing access to the first step.
type Plan interface {
// FirstStep returns the first step to be executed in the plan.
FirstStep() string
// Marshaler serializes the Plan into JSON.
// The resulting JSON can be used in prompt templates.
json.Marshaler
// Unmarshaler deserializes JSON content into the Plan.
// This processes output from structured chat models or tool calls into the Plan structure.
json.Unmarshaler
}
// NewPlan is a function type that creates a new Plan instance.
type NewPlan func(ctx context.Context) Plan
// defaultPlan is the default implementation of the Plan interface.
//
// JSON Schema:
//
// {
// "type": "object",
// "properties": {
// "steps": {
// "type": "array",
// "items": {
// "type": "string"
// },
// "description": "Ordered list of actions to be taken. Each step should be clear, actionable, and arranged in a logical sequence."
// }
// },
// "required": ["steps"]
// }
type defaultPlan struct {
// Steps contains the ordered list of actions to be taken.
// Each step should be clear, actionable, and arranged in a logical sequence.
Steps []string `json:"steps"`
}
// FirstStep returns the first step in the plan or an empty string if no steps exist.
func (p *defaultPlan) FirstStep() string {
if len(p.Steps) == 0 {
return ""
}
return p.Steps[0]
}
func (p *defaultPlan) MarshalJSON() ([]byte, error) {
type planTyp defaultPlan
return sonic.Marshal((*planTyp)(p))
}
func (p *defaultPlan) UnmarshalJSON(bytes []byte) error {
type planTyp defaultPlan
return sonic.Unmarshal(bytes, (*planTyp)(p))
}
// Response represents the final response to the user.
// This struct is used for JSON serialization/deserialization of the final response
// generated by the model.
type Response struct {
// Response is the complete response to provide to the user.
// This field is required.
Response string `json:"response"`
}
var (
// PlanToolInfo defines the schema for the Plan tool that can be used with ToolCallingChatModel.
// This schema instructs the model to generate a structured plan with ordered steps.
PlanToolInfo = schema.ToolInfo{
Name: "plan",
Desc: "Plan with a list of steps to execute in order. Each step should be clear, actionable, and arranged in a logical sequence. The output will be used to guide the execution process.",
ParamsOneOf: schema.NewParamsOneOfByParams(
map[string]*schema.ParameterInfo{
"steps": {
Type: schema.Array,
ElemInfo: &schema.ParameterInfo{Type: schema.String},
Desc: "different steps to follow, should be in sorted order",
Required: true,
},
},
),
}
// RespondToolInfo defines the schema for the response tool that can be used with ToolCallingChatModel.
// This schema instructs the model to generate a direct response to the user.
RespondToolInfo = schema.ToolInfo{
Name: "respond",
Desc: "Generate a direct response to the user. Use this tool when you have all the information needed to provide a final answer.",
ParamsOneOf: schema.NewParamsOneOfByParams(
map[string]*schema.ParameterInfo{
"response": {
Type: schema.String,
Desc: "The complete response to provide to the user",
Required: true,
},
},
),
}
// PlannerPrompt is the prompt template for the planner.
// It provides context and guidance to the planner on how to generate the Plan.
PlannerPrompt = prompt.FromMessages(schema.FString,
schema.SystemMessage(`You are an expert planning agent. Given an objective, create a comprehensive step-by-step plan to achieve the objective.
## YOUR TASK
Analyze the objective and generate a strategic plan that breaks down the goal into manageable, executable steps.
## PLANNING REQUIREMENTS
Each step in your plan must be:
- **Specific and actionable**: Clear instructions that can be executed without ambiguity
- **Self-contained**: Include all necessary context, parameters, and requirements
- **Independently executable**: Can be performed by an agent without dependencies on other steps
- **Logically sequenced**: Arranged in optimal order for efficient execution
- **Objective-focused**: Directly contribute to achieving the main goal
## PLANNING GUIDELINES
- Eliminate redundant or unnecessary steps
- Include relevant constraints, parameters, and success criteria for each step
- Ensure the final step produces a complete answer or deliverable
- Anticipate potential challenges and include mitigation strategies
- Structure steps to build upon each other logically
- Provide sufficient detail for successful execution
## QUALITY CRITERIA
- Plan completeness: Does it address all aspects of the objective?
- Step clarity: Can each step be understood and executed independently?
- Logical flow: Do steps follow a sensible progression?
- Efficiency: Is this the most direct path to the objective?
- Adaptability: Can the plan handle unexpected results or changes?`),
schema.MessagesPlaceholder("input", false),
)
// ExecutorPrompt is the prompt template for the executor.
// It provides context and guidance to the executor on how to execute the Task.
ExecutorPrompt = prompt.FromMessages(schema.FString,
schema.SystemMessage(`You are a diligent and meticulous executor agent. Follow the given plan and execute your tasks carefully and thoroughly.`),
schema.UserMessage(`## OBJECTIVE
{input}
## Given the following plan:
{plan}
## COMPLETED STEPS & RESULTS
{executed_steps}
## Your task is to execute the first step, which is:
{step}`))
// ReplannerPrompt is the prompt template for the replanner.
// It provides context and guidance to the replanner on how to regenerate the Plan.
ReplannerPrompt = prompt.FromMessages(schema.FString,
schema.SystemMessage(
`You are going to review the progress toward an objective. Analyze the current state and determine the optimal next action.
## YOUR TASK
Based on the progress above, you MUST choose exactly ONE action:
### Option 1: COMPLETE (if objective is fully achieved)
Call '{respond_tool}' with:
- A comprehensive final answer
- Clear conclusion summarizing how the objective was met
- Key insights from the execution process
### Option 2: CONTINUE (if more work is needed)
Call '{plan_tool}' with a revised plan that:
- Contains ONLY remaining steps (exclude completed ones)
- Incorporates lessons learned from executed steps
- Addresses any gaps or issues discovered
- Maintains logical step sequence
## PLANNING REQUIREMENTS
Each step in your plan must be:
- **Specific and actionable**: Clear instructions that can be executed without ambiguity
- **Self-contained**: Include all necessary context, parameters, and requirements
- **Independently executable**: Can be performed by an agent without dependencies on other steps
- **Logically sequenced**: Arranged in optimal order for efficient execution
- **Objective-focused**: Directly contribute to achieving the main goal
## PLANNING GUIDELINES
- Eliminate redundant or unnecessary steps
- Adapt strategy based on new information
- Include relevant constraints, parameters, and success criteria for each step
## DECISION CRITERIA
- Has the original objective been completely satisfied?
- Are there any remaining requirements or sub-goals?
- Do the results suggest a need for strategy adjustment?
- What specific actions are still required?`),
schema.UserMessage(`## OBJECTIVE
{input}
## ORIGINAL PLAN
{plan}
## COMPLETED STEPS & RESULTS
{executed_steps}`),
)
)
const (
// UserInputSessionKey is the session key for the user input.
UserInputSessionKey = "UserInput"
// PlanSessionKey is the session key for the plan.
PlanSessionKey = "Plan"
// ExecutedStepSessionKey is the session key for the execute result.
ExecutedStepSessionKey = "ExecutedStep"
// ExecutedStepsSessionKey is the session key for the execute results.
ExecutedStepsSessionKey = "ExecutedSteps"
)
// PlannerConfig provides configuration options for creating a planner agent.
// There are two ways to configure the planner to generate structured Plan output:
// 1. Use ChatModelWithFormattedOutput: A model pre-configured to output in the Plan format
// 2. Use ToolCallingChatModel + ToolInfo: A model that uses tool calling to generate
// the Plan structure
type PlannerConfig struct {
// ChatModelWithFormattedOutput is a model pre-configured to output in the Plan format.
// Create this by configuring a model to output structured data directly.
// See example: https://github.com/cloudwego/eino-ext/blob/main/components/model/openai/examples/structured/structured.go
ChatModelWithFormattedOutput model.BaseChatModel
// ToolCallingChatModel is a model that supports tool calling capabilities.
// When provided with ToolInfo, it will use tool calling to generate the Plan structure.
ToolCallingChatModel model.ToolCallingChatModel
// ToolInfo defines the schema for the Plan structure when using tool calling.
// Optional. If not provided, PlanToolInfo will be used as the default.
ToolInfo *schema.ToolInfo
// GenInputFn is a function that generates the input messages for the planner.
// Optional. If not provided, defaultGenPlannerInputFn will be used.
GenInputFn GenPlannerModelInputFn
// NewPlan creates a new Plan instance for JSON.
// The returned Plan will be used to unmarshal the model-generated JSON output.
// Optional. If not provided, defaultNewPlan will be used.
NewPlan NewPlan
}
// GenPlannerModelInputFn is a function type that generates input messages for the planner.
type GenPlannerModelInputFn func(ctx context.Context, userInput []adk.Message) ([]adk.Message, error)
func defaultNewPlan(ctx context.Context) Plan {
return &defaultPlan{}
}
func defaultGenPlannerInputFn(ctx context.Context, userInput []adk.Message) ([]adk.Message, error) {
msgs, err := PlannerPrompt.Format(ctx, map[string]any{
"input": userInput,
})
if err != nil {
return nil, err
}
return msgs, nil
}
type planner struct {
toolCall bool
chatModel model.BaseChatModel
genInputFn GenPlannerModelInputFn
newPlan NewPlan
}
func (p *planner) Name(_ context.Context) string {
return "planner"
}
func (p *planner) Description(_ context.Context) string {
return "a planner agent"
}
func argToContent(msg adk.Message) (adk.Message, error) {
if len(msg.ToolCalls) == 0 {
return nil, schema.ErrNoValue
}
return schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil), nil
}
func (p *planner) Run(ctx context.Context, input *adk.AgentInput,
_ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {
iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
adk.AddSessionValue(ctx, UserInputSessionKey, input.Messages)
go func() {
defer func() {
panicErr := recover()
if panicErr != nil {
e := safe.NewPanicErr(panicErr, debug.Stack())
generator.Send(&adk.AgentEvent{Err: e})
}
generator.Close()
}()
c := compose.NewChain[*adk.AgentInput, Plan]().
AppendLambda(
compose.InvokableLambda(func(ctx context.Context, input *adk.AgentInput) (output []adk.Message, err error) {
return p.genInputFn(ctx, input.Messages)
}),
).
AppendChatModel(p.chatModel).
AppendLambda(
compose.CollectableLambda(func(ctx context.Context, sr *schema.StreamReader[adk.Message]) (adk.Message, error) {
if input.EnableStreaming {
ss := sr.Copy(2)
var sOutput *schema.StreamReader[*schema.Message]
if p.toolCall {
sOutput = schema.StreamReaderWithConvert(ss[0], argToContent)
} else {
sOutput = ss[0]
}
generator.Send(adk.EventFromMessage(nil, sOutput, schema.Assistant, ""))
return schema.ConcatMessageStream(ss[1])
}
msg, err := schema.ConcatMessageStream(sr)
if err != nil {
return nil, err
}
var output adk.Message
if p.toolCall {
if len(msg.ToolCalls) == 0 {
return nil, fmt.Errorf("no tool call")
}
output = schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil)
} else {
output = msg
}
generator.Send(adk.EventFromMessage(output, nil, schema.Assistant, ""))
return msg, nil
}),
).
AppendLambda(
compose.InvokableLambda(func(ctx context.Context, msg adk.Message) (plan Plan, err error) {
var planJSON string
if p.toolCall {
if len(msg.ToolCalls) == 0 {
return nil, fmt.Errorf("no tool call")
}
planJSON = msg.ToolCalls[0].Function.Arguments
} else {
planJSON = msg.Content
}
plan = p.newPlan(ctx)
err = plan.UnmarshalJSON([]byte(planJSON))
if err != nil {
return nil, fmt.Errorf("unmarshal plan error: %w", err)
}
adk.AddSessionValue(ctx, PlanSessionKey, plan)
return plan, nil
}),
)
var opts []compose.Option
if p.toolCall {
opts = append(opts, compose.WithChatModelOption(model.WithToolChoice(schema.ToolChoiceForced)))
}
r, err := c.Compile(ctx, compose.WithGraphName(p.Name(ctx)))
if err != nil { // unexpected
generator.Send(&adk.AgentEvent{Err: err})
return
}
_, err = r.Stream(ctx, input, opts...)
if err != nil {
generator.Send(&adk.AgentEvent{Err: err})
return
}
}()
return iterator
}
// NewPlanner creates a new planner agent based on the provided configuration.
// The planner agent uses either ChatModelWithFormattedOutput or ToolCallingChatModel+ToolInfo
// to generate structured Plan output.
//
// If ChatModelWithFormattedOutput is provided, it will be used directly.
// If ToolCallingChatModel is provided, it will be configured with ToolInfo (or PlanToolInfo by default)
// to generate structured Plan output.
func NewPlanner(_ context.Context, cfg *PlannerConfig) (adk.Agent, error) {
var chatModel model.BaseChatModel
var toolCall bool
if cfg.ChatModelWithFormattedOutput != nil {
chatModel = cfg.ChatModelWithFormattedOutput
} else {
toolCall = true
toolInfo := cfg.ToolInfo
if toolInfo == nil {
toolInfo = &PlanToolInfo
}
var err error
chatModel, err = cfg.ToolCallingChatModel.WithTools([]*schema.ToolInfo{toolInfo})
if err != nil {
return nil, err
}
}
inputFn := cfg.GenInputFn
if inputFn == nil {
inputFn = defaultGenPlannerInputFn
}
planParser := cfg.NewPlan
if planParser == nil {
planParser = defaultNewPlan
}
return &planner{
toolCall: toolCall,
chatModel: chatModel,
genInputFn: inputFn,
newPlan: planParser,
}, nil
}
// ExecutionContext is the input information for the executor and the planner.
type ExecutionContext struct {
UserInput []adk.Message
Plan Plan
ExecutedSteps []ExecutedStep
}
// GenModelInputFn is a function that generates the input messages for the executor and the planner.
type GenModelInputFn func(ctx context.Context, in *ExecutionContext) ([]adk.Message, error)
// ExecutorConfig provides configuration options for creating an executor agent.
type ExecutorConfig struct {
// Model is the chat model used by the executor.
// If the executor uses any tools, this model must support the model.WithTools call option,
// as that's how the executor configures the model with tool information.
Model model.BaseChatModel
// ToolsConfig specifies the tools available to the executor.
ToolsConfig adk.ToolsConfig
// MaxIterations defines the upper limit of ChatModel generation cycles.
// The agent will terminate with an error if this limit is exceeded.
// Optional. Defaults to 20.
MaxIterations int
// GenInputFn generates the input messages for the Executor.
// Optional. If not provided, defaultGenExecutorInputFn will be used.
GenInputFn GenModelInputFn
}
type ExecutedStep struct {
Step string
Result string
}
// NewExecutor creates a new executor agent.
func NewExecutor(ctx context.Context, cfg *ExecutorConfig) (adk.Agent, error) {
genInputFn := cfg.GenInputFn
if genInputFn == nil {
genInputFn = defaultGenExecutorInputFn
}
genInput := func(ctx context.Context, instruction string, _ *adk.AgentInput) ([]adk.Message, error) {
plan, ok := adk.GetSessionValue(ctx, PlanSessionKey)
if !ok {
panic("impossible: plan not found")
}
plan_ := plan.(Plan)
userInput, ok := adk.GetSessionValue(ctx, UserInputSessionKey)
if !ok {
panic("impossible: user input not found")
}
userInput_ := userInput.([]adk.Message)
var executedSteps_ []ExecutedStep
executedStep, ok := adk.GetSessionValue(ctx, ExecutedStepsSessionKey)
if ok {
executedSteps_ = executedStep.([]ExecutedStep)
}
in := &ExecutionContext{
UserInput: userInput_,
Plan: plan_,
ExecutedSteps: executedSteps_,
}
msgs, err := genInputFn(ctx, in)
if err != nil {
return nil, err
}
return msgs, nil
}
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "executor",
Description: "an executor agent",
Model: cfg.Model,
ToolsConfig: cfg.ToolsConfig,
GenModelInput: genInput,
MaxIterations: cfg.MaxIterations,
OutputKey: ExecutedStepSessionKey,
})
if err != nil {
return nil, err
}
return agent, nil
}
func defaultGenExecutorInputFn(ctx context.Context, in *ExecutionContext) ([]adk.Message, error) {
planContent, err := in.Plan.MarshalJSON()
if err != nil {
return nil, err
}
userMsgs, err := ExecutorPrompt.Format(ctx, map[string]any{
"input": formatInput(in.UserInput),
"plan": string(planContent),
"executed_steps": formatExecutedSteps(in.ExecutedSteps),
"step": in.Plan.FirstStep(),
})
if err != nil {
return nil, err
}
return userMsgs, nil
}
type replanner struct {
chatModel model.ToolCallingChatModel
planTool *schema.ToolInfo
respondTool *schema.ToolInfo
genInputFn GenModelInputFn
newPlan NewPlan
}
type ReplannerConfig struct {
// ChatModel is the model that supports tool calling capabilities.
// It will be configured with PlanTool and RespondTool to generate updated plans or responses.
ChatModel model.ToolCallingChatModel
// PlanTool defines the schema for the Plan tool that can be used with ToolCallingChatModel.
// Optional. If not provided, the default PlanToolInfo will be used.
PlanTool *schema.ToolInfo
// RespondTool defines the schema for the response tool that can be used with ToolCallingChatModel.
// Optional. If not provided, the default RespondToolInfo will be used.
RespondTool *schema.ToolInfo
// GenInputFn generates the input messages for the Replanner.
// Optional. If not provided, buildGenReplannerInputFn will be used.
GenInputFn GenModelInputFn
// NewPlan creates a new Plan instance.
// The returned Plan will be used to unmarshal the model-generated JSON output from PlanTool.
// Optional. If not provided, defaultNewPlan will be used.
NewPlan NewPlan
}
// formatInput formats the input messages into a string.
func formatInput(input []adk.Message) string {
var sb strings.Builder
for _, msg := range input {
sb.WriteString(msg.Content)
sb.WriteString("\n")
}
return sb.String()
}
func formatExecutedSteps(results []ExecutedStep) string {
var sb strings.Builder
for _, result := range results {
sb.WriteString(fmt.Sprintf("Step: %s\nResult: %s\n\n", result.Step, result.Result))
}
return sb.String()
}
func (r *replanner) Name(_ context.Context) string {
return "replanner"
}
func (r *replanner) Description(_ context.Context) string {
return "a replanner agent"
}
func (r *replanner) genInput(ctx context.Context) ([]adk.Message, error) {
executedStep, ok := adk.GetSessionValue(ctx, ExecutedStepSessionKey)
if !ok {
panic("impossible: execute result not found")
}
executedStep_ := executedStep.(string)
plan, ok := adk.GetSessionValue(ctx, PlanSessionKey)
if !ok {
panic("impossible: plan not found")
}
plan_ := plan.(Plan)
step := plan_.FirstStep()
var executedSteps_ []ExecutedStep
executedSteps, ok := adk.GetSessionValue(ctx, ExecutedStepsSessionKey)
if ok {
executedSteps_ = executedSteps.([]ExecutedStep)
}
executedSteps_ = append(executedSteps_, ExecutedStep{
Step: step,
Result: executedStep_,
})
adk.AddSessionValue(ctx, ExecutedStepsSessionKey, executedSteps_)
userInput, ok := adk.GetSessionValue(ctx, UserInputSessionKey)
if !ok {
panic("impossible: user input not found")
}
userInput_ := userInput.([]adk.Message)
in := &ExecutionContext{
UserInput: userInput_,
Plan: plan_,
ExecutedSteps: executedSteps_,
}
genInputFn := r.genInputFn
if genInputFn == nil {
genInputFn = buildGenReplannerInputFn(r.planTool.Name, r.respondTool.Name)
}
msgs, err := genInputFn(ctx, in)
if err != nil {
return nil, err
}
return msgs, nil
}
func (r *replanner) Run(ctx context.Context, input *adk.AgentInput, _ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {
iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
go func() {
defer func() {
panicErr := recover()
if panicErr != nil {
e := safe.NewPanicErr(panicErr, debug.Stack())
generator.Send(&adk.AgentEvent{Err: e})
}
generator.Close()
}()
callOpt := model.WithToolChoice(schema.ToolChoiceForced)
c := compose.NewChain[struct{}, any]().
AppendLambda(
compose.InvokableLambda(func(ctx context.Context, input struct{}) (output []adk.Message, err error) {
return r.genInput(ctx)
}),
).
AppendChatModel(r.chatModel).
AppendLambda(
compose.CollectableLambda(func(ctx context.Context, sr *schema.StreamReader[adk.Message]) (adk.Message, error) {
if input.EnableStreaming {
ss := sr.Copy(2)
sOutput := schema.StreamReaderWithConvert(ss[0], argToContent)
generator.Send(adk.EventFromMessage(nil, sOutput, schema.Assistant, ""))
return schema.ConcatMessageStream(ss[1])
}
msg, err := schema.ConcatMessageStream(sr)
if err != nil {
return nil, err
}
if len(msg.ToolCalls) > 0 {
output := schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil)
generator.Send(adk.EventFromMessage(output, nil, schema.Assistant, ""))
}
return msg, nil
}),
).
AppendLambda(
compose.InvokableLambda(func(ctx context.Context, msg adk.Message) (msgOrPlan any, err error) {
if len(msg.ToolCalls) == 0 {
return nil, fmt.Errorf("no tool call")
}
// exit
if msg.ToolCalls[0].Function.Name == r.respondTool.Name {
action := adk.NewBreakLoopAction(r.Name(ctx))
generator.Send(&adk.AgentEvent{Action: action})
return msg, nil
}
// replan
if msg.ToolCalls[0].Function.Name != r.planTool.Name {
return nil, fmt.Errorf("unexpected tool call: %s", msg.ToolCalls[0].Function.Name)
}
plan := r.newPlan(ctx)
if err = plan.UnmarshalJSON([]byte(msg.ToolCalls[0].Function.Arguments)); err != nil {
return nil, fmt.Errorf("unmarshal plan error: %w", err)
}
adk.AddSessionValue(ctx, PlanSessionKey, plan)
return plan, nil
}),
)
runnable, err := c.Compile(ctx, compose.WithGraphName(r.Name(ctx)))
if err != nil {
generator.Send(&adk.AgentEvent{Err: err})
return
}
_, err = runnable.Stream(ctx, struct{}{}, compose.WithChatModelOption(callOpt))
if err != nil {
generator.Send(&adk.AgentEvent{Err: err})
return
}
}()
return iterator
}
func buildGenReplannerInputFn(planToolName, respondToolName string) GenModelInputFn {
return func(ctx context.Context, in *ExecutionContext) ([]adk.Message, error) {
planContent, err := in.Plan.MarshalJSON()
if err != nil {
return nil, err
}
msgs, err := ReplannerPrompt.Format(ctx, map[string]any{
"plan": string(planContent),
"input": formatInput(in.UserInput),
"executed_steps": formatExecutedSteps(in.ExecutedSteps),
"plan_tool": planToolName,
"respond_tool": respondToolName,
})
if err != nil {
return nil, err
}
return msgs, nil
}
}
// NewReplanner creates a plan-execute-replan agent wired with plan and respond tools.
// It configures the provided ToolCallingChatModel with the tools and returns an Agent.
func NewReplanner(_ context.Context, cfg *ReplannerConfig) (adk.Agent, error) {
planTool := cfg.PlanTool
if planTool == nil {
planTool = &PlanToolInfo
}
respondTool := cfg.RespondTool
if respondTool == nil {
respondTool = &RespondToolInfo
}
chatModel, err := cfg.ChatModel.WithTools([]*schema.ToolInfo{planTool, respondTool})
if err != nil {
return nil, err
}
planParser := cfg.NewPlan
if planParser == nil {
planParser = defaultNewPlan
}
return &replanner{
chatModel: chatModel,
planTool: planTool,
respondTool: respondTool,
genInputFn: cfg.GenInputFn,
newPlan: planParser,
}, nil
}
// Config provides configuration options for creating a plan-execute-replan agent.
type Config struct {
// Planner specifies the agent that generates the plan.
// You can use provided NewPlanner to create a planner agent.
Planner adk.Agent
// Executor specifies the agent that executes the plan generated by planner or replanner.
// You can use provided NewExecutor to create an executor agent.
Executor adk.Agent
// Replanner specifies the agent that replans the plan.
// You can use provided NewReplanner to create a replanner agent.
Replanner adk.Agent
// MaxIterations defines the maximum number of loops for 'execute-replan'.
// Optional. If not provided, 10 will be used as the default.
MaxIterations int
}
// New creates a new plan-execute-replan agent with the given configuration.
// The plan-execute-replan pattern works in three phases:
// 1. Planning: Generate a structured plan with clear, actionable steps
// 2. Execution: Execute the first step of the plan
// 3. Replanning: Evaluate progress and either complete the task or revise the plan
// This approach enables complex problem-solving through iterative refinement.
func New(ctx context.Context, cfg *Config) (adk.ResumableAgent, error) {
maxIterations := cfg.MaxIterations
if maxIterations <= 0 {
maxIterations = 10
}
loop, err := adk.NewLoopAgent(ctx, &adk.LoopAgentConfig{
Name: "execute_replan",
SubAgents: []adk.Agent{cfg.Executor, cfg.Replanner},
MaxIterations: maxIterations,
})
if err != nil {
return nil, err
}
return adk.NewSequentialAgent(ctx, &adk.SequentialAgentConfig{
Name: "plan_execute_replan",
SubAgents: []adk.Agent{cfg.Planner, loop},
})
}