Files
2026-07-13 12:49:10 +08:00

881 lines
27 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 planexecutereplan 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},
})
}