Files
Memoh/internal/agent/tools/schedule.go
T
Acbox Liu b3a39ad93d refactor: replace persistent subagents with ephemeral spawn tool (#280)
* refactor: replace persistent subagents with ephemeral spawn tool (#subagent)

- Drop subagents table, remove all persistent subagent infrastructure
- Add 'subagent' session type with parent_session_id on bot_sessions
- Rewrite subagent tool as single 'spawn' tool with parallel execution
- Create system_subagent.md prompt, add _subagent.md include for chat
- Limit subagent tools to file, exec, web_search, web_fetch only
- Merge subagent token usage into parent chat session in reporting
- Remove frontend subagent management page, update chat UI for spawn
- Fix UTF-8 truncation in session title, fix query not passed to agent

* refactor: remove history message page
2026-03-22 19:03:28 +08:00

247 lines
7.1 KiB
Go

package tools
import (
"context"
"errors"
"log/slog"
"strings"
sdk "github.com/memohai/twilight-ai/sdk"
sched "github.com/memohai/memoh/internal/schedule"
)
type ScheduleProvider struct {
service Scheduler
logger *slog.Logger
}
// Scheduler is the interface for schedule CRUD operations.
type Scheduler interface {
List(ctx context.Context, botID string) ([]sched.Schedule, error)
Get(ctx context.Context, id string) (sched.Schedule, error)
Create(ctx context.Context, botID string, req sched.CreateRequest) (sched.Schedule, error)
Update(ctx context.Context, id string, req sched.UpdateRequest) (sched.Schedule, error)
Delete(ctx context.Context, id string) error
}
func NewScheduleProvider(log *slog.Logger, service Scheduler) *ScheduleProvider {
if log == nil {
log = slog.Default()
}
return &ScheduleProvider{
service: service,
logger: log.With(slog.String("tool", "schedule")),
}
}
func (p *ScheduleProvider) Tools(_ context.Context, session SessionContext) ([]sdk.Tool, error) {
if session.IsSubagent || p.service == nil {
return nil, nil
}
sess := session
return []sdk.Tool{
{
Name: "list_schedule", Description: "List schedules for current bot",
Parameters: emptyObjectSchema(),
Execute: func(ctx *sdk.ToolExecContext, _ any) (any, error) {
botID := strings.TrimSpace(sess.BotID)
if botID == "" {
return nil, errors.New("bot_id is required")
}
items, err := p.service.List(ctx.Context, botID)
if err != nil {
return nil, err
}
return map[string]any{"items": items}, nil
},
},
{
Name: "get_schedule", Description: "Get a schedule by id",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"id": map[string]any{"type": "string", "description": "Schedule ID"},
},
"required": []string{"id"},
},
Execute: func(ctx *sdk.ToolExecContext, input any) (any, error) {
args := inputAsMap(input)
botID := strings.TrimSpace(sess.BotID)
if botID == "" {
return nil, errors.New("bot_id is required")
}
id := StringArg(args, "id")
if id == "" {
return nil, errors.New("id is required")
}
item, err := p.service.Get(ctx.Context, id)
if err != nil {
return nil, err
}
if item.BotID != botID {
return nil, errors.New("bot mismatch")
}
return item, nil
},
},
{
Name: "create_schedule", Description: "Create a new schedule",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"}, "description": map[string]any{"type": "string"},
"pattern": map[string]any{"type": "string"}, "command": map[string]any{"type": "string"},
"max_calls": map[string]any{"type": []string{"integer", "null"}, "description": "Optional max calls, null means unlimited"},
"enabled": map[string]any{"type": "boolean"},
},
"required": []string{"name", "description", "pattern", "command"},
},
Execute: func(ctx *sdk.ToolExecContext, input any) (any, error) {
args := inputAsMap(input)
botID := strings.TrimSpace(sess.BotID)
if botID == "" {
return nil, errors.New("bot_id is required")
}
name := StringArg(args, "name")
description := StringArg(args, "description")
pattern := StringArg(args, "pattern")
command := StringArg(args, "command")
if name == "" || description == "" || pattern == "" || command == "" {
return nil, errors.New("name, description, pattern, command are required")
}
req := sched.CreateRequest{Name: name, Description: description, Pattern: pattern, Command: command}
maxCalls, err := parseNullableIntArg(args, "max_calls")
if err != nil {
return nil, err
}
req.MaxCalls = maxCalls
if enabled, ok, err := BoolArg(args, "enabled"); err != nil {
return nil, err
} else if ok {
req.Enabled = &enabled
}
item, err := p.service.Create(ctx.Context, botID, req)
if err != nil {
return nil, err
}
return item, nil
},
},
{
Name: "update_schedule", Description: "Update an existing schedule",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"id": map[string]any{"type": "string"}, "name": map[string]any{"type": "string"},
"description": map[string]any{"type": "string"}, "pattern": map[string]any{"type": "string"},
"command": map[string]any{"type": "string"},
"max_calls": map[string]any{"type": []string{"integer", "null"}},
"enabled": map[string]any{"type": "boolean"},
},
"required": []string{"id"},
},
Execute: func(ctx *sdk.ToolExecContext, input any) (any, error) {
args := inputAsMap(input)
botID := strings.TrimSpace(sess.BotID)
if botID == "" {
return nil, errors.New("bot_id is required")
}
id := StringArg(args, "id")
if id == "" {
return nil, errors.New("id is required")
}
req := sched.UpdateRequest{}
maxCalls, err := parseNullableIntArg(args, "max_calls")
if err != nil {
return nil, err
}
req.MaxCalls = maxCalls
if v := StringArg(args, "name"); v != "" {
req.Name = &v
}
if v := StringArg(args, "description"); v != "" {
req.Description = &v
}
if v := StringArg(args, "pattern"); v != "" {
req.Pattern = &v
}
if v := StringArg(args, "command"); v != "" {
req.Command = &v
}
if enabled, ok, err := BoolArg(args, "enabled"); err != nil {
return nil, err
} else if ok {
req.Enabled = &enabled
}
item, err := p.service.Update(ctx.Context, id, req)
if err != nil {
return nil, err
}
if item.BotID != botID {
return nil, errors.New("bot mismatch")
}
return item, nil
},
},
{
Name: "delete_schedule", Description: "Delete a schedule by id",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"id": map[string]any{"type": "string", "description": "Schedule ID"},
},
"required": []string{"id"},
},
Execute: func(ctx *sdk.ToolExecContext, input any) (any, error) {
args := inputAsMap(input)
botID := strings.TrimSpace(sess.BotID)
if botID == "" {
return nil, errors.New("bot_id is required")
}
id := StringArg(args, "id")
if id == "" {
return nil, errors.New("id is required")
}
item, err := p.service.Get(ctx.Context, id)
if err != nil {
return nil, err
}
if item.BotID != botID {
return nil, errors.New("bot mismatch")
}
if err := p.service.Delete(ctx.Context, id); err != nil {
return nil, err
}
return map[string]any{"success": true}, nil
},
},
}, nil
}
func parseNullableIntArg(arguments map[string]any, key string) (sched.NullableInt, error) {
req := sched.NullableInt{}
if arguments == nil {
return req, nil
}
raw, exists := arguments[key]
if !exists {
return req, nil
}
req.Set = true
if raw == nil {
req.Value = nil
return req, nil
}
value, _, err := IntArg(arguments, key)
if err != nil {
return sched.NullableInt{}, err
}
req.Value = &value
return req, nil
}
func emptyObjectSchema() map[string]any {
return map[string]any{"type": "object", "properties": map[string]any{}}
}