feat(agentic): add direct workspace messaging
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
3a9834ec03
commit
b0662c282b
6 changed files with 594 additions and 1 deletions
131
pkg/agentic/commands_message.go
Normal file
131
pkg/agentic/commands_message.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import core "dappco.re/go/core"
|
||||
|
||||
func (s *PrepSubsystem) cmdMessageSend(options core.Options) core.Result {
|
||||
workspace := optionStringValue(options, "workspace", "_arg")
|
||||
fromAgent := optionStringValue(options, "from_agent", "from")
|
||||
toAgent := optionStringValue(options, "to_agent", "to")
|
||||
content := optionStringValue(options, "content", "body")
|
||||
if workspace == "" || fromAgent == "" || toAgent == "" || core.Trim(content) == "" {
|
||||
core.Print(nil, "usage: core-agent message send <workspace> --from=codex --to=claude --subject=\"Review\" --content=\"Please check the prompt.\"")
|
||||
return core.Result{Value: core.E("agentic.cmdMessageSend", "workspace, from_agent, to_agent, and content are required", nil), OK: false}
|
||||
}
|
||||
|
||||
result := s.handleMessageSend(s.commandContext(), core.NewOptions(
|
||||
core.Option{Key: "workspace", Value: workspace},
|
||||
core.Option{Key: "from_agent", Value: fromAgent},
|
||||
core.Option{Key: "to_agent", Value: toAgent},
|
||||
core.Option{Key: "subject", Value: optionStringValue(options, "subject")},
|
||||
core.Option{Key: "content", Value: content},
|
||||
))
|
||||
if !result.OK {
|
||||
err := commandResultError("agentic.cmdMessageSend", result)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
output, ok := result.Value.(MessageSendOutput)
|
||||
if !ok {
|
||||
err := core.E("agentic.cmdMessageSend", "invalid message send output", nil)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
core.Print(nil, "sent: %s", output.Message.ID)
|
||||
core.Print(nil, "from: %s", output.Message.FromAgent)
|
||||
core.Print(nil, "to: %s", output.Message.ToAgent)
|
||||
if output.Message.Subject != "" {
|
||||
core.Print(nil, "subject: %s", output.Message.Subject)
|
||||
}
|
||||
core.Print(nil, "content: %s", output.Message.Content)
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) cmdMessageInbox(options core.Options) core.Result {
|
||||
workspace := optionStringValue(options, "workspace", "_arg")
|
||||
agent := optionStringValue(options, "agent", "agent_id", "agent-id")
|
||||
if workspace == "" || agent == "" {
|
||||
core.Print(nil, "usage: core-agent message inbox <workspace> --agent=claude [--limit=50]")
|
||||
return core.Result{Value: core.E("agentic.cmdMessageInbox", "workspace and agent are required", nil), OK: false}
|
||||
}
|
||||
|
||||
result := s.handleMessageInbox(s.commandContext(), core.NewOptions(
|
||||
core.Option{Key: "workspace", Value: workspace},
|
||||
core.Option{Key: "agent", Value: agent},
|
||||
core.Option{Key: "limit", Value: optionIntValue(options, "limit")},
|
||||
))
|
||||
if !result.OK {
|
||||
err := commandResultError("agentic.cmdMessageInbox", result)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
output, ok := result.Value.(MessageListOutput)
|
||||
if !ok {
|
||||
err := core.E("agentic.cmdMessageInbox", "invalid message inbox output", nil)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
if len(output.Messages) == 0 {
|
||||
core.Print(nil, "no messages")
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
core.Print(nil, "count: %d", output.Count)
|
||||
for _, message := range output.Messages {
|
||||
core.Print(nil, " [%s] %s -> %s", message.CreatedAt, message.FromAgent, message.ToAgent)
|
||||
if message.Subject != "" {
|
||||
core.Print(nil, " subject: %s", message.Subject)
|
||||
}
|
||||
core.Print(nil, " %s", message.Content)
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) cmdMessageConversation(options core.Options) core.Result {
|
||||
workspace := optionStringValue(options, "workspace", "_arg")
|
||||
agent := optionStringValue(options, "agent", "agent_id", "agent-id")
|
||||
withAgent := optionStringValue(options, "with_agent", "with-agent", "with", "to_agent", "to-agent")
|
||||
if workspace == "" || agent == "" || withAgent == "" {
|
||||
core.Print(nil, "usage: core-agent message conversation <workspace> --agent=codex --with=claude [--limit=50]")
|
||||
return core.Result{Value: core.E("agentic.cmdMessageConversation", "workspace, agent, and with_agent are required", nil), OK: false}
|
||||
}
|
||||
|
||||
result := s.handleMessageConversation(s.commandContext(), core.NewOptions(
|
||||
core.Option{Key: "workspace", Value: workspace},
|
||||
core.Option{Key: "agent", Value: agent},
|
||||
core.Option{Key: "with_agent", Value: withAgent},
|
||||
core.Option{Key: "limit", Value: optionIntValue(options, "limit")},
|
||||
))
|
||||
if !result.OK {
|
||||
err := commandResultError("agentic.cmdMessageConversation", result)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
output, ok := result.Value.(MessageListOutput)
|
||||
if !ok {
|
||||
err := core.E("agentic.cmdMessageConversation", "invalid message conversation output", nil)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
if len(output.Messages) == 0 {
|
||||
core.Print(nil, "no messages")
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
core.Print(nil, "count: %d", output.Count)
|
||||
for _, message := range output.Messages {
|
||||
core.Print(nil, " [%s] %s -> %s", message.CreatedAt, message.FromAgent, message.ToAgent)
|
||||
if message.Subject != "" {
|
||||
core.Print(nil, " subject: %s", message.Subject)
|
||||
}
|
||||
core.Print(nil, " %s", message.Content)
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
|
@ -13,6 +13,12 @@ func (s *PrepSubsystem) registerPlatformCommands() {
|
|||
c.Command("sync/status", core.Command{Description: "Show platform sync status for the current or named agent", Action: s.cmdSyncStatus})
|
||||
c.Command("auth/provision", core.Command{Description: "Provision a platform API key for an authenticated agent user", Action: s.cmdAuthProvision})
|
||||
c.Command("auth/revoke", core.Command{Description: "Revoke a platform API key", Action: s.cmdAuthRevoke})
|
||||
c.Command("message/send", core.Command{Description: "Send a direct message to another agent", Action: s.cmdMessageSend})
|
||||
c.Command("messages/send", core.Command{Description: "Send a direct message to another agent", Action: s.cmdMessageSend})
|
||||
c.Command("message/inbox", core.Command{Description: "List direct messages for an agent", Action: s.cmdMessageInbox})
|
||||
c.Command("messages/inbox", core.Command{Description: "List direct messages for an agent", Action: s.cmdMessageInbox})
|
||||
c.Command("message/conversation", core.Command{Description: "List a direct conversation between two agents", Action: s.cmdMessageConversation})
|
||||
c.Command("messages/conversation", core.Command{Description: "List a direct conversation between two agents", Action: s.cmdMessageConversation})
|
||||
|
||||
c.Command("fleet/register", core.Command{Description: "Register a fleet node with the platform API", Action: s.cmdFleetRegister})
|
||||
c.Command("fleet/heartbeat", core.Command{Description: "Send a heartbeat for a registered fleet node", Action: s.cmdFleetHeartbeat})
|
||||
|
|
|
|||
336
pkg/agentic/message.go
Normal file
336
pkg/agentic/message.go
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// message := agentic.AgentMessage{Workspace: "core/go-io/task-5", FromAgent: "codex", ToAgent: "claude", Subject: "Review", Content: "Please check the prompt."}
|
||||
type AgentMessage struct {
|
||||
ID string `json:"id"`
|
||||
Workspace string `json:"workspace"`
|
||||
FromAgent string `json:"from_agent"`
|
||||
ToAgent string `json:"to_agent"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Content string `json:"content"`
|
||||
ReadAt string `json:"read_at,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.MessageSendInput{Workspace: "core/go-io/task-5", FromAgent: "codex", ToAgent: "claude", Subject: "Review", Content: "Please check the prompt."}
|
||||
type MessageSendInput struct {
|
||||
Workspace string `json:"workspace"`
|
||||
FromAgent string `json:"from_agent"`
|
||||
ToAgent string `json:"to_agent"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// input := agentic.MessageInboxInput{Workspace: "core/go-io/task-5", Agent: "claude"}
|
||||
type MessageInboxInput struct {
|
||||
Workspace string `json:"workspace"`
|
||||
Agent string `json:"agent"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.MessageConversationInput{Workspace: "core/go-io/task-5", Agent: "codex", WithAgent: "claude"}
|
||||
type MessageConversationInput struct {
|
||||
Workspace string `json:"workspace"`
|
||||
Agent string `json:"agent"`
|
||||
WithAgent string `json:"with_agent"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
// out := agentic.MessageSendOutput{Success: true, Message: agentic.AgentMessage{ID: "msg-1"}}
|
||||
type MessageSendOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Message AgentMessage `json:"message"`
|
||||
}
|
||||
|
||||
// out := agentic.MessageListOutput{Success: true, Count: 1, Messages: []agentic.AgentMessage{{ID: "msg-1"}}}
|
||||
type MessageListOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Count int `json:"count"`
|
||||
Messages []AgentMessage `json:"messages"`
|
||||
}
|
||||
|
||||
// result := c.Action("agentic.message.send").Run(ctx, core.NewOptions(
|
||||
//
|
||||
// core.Option{Key: "workspace", Value: "core/go-io/task-5"},
|
||||
// core.Option{Key: "from_agent", Value: "codex"},
|
||||
// core.Option{Key: "to_agent", Value: "claude"},
|
||||
//
|
||||
// ))
|
||||
func (s *PrepSubsystem) handleMessageSend(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.messageSend(ctx, nil, MessageSendInput{
|
||||
Workspace: optionStringValue(options, "workspace", "_arg"),
|
||||
FromAgent: optionStringValue(options, "from_agent", "from"),
|
||||
ToAgent: optionStringValue(options, "to_agent", "to"),
|
||||
Subject: optionStringValue(options, "subject"),
|
||||
Content: optionStringValue(options, "content", "body"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("agentic.message.inbox").Run(ctx, core.NewOptions(core.Option{Key: "workspace", Value: "core/go-io/task-5"}))
|
||||
func (s *PrepSubsystem) handleMessageInbox(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.messageInbox(ctx, nil, MessageInboxInput{
|
||||
Workspace: optionStringValue(options, "workspace", "_arg"),
|
||||
Agent: optionStringValue(options, "agent", "agent_id", "agent-id"),
|
||||
Limit: optionIntValue(options, "limit"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("agentic.message.conversation").Run(ctx, core.NewOptions(
|
||||
//
|
||||
// core.Option{Key: "workspace", Value: "core/go-io/task-5"},
|
||||
// core.Option{Key: "agent", Value: "codex"},
|
||||
// core.Option{Key: "with_agent", Value: "claude"},
|
||||
//
|
||||
// ))
|
||||
func (s *PrepSubsystem) handleMessageConversation(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.messageConversation(ctx, nil, MessageConversationInput{
|
||||
Workspace: optionStringValue(options, "workspace", "_arg"),
|
||||
Agent: optionStringValue(options, "agent", "agent_id", "agent-id"),
|
||||
WithAgent: optionStringValue(options, "with_agent", "with-agent", "with", "to_agent", "to-agent"),
|
||||
Limit: optionIntValue(options, "limit"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) registerMessageTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "agentic_message_send",
|
||||
Description: "Send a direct message between two agents within a workspace.",
|
||||
}, s.messageSend)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "agentic_message_inbox",
|
||||
Description: "List messages delivered to an agent within a workspace.",
|
||||
}, s.messageInbox)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "agentic_message_conversation",
|
||||
Description: "List the chronological conversation between two agents within a workspace.",
|
||||
}, s.messageConversation)
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) messageSend(_ context.Context, _ *mcp.CallToolRequest, input MessageSendInput) (*mcp.CallToolResult, MessageSendOutput, error) {
|
||||
message, err := messageStoreSend(input)
|
||||
if err != nil {
|
||||
return nil, MessageSendOutput{}, err
|
||||
}
|
||||
return nil, MessageSendOutput{Success: true, Message: message}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) messageInbox(_ context.Context, _ *mcp.CallToolRequest, input MessageInboxInput) (*mcp.CallToolResult, MessageListOutput, error) {
|
||||
messages, err := messageStoreInbox(input.Workspace, input.Agent, input.Limit)
|
||||
if err != nil {
|
||||
return nil, MessageListOutput{}, err
|
||||
}
|
||||
return nil, MessageListOutput{Success: true, Count: len(messages), Messages: messages}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) messageConversation(_ context.Context, _ *mcp.CallToolRequest, input MessageConversationInput) (*mcp.CallToolResult, MessageListOutput, error) {
|
||||
messages, err := messageStoreConversation(input.Workspace, input.Agent, input.WithAgent, input.Limit)
|
||||
if err != nil {
|
||||
return nil, MessageListOutput{}, err
|
||||
}
|
||||
return nil, MessageListOutput{Success: true, Count: len(messages), Messages: messages}, nil
|
||||
}
|
||||
|
||||
func messageStoreSend(input MessageSendInput) (AgentMessage, error) {
|
||||
if input.Workspace == "" {
|
||||
return AgentMessage{}, core.E("messageSend", "workspace is required", nil)
|
||||
}
|
||||
if input.FromAgent == "" {
|
||||
return AgentMessage{}, core.E("messageSend", "from_agent is required", nil)
|
||||
}
|
||||
if input.ToAgent == "" {
|
||||
return AgentMessage{}, core.E("messageSend", "to_agent is required", nil)
|
||||
}
|
||||
if core.Trim(input.Content) == "" {
|
||||
return AgentMessage{}, core.E("messageSend", "content is required", nil)
|
||||
}
|
||||
|
||||
messages, err := readWorkspaceMessages(input.Workspace)
|
||||
if err != nil {
|
||||
return AgentMessage{}, err
|
||||
}
|
||||
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
message := AgentMessage{
|
||||
ID: messageID(),
|
||||
Workspace: core.Trim(input.Workspace),
|
||||
FromAgent: core.Trim(input.FromAgent),
|
||||
ToAgent: core.Trim(input.ToAgent),
|
||||
Subject: core.Trim(input.Subject),
|
||||
Content: input.Content,
|
||||
CreatedAt: now,
|
||||
}
|
||||
messages = append(messages, message)
|
||||
|
||||
if err := writeWorkspaceMessages(input.Workspace, messages); err != nil {
|
||||
return AgentMessage{}, err
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func messageStoreInbox(workspace, agent string, limit int) ([]AgentMessage, error) {
|
||||
if workspace == "" {
|
||||
return nil, core.E("messageInbox", "workspace is required", nil)
|
||||
}
|
||||
if agent == "" {
|
||||
return nil, core.E("messageInbox", "agent is required", nil)
|
||||
}
|
||||
|
||||
return messageStoreFilter(workspace, limit, func(message AgentMessage) bool {
|
||||
return message.ToAgent == agent
|
||||
})
|
||||
}
|
||||
|
||||
func messageStoreConversation(workspace, agent, withAgent string, limit int) ([]AgentMessage, error) {
|
||||
if workspace == "" {
|
||||
return nil, core.E("messageConversation", "workspace is required", nil)
|
||||
}
|
||||
if agent == "" {
|
||||
return nil, core.E("messageConversation", "agent is required", nil)
|
||||
}
|
||||
if withAgent == "" {
|
||||
return nil, core.E("messageConversation", "with_agent is required", nil)
|
||||
}
|
||||
|
||||
return messageStoreFilter(workspace, limit, func(message AgentMessage) bool {
|
||||
return (message.FromAgent == agent && message.ToAgent == withAgent) || (message.FromAgent == withAgent && message.ToAgent == agent)
|
||||
})
|
||||
}
|
||||
|
||||
func messageStoreFilter(workspace string, limit int, match func(AgentMessage) bool) ([]AgentMessage, error) {
|
||||
messages, err := readWorkspaceMessages(workspace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filtered := make([]AgentMessage, 0, len(messages))
|
||||
for _, message := range messages {
|
||||
message = normaliseAgentMessage(message)
|
||||
if match(message) {
|
||||
filtered = append(filtered, message)
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(filtered, func(i, j int) bool {
|
||||
return filtered[i].CreatedAt < filtered[j].CreatedAt
|
||||
})
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if len(filtered) > limit {
|
||||
filtered = filtered[len(filtered)-limit:]
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func messageRoot() string {
|
||||
return core.JoinPath(CoreRoot(), "messages")
|
||||
}
|
||||
|
||||
func messagePath(workspace string) string {
|
||||
return core.JoinPath(messageRoot(), core.Concat(core.SanitisePath(workspace), ".json"))
|
||||
}
|
||||
|
||||
func readWorkspaceMessages(workspace string) ([]AgentMessage, error) {
|
||||
if workspace == "" {
|
||||
return []AgentMessage{}, nil
|
||||
}
|
||||
|
||||
result := fs.Read(messagePath(workspace))
|
||||
if !result.OK {
|
||||
err, _ := result.Value.(error)
|
||||
if err == nil || core.Contains(err.Error(), "no such file") {
|
||||
return []AgentMessage{}, nil
|
||||
}
|
||||
return nil, core.E("readWorkspaceMessages", "failed to read message store", err)
|
||||
}
|
||||
|
||||
content := core.Trim(result.Value.(string))
|
||||
if content == "" {
|
||||
return []AgentMessage{}, nil
|
||||
}
|
||||
|
||||
var messages []AgentMessage
|
||||
if parseResult := core.JSONUnmarshalString(content, &messages); !parseResult.OK {
|
||||
err, _ := parseResult.Value.(error)
|
||||
return nil, core.E("readWorkspaceMessages", "failed to parse message store", err)
|
||||
}
|
||||
|
||||
for i := range messages {
|
||||
messages[i] = normaliseAgentMessage(messages[i])
|
||||
}
|
||||
|
||||
sort.SliceStable(messages, func(i, j int) bool {
|
||||
return messages[i].CreatedAt < messages[j].CreatedAt
|
||||
})
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func writeWorkspaceMessages(workspace string, messages []AgentMessage) error {
|
||||
if workspace == "" {
|
||||
return core.E("writeWorkspaceMessages", "workspace is required", nil)
|
||||
}
|
||||
|
||||
normalised := make([]AgentMessage, 0, len(messages))
|
||||
for _, message := range messages {
|
||||
normalised = append(normalised, normaliseAgentMessage(message))
|
||||
}
|
||||
|
||||
if ensureDirResult := fs.EnsureDir(messageRoot()); !ensureDirResult.OK {
|
||||
err, _ := ensureDirResult.Value.(error)
|
||||
return core.E("writeWorkspaceMessages", "failed to create message store directory", err)
|
||||
}
|
||||
|
||||
if writeResult := fs.WriteAtomic(messagePath(workspace), core.JSONMarshalString(normalised)); !writeResult.OK {
|
||||
err, _ := writeResult.Value.(error)
|
||||
return core.E("writeWorkspaceMessages", "failed to write message store", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func normaliseAgentMessage(message AgentMessage) AgentMessage {
|
||||
message.Workspace = core.Trim(message.Workspace)
|
||||
message.FromAgent = core.Trim(message.FromAgent)
|
||||
message.ToAgent = core.Trim(message.ToAgent)
|
||||
message.Subject = core.Trim(message.Subject)
|
||||
if message.ID == "" {
|
||||
message.ID = messageID()
|
||||
}
|
||||
if message.CreatedAt == "" {
|
||||
message.CreatedAt = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func messageID() string {
|
||||
return core.Concat("msg-", core.Sprint(time.Now().UnixNano()))
|
||||
}
|
||||
97
pkg/agentic/message_test.go
Normal file
97
pkg/agentic/message_test.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMessage_MessageSend_Good_PersistsAndReadsBack(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
|
||||
result := s.cmdMessageSend(core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "core/go-io/task-5"},
|
||||
core.Option{Key: "from", Value: "codex"},
|
||||
core.Option{Key: "to", Value: "claude"},
|
||||
core.Option{Key: "subject", Value: "Review"},
|
||||
core.Option{Key: "content", Value: "Please check the prompt."},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(MessageSendOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "core/go-io/task-5", output.Message.Workspace)
|
||||
assert.Equal(t, "codex", output.Message.FromAgent)
|
||||
assert.Equal(t, "claude", output.Message.ToAgent)
|
||||
assert.Equal(t, "Review", output.Message.Subject)
|
||||
assert.Equal(t, "Please check the prompt.", output.Message.Content)
|
||||
assert.NotEmpty(t, output.Message.ID)
|
||||
assert.NotEmpty(t, output.Message.CreatedAt)
|
||||
|
||||
messageStorePath := messagePath("core/go-io/task-5")
|
||||
assert.True(t, fs.Exists(messageStorePath))
|
||||
|
||||
inboxResult := s.cmdMessageInbox(core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "core/go-io/task-5"},
|
||||
core.Option{Key: "agent", Value: "claude"},
|
||||
))
|
||||
require.True(t, inboxResult.OK)
|
||||
|
||||
inbox, ok := inboxResult.Value.(MessageListOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 1, inbox.Count)
|
||||
require.Len(t, inbox.Messages, 1)
|
||||
assert.Equal(t, output.Message.ID, inbox.Messages[0].ID)
|
||||
|
||||
conversationResult := s.cmdMessageConversation(core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "core/go-io/task-5"},
|
||||
core.Option{Key: "agent", Value: "codex"},
|
||||
core.Option{Key: "with", Value: "claude"},
|
||||
))
|
||||
require.True(t, conversationResult.OK)
|
||||
|
||||
conversation, ok := conversationResult.Value.(MessageListOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 1, conversation.Count)
|
||||
require.Len(t, conversation.Messages, 1)
|
||||
assert.Equal(t, output.Message.ID, conversation.Messages[0].ID)
|
||||
}
|
||||
|
||||
func TestMessage_MessageSend_Bad_MissingRequiredFields(t *testing.T) {
|
||||
s := newTestPrep(t)
|
||||
|
||||
result := s.cmdMessageSend(core.NewOptions(
|
||||
core.Option{Key: "_arg", Value: "core/go-io/task-5"},
|
||||
core.Option{Key: "from", Value: "codex"},
|
||||
))
|
||||
|
||||
assert.False(t, result.OK)
|
||||
require.Error(t, result.Value.(error))
|
||||
assert.Contains(t, result.Value.(error).Error(), "required")
|
||||
}
|
||||
|
||||
func TestMessage_MessageInbox_Ugly_CorruptStore(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
require.True(t, fs.EnsureDir(messageRoot()).OK)
|
||||
require.True(t, fs.Write(messagePath("core/go-io/task-5"), "{broken json").OK)
|
||||
|
||||
result := s.handleMessageInbox(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "workspace", Value: "core/go-io/task-5"},
|
||||
core.Option{Key: "agent", Value: "claude"},
|
||||
))
|
||||
|
||||
assert.False(t, result.OK)
|
||||
require.Error(t, result.Value.(error))
|
||||
assert.Contains(t, result.Value.(error).Error(), "failed to parse message store")
|
||||
}
|
||||
|
|
@ -94,7 +94,8 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result {
|
|||
"agentic.prompt", "agentic.task", "agentic.flow", "agentic.persona",
|
||||
"agentic.sync.status", "agentic.fleet.nodes", "agentic.fleet.stats", "agentic.fleet.events",
|
||||
"agentic.credits.balance", "agentic.credits.history",
|
||||
"agentic.subscription.detect", "agentic.subscription.budget":
|
||||
"agentic.subscription.detect", "agentic.subscription.budget",
|
||||
"agentic.message.send", "agentic.message.inbox", "agentic.message.conversation":
|
||||
return core.Entitlement{Allowed: true, Unlimited: true}
|
||||
}
|
||||
if s.frozen {
|
||||
|
|
@ -157,6 +158,12 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result {
|
|||
c.Action("agent.subscription.budget", s.handleSubscriptionBudget).Description = "Get the compute budget for a fleet node"
|
||||
c.Action("agentic.subscription.budget.update", s.handleSubscriptionBudgetUpdate).Description = "Update the compute budget for a fleet node"
|
||||
c.Action("agent.subscription.budget.update", s.handleSubscriptionBudgetUpdate).Description = "Update the compute budget for a fleet node"
|
||||
c.Action("agentic.message.send", s.handleMessageSend).Description = "Send a direct message between agents"
|
||||
c.Action("agent.message.send", s.handleMessageSend).Description = "Send a direct message between agents"
|
||||
c.Action("agentic.message.inbox", s.handleMessageInbox).Description = "List direct messages for an agent"
|
||||
c.Action("agent.message.inbox", s.handleMessageInbox).Description = "List direct messages for an agent"
|
||||
c.Action("agentic.message.conversation", s.handleMessageConversation).Description = "List a direct conversation between two agents"
|
||||
c.Action("agent.message.conversation", s.handleMessageConversation).Description = "List a direct conversation between two agents"
|
||||
|
||||
c.Action("agentic.dispatch", s.handleDispatch).Description = "Prep workspace and spawn a subagent"
|
||||
c.Action("agentic.dispatch.sync", s.handleDispatchSync).Description = "Dispatch a single task synchronously and block until it completes"
|
||||
|
|
@ -378,6 +385,7 @@ func (s *PrepSubsystem) RegisterTools(server *mcp.Server) {
|
|||
s.registerTaskTools(server)
|
||||
s.registerTemplateTools(server)
|
||||
s.registerIssueTools(server)
|
||||
s.registerMessageTools(server)
|
||||
s.registerSprintTools(server)
|
||||
s.registerContentTools(server)
|
||||
s.registerLanguageTools(server)
|
||||
|
|
|
|||
|
|
@ -518,6 +518,12 @@ func TestPrep_OnStartup_Good_RegistersSessionActions(t *testing.T) {
|
|||
assert.True(t, c.Action("issue.comment").Exists())
|
||||
assert.True(t, c.Action("issue.report").Exists())
|
||||
assert.True(t, c.Action("issue.archive").Exists())
|
||||
assert.True(t, c.Action("agentic.message.send").Exists())
|
||||
assert.True(t, c.Action("agent.message.send").Exists())
|
||||
assert.True(t, c.Action("agentic.message.inbox").Exists())
|
||||
assert.True(t, c.Action("agent.message.inbox").Exists())
|
||||
assert.True(t, c.Action("agentic.message.conversation").Exists())
|
||||
assert.True(t, c.Action("agent.message.conversation").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.update").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.assign").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.comment").Exists())
|
||||
|
|
@ -606,6 +612,12 @@ func TestPrep_OnStartup_Good_RegistersPlatformCommandAlias(t *testing.T) {
|
|||
require.True(t, s.OnStartup(context.Background()).OK)
|
||||
assert.Contains(t, c.Commands(), "auth/provision")
|
||||
assert.Contains(t, c.Commands(), "auth/revoke")
|
||||
assert.Contains(t, c.Commands(), "message/send")
|
||||
assert.Contains(t, c.Commands(), "messages/send")
|
||||
assert.Contains(t, c.Commands(), "message/inbox")
|
||||
assert.Contains(t, c.Commands(), "messages/inbox")
|
||||
assert.Contains(t, c.Commands(), "message/conversation")
|
||||
assert.Contains(t, c.Commands(), "messages/conversation")
|
||||
assert.Contains(t, c.Commands(), "subscription/budget/update")
|
||||
assert.Contains(t, c.Commands(), "subscription/update-budget")
|
||||
assert.Contains(t, c.Commands(), "fleet/events")
|
||||
|
|
@ -641,6 +653,9 @@ func TestPrep_RegisterTools_Good_RegistersCompletionTool(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Contains(t, toolNames, "agentic_complete")
|
||||
assert.Contains(t, toolNames, "agentic_message_send")
|
||||
assert.Contains(t, toolNames, "agentic_message_inbox")
|
||||
assert.Contains(t, toolNames, "agentic_message_conversation")
|
||||
}
|
||||
|
||||
func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue