Files
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

113 lines
4.6 KiB
Go

package tools
import (
"context"
"log/slog"
sdk "github.com/memohai/twilight-ai/sdk"
"github.com/memohai/memoh/internal/messaging"
)
type MessageProvider struct {
exec *messaging.Executor
}
func NewMessageProvider(log *slog.Logger, sender messaging.Sender, reactor messaging.Reactor, resolver messaging.ChannelTypeResolver, assetResolver messaging.AssetResolver) *MessageProvider {
if log == nil {
log = slog.Default()
}
return &MessageProvider{
exec: &messaging.Executor{
Sender: sender,
Reactor: reactor,
Resolver: resolver,
AssetResolver: assetResolver,
Logger: log.With(slog.String("tool", "message")),
},
}
}
func (p *MessageProvider) Tools(_ context.Context, session SessionContext) ([]sdk.Tool, error) {
if session.IsSubagent {
return nil, nil
}
var tools []sdk.Tool
sess := session
if p.exec.CanSend() {
tools = append(tools, sdk.Tool{
Name: "send",
Description: "Send a message to a DIFFERENT channel or person — NOT for replying to the current conversation. Use this only for cross-channel messaging or forwarding.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"bot_id": map[string]any{"type": "string", "description": "Bot ID, optional and defaults to current bot"},
"platform": map[string]any{"type": "string", "description": "Channel platform name"},
"target": map[string]any{"type": "string", "description": "Channel target (chat/group/thread ID). Use get_contacts to find available targets."},
"text": map[string]any{"type": "string", "description": "Message text shortcut when message object is omitted"},
"reply_to": map[string]any{"type": "string", "description": "Message ID to reply to. The reply will reference this message on the platform."},
"attachments": map[string]any{"type": "array", "description": "File paths or URLs to attach.", "items": map[string]any{"type": "string"}},
"message": map[string]any{"type": "object", "description": "Structured message payload with text/parts/attachments"},
},
"required": []string{},
},
Execute: func(ctx *sdk.ToolExecContext, input any) (any, error) {
return p.execSend(ctx.Context, sess, inputAsMap(input))
},
})
}
if p.exec.CanReact() {
tools = append(tools, sdk.Tool{
Name: "react",
Description: "Add or remove an emoji reaction on a channel message",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"bot_id": map[string]any{"type": "string", "description": "Bot ID, optional and defaults to current bot"},
"platform": map[string]any{"type": "string", "description": "Channel platform name. Defaults to current session platform."},
"target": map[string]any{"type": "string", "description": "Channel target (chat/group ID). Defaults to current session reply target."},
"message_id": map[string]any{"type": "string", "description": "The message ID to react to"},
"emoji": map[string]any{"type": "string", "description": "Emoji to react with (e.g. 👍, ❤️). Required when adding a reaction."},
"remove": map[string]any{"type": "boolean", "description": "If true, remove the reaction instead of adding it. Default false."},
},
"required": []string{"message_id"},
},
Execute: func(ctx *sdk.ToolExecContext, input any) (any, error) {
return p.execReact(ctx.Context, sess, inputAsMap(input))
},
})
}
return tools, nil
}
func (p *MessageProvider) execSend(ctx context.Context, session SessionContext, args map[string]any) (any, error) {
result, err := p.exec.Send(ctx, toMessagingSession(session), args)
if err != nil {
return nil, err
}
return map[string]any{
"ok": true, "bot_id": result.BotID, "platform": result.Platform, "target": result.Target,
"instruction": "Message delivered successfully. You have completed your response. Please STOP now and do not call any more tools.",
}, nil
}
func (p *MessageProvider) execReact(ctx context.Context, session SessionContext, args map[string]any) (any, error) {
result, err := p.exec.React(ctx, toMessagingSession(session), args)
if err != nil {
return nil, err
}
return map[string]any{
"ok": true, "bot_id": result.BotID, "platform": result.Platform,
"target": result.Target, "message_id": result.MessageID, "emoji": result.Emoji, "action": result.Action,
}, nil
}
func toMessagingSession(s SessionContext) messaging.SessionContext {
return messaging.SessionContext{
BotID: s.BotID,
ChatID: s.ChatID,
CurrentPlatform: s.CurrentPlatform,
ReplyTarget: s.ReplyTarget,
}
}