Skip to content

Commit bdc6889

Browse files
bankseanAutoformatter
authored andcommitted
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
1 parent 6fe809c commit bdc6889

File tree

11 files changed

+361
-23
lines changed

11 files changed

+361
-23
lines changed

cmd/go2ts/go2ts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func TS() *go2ts.Go2TS {
5050
loop.PortMessageType,
5151
loop.CompactMessageType,
5252
loop.SlugMessageType,
53+
loop.ExternalMessageType,
5354
},
5455
)
5556

loop/agent.go

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,25 @@ type CodingAgent interface {
167167

168168
// ModelName returns the name of the model the agent is using.
169169
ModelName() string
170+
171+
// ExternalMessage enqueues an external message to the agent and returns immediately.
172+
ExternalMessage(ctx context.Context, msg ExternalMessage) error
170173
}
171174

172175
type CodingAgentMessageType string
173176

174177
const (
175-
UserMessageType CodingAgentMessageType = "user"
176-
AgentMessageType CodingAgentMessageType = "agent"
177-
ErrorMessageType CodingAgentMessageType = "error"
178-
BudgetMessageType CodingAgentMessageType = "budget" // dedicated for "out of budget" errors
179-
ToolUseMessageType CodingAgentMessageType = "tool"
180-
CommitMessageType CodingAgentMessageType = "commit" // for displaying git commits
181-
AutoMessageType CodingAgentMessageType = "auto" // for automated notifications like autoformatting
182-
CompactMessageType CodingAgentMessageType = "compact" // for conversation compaction notifications
183-
PortMessageType CodingAgentMessageType = "port" // for port monitoring events
184-
SlugMessageType CodingAgentMessageType = "slug" // for slug updates
178+
UserMessageType CodingAgentMessageType = "user"
179+
AgentMessageType CodingAgentMessageType = "agent"
180+
ErrorMessageType CodingAgentMessageType = "error"
181+
BudgetMessageType CodingAgentMessageType = "budget" // dedicated for "out of budget" errors
182+
ToolUseMessageType CodingAgentMessageType = "tool"
183+
CommitMessageType CodingAgentMessageType = "commit" // for displaying git commits
184+
AutoMessageType CodingAgentMessageType = "auto" // for automated notifications like autoformatting
185+
CompactMessageType CodingAgentMessageType = "compact" // for conversation compaction notifications
186+
PortMessageType CodingAgentMessageType = "port" // for port monitoring events
187+
SlugMessageType CodingAgentMessageType = "slug" // for slug updates
188+
ExternalMessageType CodingAgentMessageType = "external" // for external notifications
185189

186190
cancelToolUseMessage = "Stop responding to my previous message. Wait for me to ask you something else before attempting to use any more tools."
187191
)
@@ -191,12 +195,13 @@ type AgentMessage struct {
191195
// EndOfTurn indicates that the AI is done working and is ready for the next user input.
192196
EndOfTurn bool `json:"end_of_turn"`
193197

194-
Content string `json:"content"`
195-
ToolName string `json:"tool_name,omitempty"`
196-
ToolInput string `json:"input,omitempty"`
197-
ToolResult string `json:"tool_result,omitempty"`
198-
ToolError bool `json:"tool_error,omitempty"`
199-
ToolCallId string `json:"tool_call_id,omitempty"`
198+
Content string `json:"content"`
199+
ExternalMessage *ExternalMessage `json:"external_message,omitempty"`
200+
ToolName string `json:"tool_name,omitempty"`
201+
ToolInput string `json:"input,omitempty"`
202+
ToolResult string `json:"tool_result,omitempty"`
203+
ToolError bool `json:"tool_error,omitempty"`
204+
ToolCallId string `json:"tool_call_id,omitempty"`
200205

201206
// ToolCalls is a list of all tool calls requested in this message (name and input pairs)
202207
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
@@ -480,6 +485,18 @@ type Agent struct {
480485
outstandingToolCalls map[string]string
481486
}
482487

488+
// ExternalMessage implements CodingAgent.
489+
// TODO: Debounce and/or coalesce these messages so they're less disruptive to the conversation.
490+
func (a *Agent) ExternalMessage(ctx context.Context, msg ExternalMessage) error {
491+
agentMsg := AgentMessage{
492+
Type: ExternalMessageType,
493+
ExternalMessage: &msg,
494+
}
495+
a.pushToOutbox(ctx, agentMsg)
496+
a.inbox <- msg.TextContent
497+
return nil
498+
}
499+
483500
// TokenContextWindow implements CodingAgent.
484501
func (a *Agent) TokenContextWindow() int {
485502
return a.config.Service.TokenContextWindow()
@@ -2729,3 +2746,11 @@ func (a *Agent) SkabandAddr() string {
27292746
}
27302747
return ""
27312748
}
2749+
2750+
// ExternalMsg represents a message from a source external to the agent/user conversation,
2751+
// such as the outcome of a github workflow run.
2752+
type ExternalMessage struct {
2753+
MessageType string `json:"message_type"`
2754+
Body any `json:"body"`
2755+
TextContent string `json:"text_content"`
2756+
}

loop/server/loophttp.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,35 @@ func New(agent loop.CodingAgent, logFile *os.File) (*Server, error) {
706706
w.WriteHeader(http.StatusOK)
707707
})
708708

