sketch: add support for 'external' message types
- adds a new CodingAgentMessageType for loop.AgentMessage
- adds an new /external handler to loophttp.go
- modifies Agent to pass the .TextContent of ExternalMessage into the convo
as though it came from the user.
- adds sketch-external-message web component, with a template for
github workflow run events specifically.
- adds demos for sketch-external-message
diff --git a/loop/agent.go b/loop/agent.go
index 8339369..0877a5a 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -167,21 +167,25 @@
// ModelName returns the name of the model the agent is using.
ModelName() string
+
+ // ExternalMessage enqueues an external message to the agent and returns immediately.
+ ExternalMessage(ctx context.Context, msg ExternalMessage) error
}
type CodingAgentMessageType string
const (
- UserMessageType CodingAgentMessageType = "user"
- AgentMessageType CodingAgentMessageType = "agent"
- ErrorMessageType CodingAgentMessageType = "error"
- BudgetMessageType CodingAgentMessageType = "budget" // dedicated for "out of budget" errors
- ToolUseMessageType CodingAgentMessageType = "tool"
- CommitMessageType CodingAgentMessageType = "commit" // for displaying git commits
- AutoMessageType CodingAgentMessageType = "auto" // for automated notifications like autoformatting
- CompactMessageType CodingAgentMessageType = "compact" // for conversation compaction notifications
- PortMessageType CodingAgentMessageType = "port" // for port monitoring events
- SlugMessageType CodingAgentMessageType = "slug" // for slug updates
+ UserMessageType CodingAgentMessageType = "user"
+ AgentMessageType CodingAgentMessageType = "agent"
+ ErrorMessageType CodingAgentMessageType = "error"
+ BudgetMessageType CodingAgentMessageType = "budget" // dedicated for "out of budget" errors
+ ToolUseMessageType CodingAgentMessageType = "tool"
+ CommitMessageType CodingAgentMessageType = "commit" // for displaying git commits
+ AutoMessageType CodingAgentMessageType = "auto" // for automated notifications like autoformatting
+ CompactMessageType CodingAgentMessageType = "compact" // for conversation compaction notifications
+ PortMessageType CodingAgentMessageType = "port" // for port monitoring events
+ SlugMessageType CodingAgentMessageType = "slug" // for slug updates
+ ExternalMessageType CodingAgentMessageType = "external" // for external notifications
cancelToolUseMessage = "Stop responding to my previous message. Wait for me to ask you something else before attempting to use any more tools."
)
@@ -191,12 +195,13 @@
// EndOfTurn indicates that the AI is done working and is ready for the next user input.
EndOfTurn bool `json:"end_of_turn"`
- Content string `json:"content"`
- ToolName string `json:"tool_name,omitempty"`
- ToolInput string `json:"input,omitempty"`
- ToolResult string `json:"tool_result,omitempty"`
- ToolError bool `json:"tool_error,omitempty"`
- ToolCallId string `json:"tool_call_id,omitempty"`
+ Content string `json:"content"`
+ ExternalMessage *ExternalMessage `json:"external_message,omitempty"`
+ ToolName string `json:"tool_name,omitempty"`
+ ToolInput string `json:"input,omitempty"`
+ ToolResult string `json:"tool_result,omitempty"`
+ ToolError bool `json:"tool_error,omitempty"`
+ ToolCallId string `json:"tool_call_id,omitempty"`
// ToolCalls is a list of all tool calls requested in this message (name and input pairs)
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
@@ -480,6 +485,18 @@
outstandingToolCalls map[string]string
}
+// ExternalMessage implements CodingAgent.
+// TODO: Debounce and/or coalesce these messages so they're less disruptive to the conversation.
+func (a *Agent) ExternalMessage(ctx context.Context, msg ExternalMessage) error {
+ agentMsg := AgentMessage{
+ Type: ExternalMessageType,
+ ExternalMessage: &msg,
+ }
+ a.pushToOutbox(ctx, agentMsg)
+ a.inbox <- msg.TextContent
+ return nil
+}
+
// TokenContextWindow implements CodingAgent.
func (a *Agent) TokenContextWindow() int {
return a.config.Service.TokenContextWindow()
@@ -2729,3 +2746,11 @@
}
return ""
}
+
+// ExternalMsg represents a message from a source external to the agent/user conversation,
+// such as the outcome of a github workflow run.
+type ExternalMessage struct {
+ MessageType string `json:"message_type"`
+ Body any `json:"body"`
+ TextContent string `json:"text_content"`
+}
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index dacc4f6..856932d 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -706,6 +706,35 @@
w.WriteHeader(http.StatusOK)
})
+ // Handler for POST /external - e.g. where you send messages about e.g. github workflow
+ // outcomes and other external events that the agent wouldn't otherwise be aware of.
+ s.mux.HandleFunc("/external", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ httpError(w, r, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ var msg loop.ExternalMessage
+
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&msg); err != nil {
+ httpError(w, r, "Invalid request body: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ if msg.TextContent == "" {
+ httpError(w, r, "Message cannot be empty", http.StatusBadRequest)
+ return
+ }
+
+ if err := agent.ExternalMessage(r.Context(), msg); err != nil {
+ httpError(w, r, "agent ExternalMessage error: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ })
+
// Handler for POST /upload - uploads a file to /tmp
s.mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
diff --git a/loop/server/loophttp_test.go b/loop/server/loophttp_test.go
index fa591b2..8d4583e 100644
--- a/loop/server/loophttp_test.go
+++ b/loop/server/loophttp_test.go
@@ -38,6 +38,11 @@
model string
}
+// ExternalMessage implements loop.CodingAgent.
+func (m *mockAgent) ExternalMessage(ctx context.Context, msg loop.ExternalMessage) error {
+ panic("unimplemented")
+}
+
// TokenContextWindow implements loop.CodingAgent.
func (m *mockAgent) TokenContextWindow() int {
return 200000