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
66 changes: 66 additions & 0 deletions agent/tools/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tools

import (
"fmt"
"strings"

"github.com/priyanshujain/openbotkit/internal/skills"
)

// BuildBaseSystemPrompt generates the shared portion of the system prompt
// from the given registry. The tool list is derived from the registry
// so it can never drift out of sync.
//
// Callers should prepend their own identity line (e.g. "You are...via Telegram")
// and append any site-specific sections (e.g. user memories).
func BuildBaseSystemPrompt(reg *Registry) string {
var b strings.Builder

// Tool list — auto-generated from registry.
b.WriteString("\n## Tools\n")
b.WriteString("Available: ")
b.WriteString(strings.Join(reg.ToolNames(), ", "))
b.WriteString(".\n")
b.WriteString("Tool names are case-sensitive. Call tools exactly as listed.\n")

// Tool usage rules.
b.WriteString(`
Rules:
- ALWAYS use tools to perform actions. Never say you will do something without calling the tool.
- Never predict or claim results before receiving them. Wait for tool output.
- Do not narrate routine tool calls — just call the tool. Only explain when the step is non-obvious or the user asked for details.
- If a tool call fails, analyze the error before retrying with a different approach.
`)

// Sub-agents section — only if the subagent tool is registered.
if reg.Has("subagent") {
b.WriteString(`
## Sub-agents
Use the subagent tool to delegate self-contained sub-tasks that don't need your conversation history.
Good uses: independent research, file operations, or multi-step tasks that can run in isolation.
Do not use subagent for simple single-tool calls — just call the tool directly.
The sub-agent has its own tools (bash, file ops, skills) but cannot spawn further sub-agents.
`)
}

// Skills section.
b.WriteString(`
## Skills
Before replying to domain-specific requests (email, WhatsApp, memories, notes, etc.):
1. Scan the "Available skills" list below for matching skill names
2. Use load_skills to read the skill's instructions
3. Use bash to run the commands from those instructions
4. If the request spans multiple domains, load and use ALL relevant skills
5. If no skill matches, use search_skills to discover one by keyword
`)

idx, err := skills.LoadIndex()
if err == nil && len(idx.Skills) > 0 {
b.WriteString("\nAvailable skills:\n")
for _, s := range idx.Skills {
fmt.Fprintf(&b, "- %s: %s\n", s.Name, s.Description)
}
}

return b.String()
}
31 changes: 31 additions & 0 deletions agent/tools/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"sort"
"time"

"github.com/priyanshujain/openbotkit/provider"
)
Expand Down Expand Up @@ -31,6 +33,35 @@ func (r *Registry) Register(t Tool) {
r.tools[t.Name()] = t
}

// NewStandardRegistry creates a registry with the standard tool set
// (bash, file_read, file_write, file_edit, load_skills, search_skills).
func NewStandardRegistry() *Registry {
r := NewRegistry()
r.Register(NewBashTool(30 * time.Second))
r.Register(&FileReadTool{})
r.Register(&FileWriteTool{})
r.Register(&FileEditTool{})
r.Register(&LoadSkillsTool{})
r.Register(&SearchSkillsTool{})
return r
}

// Has returns true if a tool with the given name is registered.
func (r *Registry) Has(name string) bool {
_, ok := r.tools[name]
return ok
}

// ToolNames returns sorted tool names registered in the registry.
func (r *Registry) ToolNames() []string {
names := make([]string, 0, len(r.tools))
for name := range r.tools {
names = append(names, name)
}
sort.Strings(names)
return names
}

const maxOutputBytes = 524288 // 512KB

// Execute implements agent.ToolExecutor.
Expand Down
83 changes: 83 additions & 0 deletions agent/tools/subagent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package tools

import (
"context"
"encoding/json"
"fmt"

"github.com/priyanshujain/openbotkit/agent"
"github.com/priyanshujain/openbotkit/provider"
)

const defaultChildMaxIter = 10

// SubagentTool delegates a task to a child agent that runs synchronously.
type SubagentTool struct {
provider provider.Provider
model string
toolFactory func() *Registry
system string
maxIter int
}

// SubagentConfig configures a SubagentTool.
type SubagentConfig struct {
Provider provider.Provider
Model string
ToolFactory func() *Registry
System string
MaxIter int // 0 defaults to 10
}

func NewSubagentTool(cfg SubagentConfig) *SubagentTool {
maxIter := cfg.MaxIter
if maxIter == 0 {
maxIter = defaultChildMaxIter
}
return &SubagentTool{
provider: cfg.Provider,
model: cfg.Model,
toolFactory: cfg.ToolFactory,
system: cfg.System,
maxIter: maxIter,
}
}

func (s *SubagentTool) Name() string { return "subagent" }
func (s *SubagentTool) Description() string {
return "Delegate a self-contained task to a sub-agent that runs independently with its own context"
}
func (s *SubagentTool) InputSchema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"task": {
"type": "string",
"description": "The task to delegate to the sub-agent"
}
},
"required": ["task"]
}`)
}

type subagentInput struct {
Task string `json:"task"`
}

func (s *SubagentTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var in subagentInput
if err := json.Unmarshal(input, &in); err != nil {
return "", fmt.Errorf("parse input: %w", err)
}
if in.Task == "" {
return "", fmt.Errorf("task is required")
}

childReg := s.toolFactory()
child := agent.New(
s.provider, s.model, childReg,
agent.WithSystem(s.system),
agent.WithMaxIterations(s.maxIter),
)
return child.Run(ctx, in.Task)
}
Loading