709+
// Handler for POST /external - e.g. where you send messages about e.g. github workflow
710+
// outcomes and other external events that the agent wouldn't otherwise be aware of.
711+
s.mux.HandleFunc("/external", func(w http.ResponseWriter, r *http.Request) {
712+
if r.Method != http.MethodPost {
713+
httpError(w, r, "Method not allowed", http.StatusMethodNotAllowed)
714+
return
715+
}
716+
var msg loop.ExternalMessage
717+
718+
decoder := json.NewDecoder(r.Body)
719+
if err := decoder.Decode(&msg); err != nil {
720+
httpError(w, r, "Invalid request body: "+err.Error(), http.StatusBadRequest)
721+
return
722+
}
723+
defer r.Body.Close()
724+
725+
if msg.TextContent == "" {
726+
httpError(w, r, "Message cannot be empty", http.StatusBadRequest)
727+
return
728+
}
729+
730+
if err := agent.ExternalMessage(r.Context(), msg); err != nil {
731+
httpError(w, r, "agent ExternalMessage error: "+err.Error(), http.StatusBadRequest)
732+
return
733+
}
734+
735+
w.WriteHeader(http.StatusOK)
736+
})
737+
709738
// Handler for POST /upload - uploads a file to /tmp
710739
s.mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
711740
if r.Method != http.MethodPost {

loop/server/loophttp_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ type mockAgent struct {
3838
model string
3939
}
4040

41+
// ExternalMessage implements loop.CodingAgent.
42+
func (m *mockAgent) ExternalMessage(ctx context.Context, msg loop.ExternalMessage) error {
43+
panic("unimplemented")
44+
}
45+
4146
// TokenContextWindow implements loop.CodingAgent.
4247
func (m *mockAgent) TokenContextWindow() int {
4348
return 200000

webui/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"test:playwright": "playwright test -c playwright-ct.config.ts"
2727
},
2828
"dependencies": {
29+
"@octokit/webhooks-types": "^7.6.1",
2930
"@tailwindcss/cli": "^4.1.10",
3031
"@tailwindcss/container-queries": "^0.1.1",
3132
"@tailwindcss/vite": "^4.1.11",

webui/src/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
// Auto-generated by sketch.dev/cmd/go2ts.go
22
// DO NOT EDIT. This file is automatically generated.
33

4+
export interface ExternalMessage {
5+
message_type: string;
6+
body: any;
7+
text_content: string;
8+
}
9+
410
export interface ToolCall {
511
name: string;
612
input: string;
@@ -29,6 +35,7 @@ export interface AgentMessage {
2935
type: CodingAgentMessageType;
3036
end_of_turn: boolean;
3137
content: string;
38+
external_message?: ExternalMessage | null;
3239
tool_name?: string;
3340
input?: string;
3441
tool_result?: string;
@@ -166,6 +173,6 @@ export interface GitLogEntry {
166173
subject: string;
167174
}
168175

169-
export type CodingAgentMessageType = 'user' | 'agent' | 'error' | 'budget' | 'tool' | 'commit' | 'auto' | 'port' | 'compact' | 'slug';
176+
export type CodingAgentMessageType = 'user' | 'agent' | 'error' | 'budget' | 'tool' | 'commit' | 'auto' | 'port' | 'compact' | 'slug' | 'external';
170177

171178
export type Duration = number;

webui/src/web-components/demo/demo-framework/demo-runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export class DemoRunner {
9696
"sketch-diff-range-picker",
9797
"sketch-timeline",
9898
"sketch-timeline-message",
99+
"sketch-external-message",
99100
"sketch-todo-panel",
100101
"sketch-tool-calls",
101102
"sketch-view-mode-select",
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { DemoModule } from "./demo-framework/types";
2+
import { demoUtils } from "./demo-fixtures/index";
3+
import { SketchExternalMessage } from "../sketch-external-message";
4+
5+
const demo: DemoModule = {
6+
title: "Sketch External Message Demo",
7+
description:
8+
"Demonstration of external message components for various external message types",
9+
imports: ["../sketch-external-message.ts"],
10+
11+
setup: async (container: HTMLElement) => {
12+
const section = demoUtils.createDemoSection(
13+
"External Message Components",
14+
"Shows different external message components for various message types",
15+
);
16+
17+
// external messages
18+
const externalMessages = [
19+
{
20+
name: "github_workflow_run: completed, success",
21+
message_type: "github_workflow_run",
22+
body: {
23+
workflow: {
24+
name: "go_tests",
25+
},
26+
workflow_run: {
27+
id: 123456789,
28+
status: "completed",
29+
conclusion: "success",
30+
head_branch: "user/sketch/slug-name",
31+
head_commit: "abc123",
32+
html_url: "https://github.com/orgs/your-org/actions/runs/123456789",
33+
},
34+
},
35+
},
36+
{
37+
name: "github_workflow_run: completed, failed",
38+
message_type: "github_workflow_run",
39+
body: {
40+
workflow: {
41+
name: "go_tests",
42+
},
43+
workflow_run: {
44+
id: 123456789,
45+
status: "completed",
46+
conclusion: "failure",
47+
head_branch: "user/sketch/slug-name",
48+
head_commit: "abc123",
49+
html_url: "https://github.com/orgs/your-org/actions/runs/123456789",
50+
},
51+
},
52+
},
53+
{
54+
name: "github_workflow_run: queued",
55+
message_type: "github_workflow_run",
56+
body: {
57+
workflow: {
58+
name: "go_tests",
59+
},
60+
workflow_run: {
61+
id: 123456789,
62+
status: "queued",
63+
head_branch: "user/sketch/slug-name",
64+
head_commit: "abc123",
65+
html_url: "https://github.com/orgs/your-org/actions/runs/123456789",
66+
},
67+
},
68+
},
69+
{
70+
name: "github_workflow_run: in_progress",
71+
message_type: "github_workflow_run",
72+
body: {
73+
workflow: {
74+
name: "go_tests",
75+
},
76+
workflow_run: {
77+
id: 123456789,
78+
status: "in_progress",
79+
head_branch: "user/sketch/slug-name",
80+
head_commit: "abc123",
81+
html_url: "https://github.com/orgs/your-org/actions/runs/123456789",
82+
},
83+
},
84+
},
85+
];
86+
87+
// Create tool cards for each tool call
88+
externalMessages.forEach((msg) => {
89+
const msgSection = document.createElement("div");
90+
msgSection.style.marginBottom = "2rem";
91+
msgSection.style.border = "1px solid #eee";
92+
msgSection.style.borderRadius = "8px";
93+
msgSection.style.padding = "1rem";
94+
95+
const header = document.createElement("h3");
96+
header.textContent = `Message: ${msg.name}`;
97+
header.style.marginTop = "0";
98+
header.style.marginBottom = "1rem";
99+
header.style.color = "var(--demo-fixture-text-color)";
100+
msgSection.appendChild(header);
101+
const msgEl = document.createElement(
102+
"sketch-external-message",
103+
) as SketchExternalMessage;
104+
105+
msgEl.message = {
106+
message_type: msg.message_type,
107+
body: msg.body,
108+
text_content: `Workflow run ${msg.body.workflow_run.id} completed with status ${msg.body.workflow_run.status} and conclusion ${msg.body.workflow_run.conclusion}.`,
109+
};
110+
msgEl.open = true;
111+
112+
msgSection.appendChild(msgEl);
113+
section.appendChild(msgSection);
114+
});
115+
116+
container.appendChild(section);
117+
},
118+
};
119+
120+
export default demo;

0 commit comments

Comments
 (0)