diff --git a/core/agent/options.go b/core/agent/options.go
index 31cfd2d..f2f09fe 100644
--- a/core/agent/options.go
+++ b/core/agent/options.go
@@ -71,8 +71,10 @@ type options struct {
prompts []DynamicPrompt
- systemPrompt string
+ systemPrompt string
innerMonologueTemplate string
+ skillPromptTemplate string
+ schedulerTaskTemplate string
// callbacks
reasoningCallback func(types.ActionCurrentState) bool
@@ -319,6 +321,14 @@ func WithInnerMonologueTemplate(template string) Option {
}
}
+// WithSkillPromptTemplate sets the template for rendering skills in the prompt. If empty, the default template is used.
+func WithSkillPromptTemplate(template string) Option {
+ return func(o *options) error {
+ o.skillPromptTemplate = template
+ return nil
+ }
+}
+
func WithMCPServers(servers ...MCPServer) Option {
return func(o *options) error {
o.mcpServers = servers
@@ -569,3 +579,12 @@ func WithSchedulerStorePath(path string) Option {
return nil
}
}
+
+// WithSchedulerTaskTemplate sets the prompt used for scheduled/recurring tasks run by the scheduler.
+// If empty, the default inner monologue template is used with the task injected.
+func WithSchedulerTaskTemplate(template string) Option {
+ return func(o *options) error {
+ o.schedulerTaskTemplate = template
+ return nil
+ }
+}
diff --git a/core/agent/scheduler_executor.go b/core/agent/scheduler_executor.go
index 16acff9..8659e97 100644
--- a/core/agent/scheduler_executor.go
+++ b/core/agent/scheduler_executor.go
@@ -15,9 +15,29 @@ type agentSchedulerExecutor struct {
// Execute processes a scheduled task by creating a job for the agent
func (e *agentSchedulerExecutor) Execute(ctx context.Context, agentName string, prompt string) (*scheduler.JobResult, error) {
- // Create a job for the reminder
+ // Render the scheduler task template - if custom template is set, it will include {{.Task}}
+ // If no custom scheduler template is set, fall back to default inner monologue template
+ innerMonologue := fmt.Sprintf("You need to execute the following task, by using the tools available to you. When the task is completed, you need to send a message to the user with send_message tool to inform them that the task is completed: %s", prompt)
+
+ if e.agent.options.schedulerTaskTemplate != "" {
+ tmpl, err := templateBase("taskTemplate", e.agent.options.schedulerTaskTemplate)
+ if err != nil {
+ return nil, fmt.Errorf("failed to render scheduler task template: %w", err)
+ }
+
+ innerMonologue, err = templateExecute(tmpl, struct {
+ Task string
+ }{
+ Task: prompt,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to render scheduler task template: %w", err)
+ }
+ }
+
+ // Create a job for the reminder with the rendered inner monologue
reminderJob := types.NewJob(
- types.WithText(fmt.Sprintf("You need to execute the following task, by using the tools available to you. When the task is completed, you need to send a message to the user with send_message tool to inform them that the task is completed: %s", prompt)),
+ types.WithText(innerMonologue),
types.WithReasoningCallback(e.agent.options.reasoningCallback),
types.WithResultCallback(e.agent.options.resultCallback),
types.WithContext(ctx),
diff --git a/core/state/config.go b/core/state/config.go
index 8420f87..a5f3d07 100644
--- a/core/state/config.go
+++ b/core/state/config.go
@@ -85,6 +85,7 @@ type AgentConfig struct {
IdentityGuidance string `json:"identity_guidance" form:"identity_guidance"`
PeriodicRuns string `json:"periodic_runs" form:"periodic_runs"`
SchedulerPollInterval string `json:"scheduler_poll_interval" form:"scheduler_poll_interval"`
+ SchedulerTaskTemplate string `json:"scheduler_task_template" form:"scheduler_task_template"`
PermanentGoal string `json:"permanent_goal" form:"permanent_goal"`
EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"`
EnableKBCompaction bool `json:"enable_kb_compaction" form:"enable_kb_compaction"`
@@ -352,6 +353,14 @@ func NewAgentConfigMeta(
HelpText: "Prompt used for periodic/standalone runs when the agent evaluates what to do next. If empty, the default autonomous agent instructions are used.",
Tags: config.Tags{Section: "PromptsGoals"},
},
+ {
+ Name: "scheduler_task_template",
+ Label: "Scheduler Task Template",
+ Type: "textarea",
+ DefaultValue: "",
+ HelpText: "Template for scheduled/recurring tasks. Use {{.Task}} to reference the task. Example: \"Execute: {{.Task}}\". If empty, the default inner monologue template is used with the task injected.",
+ Tags: config.Tags{Section: "PromptsGoals"},
+ },
{
Name: "standalone_job",
Label: "Standalone Job",
diff --git a/core/state/pool.go b/core/state/pool.go
index 49c2e1f..0464380 100644
--- a/core/state/pool.go
+++ b/core/state/pool.go
@@ -441,6 +441,7 @@ func (a *AgentPool) startAgentWithConfig(name, pooldir string, config *AgentConf
}),
WithSystemPrompt(config.SystemPrompt),
WithInnerMonologueTemplate(config.InnerMonologueTemplate),
+ WithSchedulerTaskTemplate(config.SchedulerTaskTemplate),
WithMultimodalModel(multimodalModel),
WithLastMessageDuration(config.LastMessageDuration),
WithAgentResultCallback(func(state types.ActionState) {
diff --git a/services/skills/prompt.go b/services/skills/prompt.go
index fb74cf0..298d686 100644
--- a/services/skills/prompt.go
+++ b/services/skills/prompt.go
@@ -1,9 +1,12 @@
package skills
import (
+ "bytes"
"fmt"
"strings"
+ "text/template"
+ "github.com/Masterminds/sprig/v3"
"github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types"
@@ -12,16 +15,34 @@ import (
const defaultSkillsIntro = "You can use the following skills to help with the task.\nTo request the skill, you need to use the `request_skill` tool. The skill name is the name of the skill you want to use.\n"
-// skillsPrompt implements agent.DynamicPrompt and injects the available skills XML block
-type skillsPrompt struct {
- listSkills func() ([]skilldomain.Skill, error)
- customIntro string
+// defaultSkillsTemplate is the default template that mimics the current XML behavior
+const defaultSkillsTemplate = defaultSkillsIntro + `
+{{range .Skills}}
+
+ {{escapeXML .Name}}
+ {{escapeXML .Description}}
+
+{{end}}
+`
+
+// Skill is a local representation of a skill for template rendering
+type Skill struct {
+ Name string
+ Description string
+ ID string
}
-// NewSkillsPrompt returns a DynamicPrompt that renders the list of available skills as XML.
-// If customIntro is non-empty, it is used as the intro before the skills list; otherwise the default intro is used.
-func NewSkillsPrompt(listSkills func() ([]skilldomain.Skill, error), customIntro string) agent.DynamicPrompt {
- return &skillsPrompt{listSkills: listSkills, customIntro: customIntro}
+// skillsPrompt implements agent.DynamicPrompt and injects the available skills XML block
+type skillsPrompt struct {
+ listSkills func() ([]skilldomain.Skill, error)
+ customTemplate string
+}
+
+// NewSkillsPrompt returns a DynamicPrompt that renders the list of available skills.
+// If customTemplate is non-empty, it is used as a template with {{.Skills}} slice.
+// Otherwise, the default template is used (mimics current XML behavior).
+func NewSkillsPrompt(listSkills func() ([]skilldomain.Skill, error), customTemplate string) agent.DynamicPrompt {
+ return &skillsPrompt{listSkills: listSkills, customTemplate: customTemplate}
}
func (p *skillsPrompt) Render(a *agent.Agent) (types.PromptResult, error) {
@@ -29,26 +50,46 @@ func (p *skillsPrompt) Render(a *agent.Agent) (types.PromptResult, error) {
if err != nil {
return types.PromptResult{}, err
}
- var sb strings.Builder
- intro := defaultSkillsIntro
- if p.customIntro != "" {
- intro = strings.TrimSpace(p.customIntro) + "\n"
- }
- sb.WriteString(intro)
- sb.WriteString("\n")
- for _, s := range skills {
- name := s.ID
+
+ // Convert skilldomain.Skill to local Skill type for template rendering
+ localSkills := make([]Skill, len(skills))
+ for i, s := range skills {
desc := ""
if s.Metadata != nil && s.Metadata.Description != "" {
desc = s.Metadata.Description
}
- sb.WriteString(" \n")
- sb.WriteString(fmt.Sprintf(" %s\n", escapeXML(name)))
- sb.WriteString(fmt.Sprintf(" %s\n", escapeXML(desc)))
- sb.WriteString(" \n")
+ localSkills[i] = Skill{
+ Name: s.ID,
+ Description: desc,
+ ID: s.ID,
+ }
}
- sb.WriteString("")
- return types.PromptResult{Content: sb.String()}, nil
+
+ // Use custom template or default
+ templ := p.customTemplate
+ if templ == "" {
+ templ = defaultSkillsTemplate
+ }
+
+ // Parse and execute the template
+ tmpl, err := template.New("skillsPrompt").Funcs(template.FuncMap{
+ "escapeXML": escapeXML,
+ }).Funcs(sprig.FuncMap()).Parse(templ)
+ if err != nil {
+ return types.PromptResult{}, fmt.Errorf("failed to parse skills template: %w", err)
+ }
+
+ var buf bytes.Buffer
+ err = tmpl.Execute(&buf, struct {
+ Skills []Skill
+ }{
+ Skills: localSkills,
+ })
+ if err != nil {
+ return types.PromptResult{}, fmt.Errorf("failed to execute skills template: %w", err)
+ }
+
+ return types.PromptResult{Content: buf.String()}, nil
}
func (p *skillsPrompt) Role() string {
diff --git a/services/skills/service.go b/services/skills/service.go
index 8b2b082..66312db 100644
--- a/services/skills/service.go
+++ b/services/skills/service.go
@@ -135,11 +135,11 @@ func (s *Service) GetSkillsPrompt(config *state.AgentConfig) (agent.DynamicPromp
if err != nil || mgr == nil {
return nil, err
}
- customIntro := ""
+ customTemplate := ""
if config != nil && config.SkillsPrompt != "" {
- customIntro = config.SkillsPrompt
+ customTemplate = config.SkillsPrompt
}
- return NewSkillsPrompt(mgr.ListSkills, customIntro), nil
+ return NewSkillsPrompt(mgr.ListSkills, customTemplate), nil
}
// GetMCPSession returns a shared MCP client session connected to the in-process skillserver (starts on first use)