Render Markdown, add ask_human context, simplify inbox responses, group inbox by task
This commit is contained in:
+25
-19
@@ -22,6 +22,7 @@ var attachmentKinds = []api.AttachmentKind{
|
||||
}
|
||||
|
||||
type askArgs struct {
|
||||
Context string `json:"context"`
|
||||
Question string `json:"question"`
|
||||
WhyBlocked string `json:"why_blocked"`
|
||||
Options []string `json:"options"`
|
||||
@@ -35,6 +36,8 @@ func (m *Manager) askTool(l *live) mcp.Tool {
|
||||
return mcp.Tool{
|
||||
Name: "ask_human",
|
||||
Description: "Ask the human a question through their inbox. Use this for every question; never ask in plain prose. " +
|
||||
"The human answers from their inbox without having read your transcript, so every question must stand on its own: " +
|
||||
"put the background they need in context. " +
|
||||
"Offer concrete options and recommend one. If blocking is true, call report and end your turn after asking; " +
|
||||
"the answer arrives as your next message. If blocking is false, continue with work that doesn't depend on the answer. " +
|
||||
"Ask several questions in one turn by calling this once per question; they are answered together. " +
|
||||
@@ -42,7 +45,8 @@ func (m *Manager) askTool(l *live) mcp.Tool {
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": {"type": "string"},
|
||||
"context": {"type": "string", "description": "Everything the human needs to answer well without reading your transcript: what you are working on, what you found, the relevant facts and data, and what each option would concretely do. Several paragraphs is normal. Markdown is rendered."},
|
||||
"question": {"type": "string", "description": "The question itself, in one or two sentences. Markdown is rendered."},
|
||||
"why_blocked": {"type": "string", "description": "What you can't do until this is answered, and why you can't decide it yourself."},
|
||||
"options": {"type": "array", "items": {"type": "string"}, "minItems": 2},
|
||||
"recommended_option": {"type": "string", "description": "One of options."},
|
||||
@@ -65,7 +69,7 @@ func (m *Manager) askTool(l *live) mcp.Tool {
|
||||
"blocking": {"type": "boolean"},
|
||||
"thread_id": {"type": "string"}
|
||||
},
|
||||
"required": ["question", "why_blocked", "options", "recommended_option", "blocking"]
|
||||
"required": ["context", "question", "why_blocked", "options", "recommended_option", "blocking"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.ask(ctx, l, raw)
|
||||
@@ -102,6 +106,7 @@ func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string
|
||||
}
|
||||
|
||||
payload := map[string]any{
|
||||
"context": args.Context,
|
||||
"question": args.Question,
|
||||
"why_blocked": args.WhyBlocked,
|
||||
"options": args.Options,
|
||||
@@ -109,7 +114,7 @@ func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string
|
||||
"attachments": attachments,
|
||||
}
|
||||
|
||||
err = m.store.CreateInboxItem(ctx, m.newItem(l, id, thread, api.InboxItemKindQuestion, priority, *args.Blocking, store.ToPayload(payload)))
|
||||
err = m.store.CreateInboxItem(ctx, m.newItem(l, id, thread, api.Question, priority, *args.Blocking, store.ToPayload(payload)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -123,6 +128,9 @@ func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string
|
||||
func (m *Manager) validateAsk(ctx context.Context, l *live, args askArgs) error {
|
||||
problems := []string{}
|
||||
|
||||
if strings.TrimSpace(args.Context) == "" {
|
||||
problems = append(problems, "context is empty: give the background the human needs to answer without reading your transcript")
|
||||
}
|
||||
if strings.TrimSpace(args.Question) == "" {
|
||||
problems = append(problems, "question is empty")
|
||||
}
|
||||
@@ -164,7 +172,7 @@ func (m *Manager) validateAsk(ctx context.Context, l *live, args askArgs) error
|
||||
|
||||
func (m *Manager) fileReport(l *live, r report.Report) {
|
||||
id := store.NewID()
|
||||
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.InboxItemKindReport, api.Fyi, false, store.ToPayload(r)))
|
||||
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.Report, api.Fyi, false, store.ToPayload(r)))
|
||||
if err != nil {
|
||||
slog.Error("file report", "session", l.id, "err", err)
|
||||
}
|
||||
@@ -172,7 +180,7 @@ func (m *Manager) fileReport(l *live, r report.Report) {
|
||||
|
||||
func (m *Manager) fileError(l *live, msg string) {
|
||||
id := store.NewID()
|
||||
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.InboxItemKindSessionError, api.Blocked, true, map[string]any{"error": msg}))
|
||||
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.SessionError, api.Blocked, true, map[string]any{"error": msg}))
|
||||
if err != nil {
|
||||
slog.Error("file session error", "session", l.id, "err", err)
|
||||
}
|
||||
@@ -181,7 +189,7 @@ func (m *Manager) fileError(l *live, msg string) {
|
||||
func (m *Manager) fileRateLimited(l *live, info map[string]any, failure string) {
|
||||
id := store.NewID()
|
||||
payload := map[string]any{"vendor": string(l.vendor), "rate_limit": info, "error": failure}
|
||||
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.InboxItemKindRateLimited, api.Blocked, true, payload))
|
||||
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.RateLimited, api.Blocked, true, payload))
|
||||
if err != nil {
|
||||
slog.Error("file rate limit", "session", l.id, "err", err)
|
||||
}
|
||||
@@ -261,28 +269,26 @@ func deliveryMessage(pending []store.Pending) string {
|
||||
b.WriteString("\n")
|
||||
|
||||
switch p.Item.Kind {
|
||||
case api.InboxItemKindQuestion:
|
||||
case api.Question:
|
||||
fmt.Fprintf(b, "Your question %s: %s\n", p.Item.Id, p.Item.Payload["question"])
|
||||
case api.InboxItemKindReport:
|
||||
case api.Report:
|
||||
fmt.Fprintf(b, "Your report %s\n", p.Item.Id)
|
||||
case api.InboxItemKindSessionError:
|
||||
case api.SessionError:
|
||||
fmt.Fprintf(b, "Your error %s: %s\n", p.Item.Id, p.Item.Payload["error"])
|
||||
case api.InboxItemKindRateLimited:
|
||||
case api.RateLimited:
|
||||
fmt.Fprintf(b, "Your rate-limited turn %s: %s\n", p.Item.Id, p.Item.Payload["error"])
|
||||
}
|
||||
|
||||
switch r.Type {
|
||||
case api.InboxResponseTypeOption:
|
||||
case api.Option:
|
||||
fmt.Fprintf(b, "%s chose: %s\n", r.User, *r.Option)
|
||||
if r.Text != nil && *r.Text != "" {
|
||||
fmt.Fprintf(b, "Note: %s\n", *r.Text)
|
||||
case api.Other:
|
||||
if p.Item.Kind == api.Question {
|
||||
fmt.Fprintf(b, "%s answered (none of your options): %s\n", r.User, *r.Text)
|
||||
fmt.Fprintf(b, "If this asks you something back, answer by calling ask_human with thread_id %q.\n", p.Item.ThreadId)
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s replied: %s\n", r.User, *r.Text)
|
||||
}
|
||||
case api.InboxResponseTypeFreeForm:
|
||||
fmt.Fprintf(b, "%s answered (none of your options): %s\n", r.User, *r.Text)
|
||||
case api.InboxResponseTypeRedirect:
|
||||
fmt.Fprintf(b, "%s redirected you. These instructions replace your current direction: %s\n", r.User, *r.Text)
|
||||
case api.InboxResponseTypeQuestion:
|
||||
fmt.Fprintf(b, "%s asked a follow-up question: %s\nAnswer it by calling ask_human with thread_id %q.\n", r.User, *r.Text, p.Item.ThreadId)
|
||||
}
|
||||
|
||||
for _, a := range r.Attachments {
|
||||
|
||||
@@ -19,11 +19,11 @@ func TestValidateAsk(t *testing.T) {
|
||||
args askArgs
|
||||
want []string
|
||||
}{
|
||||
{"valid", askArgs{Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "a", Blocking: &yes}, nil},
|
||||
{"empty", askArgs{}, []string{"question is empty", "why_blocked is empty", "at least two", "recommended_option", "blocking is required"}},
|
||||
{"one option", askArgs{Question: "q", WhyBlocked: "w", Options: []string{"a"}, RecommendedOption: "a", Blocking: &yes}, []string{"at least two"}},
|
||||
{"bad recommendation", askArgs{Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "c", Blocking: &yes}, []string{"recommended_option"}},
|
||||
{"bad attachment", askArgs{Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "a", Blocking: &yes,
|
||||
{"valid", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "a", Blocking: &yes}, nil},
|
||||
{"empty", askArgs{}, []string{"context is empty", "question is empty", "why_blocked is empty", "at least two", "recommended_option", "blocking is required"}},
|
||||
{"one option", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a"}, RecommendedOption: "a", Blocking: &yes}, []string{"at least two"}},
|
||||
{"bad recommendation", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "c", Blocking: &yes}, []string{"recommended_option"}},
|
||||
{"bad attachment", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "a", Blocking: &yes,
|
||||
Attachments: []api.Attachment{{Kind: "video"}}}, []string{`attachments[0].kind "video"`}},
|
||||
}
|
||||
|
||||
@@ -53,25 +53,24 @@ func TestValidateAsk(t *testing.T) {
|
||||
|
||||
func TestDeliveryMessage(t *testing.T) {
|
||||
str := func(s string) *string { return &s }
|
||||
question := api.InboxItem{Id: "q1", ThreadId: "q1", Kind: api.InboxItemKindQuestion, Payload: map[string]any{"question": "Which color?"}}
|
||||
report := api.InboxItem{Id: "r1", ThreadId: "r1", Kind: api.InboxItemKindReport, Payload: map[string]any{}}
|
||||
question := api.InboxItem{Id: "q1", ThreadId: "q1", Kind: api.Question, Payload: map[string]any{"question": "Which color?"}}
|
||||
failure := api.InboxItem{Id: "e1", ThreadId: "e1", Kind: api.SessionError, Payload: map[string]any{"error": "process died"}}
|
||||
|
||||
msg := deliveryMessage([]store.Pending{
|
||||
{Item: question, Response: api.InboxResponse{User: "alice", Type: api.InboxResponseTypeOption, Option: str("blue"), Text: str("navy if possible")}},
|
||||
{Item: question, Response: api.InboxResponse{User: "bob", Type: api.InboxResponseTypeQuestion, Text: str("what is it for?"),
|
||||
{Item: question, Response: api.InboxResponse{User: "alice", Type: api.Option, Option: str("blue")}},
|
||||
{Item: question, Response: api.InboxResponse{User: "bob", Type: api.Other, Text: str("what is it for?"),
|
||||
Attachments: []api.Attachment{{Kind: api.AttachmentKindUrl, Url: str("https://example.com")}}}},
|
||||
{Item: report, Response: api.InboxResponse{User: "alice", Type: api.InboxResponseTypeRedirect, Text: str("stop and write tests")}},
|
||||
{Item: failure, Response: api.InboxResponse{User: "alice", Type: api.Other, Text: str("stop and write tests")}},
|
||||
})
|
||||
|
||||
for _, want := range []string{
|
||||
"Your question q1: Which color?",
|
||||
"alice chose: blue",
|
||||
"Note: navy if possible",
|
||||
"bob asked a follow-up question: what is it for?",
|
||||
"bob answered (none of your options): what is it for?",
|
||||
`calling ask_human with thread_id "q1"`,
|
||||
`"url":"https://example.com"`,
|
||||
"Your report r1",
|
||||
"alice redirected you. These instructions replace your current direction: stop and write tests",
|
||||
"Your error e1: process died",
|
||||
"alice replied: stop and write tests",
|
||||
} {
|
||||
if !strings.Contains(msg, want) {
|
||||
t.Fatalf("message missing %q:\n%s", want, msg)
|
||||
|
||||
@@ -317,7 +317,7 @@ func (e *env) openQuestions(sessionID string) []api.InboxItem {
|
||||
}
|
||||
questions := []api.InboxItem{}
|
||||
for _, item := range items {
|
||||
if item.Kind == api.InboxItemKindQuestion {
|
||||
if item.Kind == api.Question {
|
||||
questions = append(questions, item)
|
||||
}
|
||||
}
|
||||
@@ -379,7 +379,7 @@ func TestAskHumanRoundTrip(t *testing.T) {
|
||||
t.Fatalf("awaiting human: %d %v", waiting, err)
|
||||
}
|
||||
|
||||
e.respond(m, q, api.InboxResponseTypeOption, "blue", "", api.Answered)
|
||||
e.respond(m, q, api.Option, "blue", "", api.Answered)
|
||||
events := e.waitIdle()
|
||||
|
||||
delivered := false
|
||||
@@ -423,7 +423,7 @@ func TestFollowUpQuestionThread(t *testing.T) {
|
||||
}
|
||||
first := questions[0]
|
||||
|
||||
e.respond(m, first, api.InboxResponseTypeQuestion, "", "Before I pick: which of the two did you recommend, and why?", api.Open)
|
||||
e.respond(m, first, api.Other, "", "Before I pick: which of the two did you recommend, and why?", api.Answered)
|
||||
e.waitIdle()
|
||||
|
||||
questions = e.openQuestions(sess.Id)
|
||||
@@ -435,15 +435,7 @@ func TestFollowUpQuestionThread(t *testing.T) {
|
||||
t.Fatalf("follow-up didn't continue the thread: first %s/%s, second %s/%s", first.Id, first.ThreadId, second.Id, second.ThreadId)
|
||||
}
|
||||
|
||||
old, err := e.store.GetInboxItem(e.ctx, first.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("get first question: %v", err)
|
||||
}
|
||||
if old.State != api.Superseded {
|
||||
t.Fatalf("first question is %s, want superseded", old.State)
|
||||
}
|
||||
|
||||
e.respond(m, second, api.InboxResponseTypeFreeForm, "", "Use green instead of either.", api.Answered)
|
||||
e.respond(m, second, api.Other, "", "Use green instead of either.", api.Answered)
|
||||
e.waitIdle()
|
||||
|
||||
out, err := os.ReadFile(filepath.Join(e.task.WorktreePath, "color.txt"))
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestRateLimitedTurn(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("list inbox: %v", err)
|
||||
}
|
||||
if len(items) != 1 || items[0].Kind != api.InboxItemKindRateLimited || items[0].Priority != api.Blocked {
|
||||
if len(items) != 1 || items[0].Kind != api.RateLimited || items[0].Priority != api.Blocked {
|
||||
t.Fatalf("want one blocked rate_limited item, got %+v", items)
|
||||
}
|
||||
limit, ok := items[0].Payload["rate_limit"].(map[string]any)
|
||||
|
||||
+66
-75
@@ -110,22 +110,22 @@ func (e AttachmentKind) Valid() bool {
|
||||
|
||||
// Defines values for InboxItemKind.
|
||||
const (
|
||||
InboxItemKindQuestion InboxItemKind = "question"
|
||||
InboxItemKindRateLimited InboxItemKind = "rate_limited"
|
||||
InboxItemKindReport InboxItemKind = "report"
|
||||
InboxItemKindSessionError InboxItemKind = "session_error"
|
||||
Question InboxItemKind = "question"
|
||||
RateLimited InboxItemKind = "rate_limited"
|
||||
Report InboxItemKind = "report"
|
||||
SessionError InboxItemKind = "session_error"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the InboxItemKind enum.
|
||||
func (e InboxItemKind) Valid() bool {
|
||||
switch e {
|
||||
case InboxItemKindQuestion:
|
||||
case Question:
|
||||
return true
|
||||
case InboxItemKindRateLimited:
|
||||
case RateLimited:
|
||||
return true
|
||||
case InboxItemKindReport:
|
||||
case Report:
|
||||
return true
|
||||
case InboxItemKindSessionError:
|
||||
case SessionError:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -158,28 +158,22 @@ func (e InboxItemPriority) Valid() bool {
|
||||
|
||||
// Defines values for InboxResponseType.
|
||||
const (
|
||||
InboxResponseTypeDismiss InboxResponseType = "dismiss"
|
||||
InboxResponseTypeFreeForm InboxResponseType = "free_form"
|
||||
InboxResponseTypeOption InboxResponseType = "option"
|
||||
InboxResponseTypeQuestion InboxResponseType = "question"
|
||||
InboxResponseTypeRedirect InboxResponseType = "redirect"
|
||||
InboxResponseTypeSnooze InboxResponseType = "snooze"
|
||||
Dismiss InboxResponseType = "dismiss"
|
||||
Option InboxResponseType = "option"
|
||||
Other InboxResponseType = "other"
|
||||
Snooze InboxResponseType = "snooze"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the InboxResponseType enum.
|
||||
func (e InboxResponseType) Valid() bool {
|
||||
switch e {
|
||||
case InboxResponseTypeDismiss:
|
||||
case Dismiss:
|
||||
return true
|
||||
case InboxResponseTypeFreeForm:
|
||||
case Option:
|
||||
return true
|
||||
case InboxResponseTypeOption:
|
||||
case Other:
|
||||
return true
|
||||
case InboxResponseTypeQuestion:
|
||||
return true
|
||||
case InboxResponseTypeRedirect:
|
||||
return true
|
||||
case InboxResponseTypeSnooze:
|
||||
case Snooze:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -191,7 +185,6 @@ const (
|
||||
Answered InboxState = "answered"
|
||||
Dismissed InboxState = "dismissed"
|
||||
Open InboxState = "open"
|
||||
Redirected InboxState = "redirected"
|
||||
Superseded InboxState = "superseded"
|
||||
)
|
||||
|
||||
@@ -204,8 +197,6 @@ func (e InboxState) Valid() bool {
|
||||
return true
|
||||
case Open:
|
||||
return true
|
||||
case Redirected:
|
||||
return true
|
||||
case Superseded:
|
||||
return true
|
||||
default:
|
||||
@@ -308,7 +299,7 @@ type InboxItem struct {
|
||||
Id string `json:"id"`
|
||||
Kind InboxItemKind `json:"kind"`
|
||||
|
||||
// Payload question: question, why_blocked, options, recommended_option, attachments. report: done, open_questions, next_steps, risks, deviations. session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error.
|
||||
// Payload question: context, question, why_blocked, options, recommended_option, attachments. report: done, next_steps, risks, deviations. session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error.
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
Priority InboxItemPriority `json:"priority"`
|
||||
ProjectId string `json:"project_id"`
|
||||
@@ -357,7 +348,7 @@ type InboxResponseCreate struct {
|
||||
// SnoozeUntil snooze: when the item reappears.
|
||||
SnoozeUntil *time.Time `json:"snooze_until,omitempty"`
|
||||
|
||||
// Text Required for free_form, redirect, question; an optional note on option.
|
||||
// Text other: the text sent to the agent.
|
||||
Text *string `json:"text,omitempty"`
|
||||
Type InboxResponseType `json:"type"`
|
||||
}
|
||||
@@ -1261,54 +1252,54 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
||||
// const string: with thousands of chunks the chained `+` fold is several
|
||||
// times slower for the Go compiler than parsing a slice literal.
|
||||
var swaggerSpec = []string{
|
||||
"zFttb9u69f8qB/r/gbaAYqf3dgPmvuru7YYM7VY06au7wqClY5s3EqmQlFMv8HcfDkk92ZQte3ZuXkWW",
|
||||
"qMPz8DuPVJ6iROaFFCiMjiZPkUJdSKHR/violFR0kUhhUBi6ZEWR8YQZLsX4dy0F3dPJEnNGV/+vcB5N",
|
||||
"ov8bN1TH7qkeO2qbzSaOUtSJ4gURiSZum1FED/xaIvVhgcLcotbcbVIoWaAy3LGWKGQG0ymzTM2lyukq",
|
||||
"SpnBK8NzjOLIrAuMJpE2iotFtInrd2ZremfnMU+Dt3OZYkZPukx/xYcStcEU7IL3gHlh1pAjExrMEmGF",
|
||||
"IpXqlYYU56zMzCjEkpIZEm0UZR5Nfot4XmSYozCoou+B9dowg7vMPJRYEid2cwamVAK4hkfGDRcLmEsF",
|
||||
"DOYKEXQmDZQiRWWZTKRISqVQJGtIWKGJyZqXNCM1OtpRHKlSCGIjtpDJ0GAa5NEwfT/tUaZTSlviJGNl",
|
||||
"ikFCbvFUOwyESZIO8aHkClPHc9QwUG9XGTFEstKpt0UHJXEbZg2HcvY7JoY4/GAMS5a5d40thDJvnYAa",
|
||||
"UKTTjAtsPeTC4AKVBWLOFgEj29sTSJlh8O3rpyCe7rlI29qdcytTyufzqCJMP9lCsdyrilQgWKGX0kRx",
|
||||
"VKosaIwcVc6cCbpseWIT+OxWgJalSjDIXsHMcpcCMTkBhYW8Upgxw1cItDLsMRRgdnmYzyew4AYUrjiZ",
|
||||
"FhQTizATlbB9GNWGKbPHPPvwTdo7CFJrpBCc6njbRVKOWntE7CdcLQzSXgVRyhLjttwRhVH83e988Ulh",
|
||||
"uMJoAB3rTLL2s4b9Qkm67LUZPnQY4ML8+V2z+SDrbSmTSHpmY6+mhsWDkeFGzOSPG4N5QOVDFDvLZHJP",
|
||||
"183DmZQZMnGq2ns22o4YNquRV5E2CqkoJlS8osVnHClmcJrxnPelgJYldxKVpT6B6iqGx+V6asXFNAZp",
|
||||
"V+oYFCYyz1GkmE7dzRhYHW71CBx3E0ilQHoPxbSiqWMQ+MNMtcGCSHF9r2NIccVt3aJH0JFoAvbPCNpy",
|
||||
"TXwCj1t34XUnsdODK/eAckip38Se0r9FFIcwzKXiZt1Wtxc8iqOFz0K44vgYxdF8zcO63e8JCrXMVkdi",
|
||||
"o36ppzTqVIXcYK4PFXsW/1/9a9bvHE2mFFu7KCzlfzCdlsLwbDindQF0cPNbu/JAvDZLhSwdXlq0lN+u",
|
||||
"M3Zcuk25DiK1/Vvu3Q4pVSHS8u626nvDjFNzuhtp/kez/WL5sMmfixv3+tttS25paTC7GgORsfHvwRy3",
|
||||
"SrAAyk4JlClmfIXqPOGVZOgDn+wvEJ13HOscBn+YMMztjSNMf0cvUD2jUQ10jUpS/5LfNO4YdVja3MLf",
|
||||
"M8BkmJW6qczdn4AUCHJuu6kq/7zSVR7rKUC7xu3SdU8n8LhEYakSc6CQFQUyZSkeh4bdvpXsZptCagmn",
|
||||
"RI3ybcoVJiaupXgPTHg5WAZCGgRZ3QjKdSLKtsFU48jSO4iSO79rlVG9veKoFs5GUSed62erAsep2jZE",
|
||||
"OudaB/NtK5l0dkGiwIR+pFDR2sL+8ATttS4LVBrTnlrpsyva+8De49NbSrOrQqr64hLWswxQ/KBjOlNM",
|
||||
"JMtjZiyC5dhTeOTS9D0q5LRqKAfEJ7tH+7UdhuvtjhsCeBX3GXCPbIMF2OE9xIeflfXxUc+xQhVlXgwA",
|
||||
"mV8X3NvWv8H2kq9wSpWSDnfT6Ww6Wxu3ekD7ljFtprhy5dbQno/y+dTWaHo3INo5IxRKJqg1anDzMJOt",
|
||||
"wQ+8RkGavhLskcoNzaamVKJnRdNVOEWlKXeR9kvX/bc13WX+bomQMYPa7HYjgCxZ+m7Fd0vUYd3jGlOY",
|
||||
"rf2TUahT8ZLv419rnCYZr3QaWGCYOi7AbDfgDYEWTHYg0DJF3MVbl8kuDLZl3DJZ1z4hxN8xfb+L9xnT",
|
||||
"OPUjqt2evj8u1hPVk+Lx5Yfgh8YvtftX+dEZ4vCsmJssHB0fpbo3lMGPCPHd7sySrrW+TTFubFXzf1zc",
|
||||
"JwT0Zu0eubbTtl0WIv7NF95dsinXRcbW096csuBmijnjWe/T3lczueDiMMtuWdxlpUW6zUOfYN+KNKi1",
|
||||
"k7nf4nEgM/QaF3PZMlg0Wyu05wTKnTtF16Pr0bXrA1CwgkeT6OfR9ejnyE2zLd9jVvAxS3Muxo0nLNB6",
|
||||
"Jclnp043aTSJ/o7mtsJapzv/6fr6bAdtfofASdstqhUqWCLLzBKYSKGk2nNEazexk8NG1rYEIRJXmpKm",
|
||||
"WzqCj5Ro7I9XGngKXAM3GjQ+vHfHFdwdidkZNDAN/7j91z9H7qcGNjeo4BPT5sreubr5FV5Ldz7lnj2U",
|
||||
"qNZQMMVyNKjeAFNIGS1jNpXhXCoEiu4VR3b+1tX8rVHIcreltZ0npqPJb08RIT9aIktt5+pQFnVYiuKW",
|
||||
"9g/WHJvY07SsNyStPEeS+n4QKlT9O7tdaStmFyvbzrJ7AGvt4l7tYIFT79MLBdsZ2fZUx7DkiyWVIdWQ",
|
||||
"C+ZcaQOv68nughmMwY03Y5iv+ZuYTCxAZim9aNeP4G88I6uAYErJRwsC6rAdBIwEBj7KU5OtgAHl+lca",
|
||||
"iGzI7p+4NpbRHqNvGaiTQvpV2GPeZiJ49KvV8G+Yi7cnnH0UuUiyMsWpn7SGmKqPFAaAbH88Gj5dtIci",
|
||||
"OzOYXVB+ZiZZcrFwAINHbpaEBk61rGd0BL+6Dk4TMihAx1AKL697bxfO446chdQBZH+wbb12sx0FOcUX",
|
||||
"xwYX9qYuZ9TfcylGUC1Wtt62UWvh4xwt9YdyIEVCRGxwTDO00Vf44RFFy2okXM9ftA1zpahmDCF0+9Fv",
|
||||
"BXDlPkr4q0zXZ8smnRHzpptqjSpx8+KQQ01RaUuMBgRx9M7xFdql5r/6RoVWvztq9V8Gr96G4xOxeJNu",
|
||||
"9tUMjfgXLBtaOg7r1A4irR8SXhsvPFZfm004FPu6vApfVi3RNtz2hdbvlWZdkdinzc94STXayr1HgwnL",
|
||||
"MnLxUqP94IkUUQaYdAWy5/P8Lt2qwS/g0Kcqp3JZr5zjPLbxqvZgJogAqgi+NCODywevagY7IHR9yLKq",
|
||||
"vtEVQHyG6grhGs+K8mVg0h1sDkLK23Nv3gcWgY+Vos6AlfGTvzoQh9v6vpCHHJC7K/O5Y26thdPCbkid",
|
||||
"43r22+uMd35ad3lPtEO7gRVE02ZYCWKCXNOnvCj9xz1lrPNc13v7NslNw2zxWc3DYK5kbtc0IvuTEb98",
|
||||
"tFN3Osp37uvBS8Se1nDtmQOPw0h/1CFFXrigbAKU/6REj5/81YEAVX0vfcEA1fkuu0dPntkLeUmtitOi",
|
||||
"VEinYy4MKlW6Y7CLsdDnqB9F6rzUHz7ZL7h3ve6mYrLXzD89q5lrpcEjo5jhv5sZPVcDFbSk77f1H2LI",
|
||||
"W8OU0cBcpCiVqOYWXFeDgBG0PATyUhuYoR0J7Br8FkXqvw+4UJztfn0wKNQ+L8asEpdMgz8OfKG9vK0R",
|
||||
"xk/050CIrpPmheLzvvzVyl3nDspO8tMicld54+rIcJgHn7Jxn/t+xVyuugVTXSVRyeTLIbhrnitMpErt",
|
||||
"U3f4AApzxgPR+xcv1h8JgPo09jQonMc7Dh0xtbSL7SMiBhof4q3zgi9sgTBbQ8G05mJhTZcxbWgtzFhy",
|
||||
"D8y/PwqeDZC29p4KneEEp2dAb78u6JDK2Q+el3k0eXt9fW0/u/U/TzoWOkPD5P5rZEDH9K0AI8F9fuKN",
|
||||
"9rpqIv50ff3m5UedqpjY26reVoueQ/nd9Disa/V+404gKpG2fOblmOJQGSW6ggAXvaFZI9XQ3FSNbF7Y",
|
||||
"ExhutD+JDJfUrvBp19Pnr7G6H+Y9czs7pMaiYrXbsr2Y8mqz+W8AAAD//w==",
|
||||
"zFvdj9u4Ef9XBmqBJIDW3tylBep7Su/SYoukDbKbp2tg0NLY5q1EaknKG3fh/70YkvqyKVv22Xv7FK9E",
|
||||
"DufjN59UnqJE5oUUKIyOJk+RQl1IodH+8UEpqehHIoVBYegnK4qMJ8xwKca/aSnomU6WmDP69WeF82gS",
|
||||
"/WncUB27t3rsqG02mzhKUSeKF0QkmrhjRhG98GuJ1PsFCnOLWnN3SKFkgcpwx1qikBlMp8wyNZcqp19R",
|
||||
"ygxeGZ5jFEdmXWA0ibRRXCyiTVzvma1pz85rngYf5zLFjN50mf6CDyVqgynYBT8B5oVZQ45MaDBLhBWK",
|
||||
"VKpXGlKcszIzoxBLSmZItFGUeTT5NeJ5kWGOwqCKvgXWa8MM7jLzUGJJnNjDGZhSCeAaHhk3XCxgLhUw",
|
||||
"mCtE0Jk0UIoUlWUykSIplUKRrCFhhSYma17SjNToaEdxpEohiI3YQiZDg2mQR8P0/bRHmU4pbYmTjJUp",
|
||||
"Bgm5xVPtMBAmSTrEh5IrTB3PUcNAfVxlxBDJSqfeFh2UxG2YNRzK2W+YGOLwvTEsWebeNbYQyrx1AmpA",
|
||||
"kU4zLrD1kguDC1QWiDlbBIxsH08gZYbB1y8fg3i65yJta3fOrUwpn8+jijD9yRaK5V5VpALBCr2UJoqj",
|
||||
"UmVBY+SocuZM0GXLE5vAJ7cCtCxVgkH2CmaWuxSIyQkoLOSVwowZvkKglWGPoQCzy8N8PoEFN6Bwxcm0",
|
||||
"oJhYhJmohO3DqDZMmT3m2Ydv0t5BkFojheBUx9suknLU2iNiP+FqYZD2KohSlhh35I4ojOLvfueLTwrD",
|
||||
"FUYD6FhnkrXfNewXStLPXpvhQ4cBLsxf3zWHD7LeljKJpGc29mpqWDwYGW7ETH6/MZgHVD5EsbNMJvf0",
|
||||
"u3k5kzJDJk5Ve89B2xHDZjXyKtJGIRXFhIpXtPiMI8UMTjOe874U0LLkTqKy1CdgS4rvJobqUQyPy/XU",
|
||||
"yo1pDNJu0TEoTGSeo0gxnbqHMbA67uoRODYnkEqBMQj8bqbaYEFbub7XMaS44rZg0SPoiDIB+88I2gJN",
|
||||
"fOaOW0/hdSej04sr94KSR6nfxJ7Sf0UUh8DLpeJm3dazFzSKo4VPP7ji+BjF0XzNw0rd7wIKtcxWR4Ki",
|
||||
"3tRTE3XKQW4w14eqPAv8L36bdThHkynF1i78Svk/TKelMDwbzmld+Rw8/NauPBCozVIhS4fXFC3ltwuM",
|
||||
"HV9uU66jR23/ll+3Y0lVgbTcuq363vji1JzuhpjfabafLR8263Nx47a/3bbklpYGs6sxEBIbfx7Mcav2",
|
||||
"CqDslAiZYsZXqM4TV0mGPvDJ/srQecexzkGRNAxz++AI09/RBipkNKqBrlFJ6jf5Q+OOUYflyy38PQNM",
|
||||
"hlmpm8Pc8wlIgSDnto2qctgrXeWtnsqza9wuXfd2Ao9LFJYqMQcKWVEgU5bicWjYYtssUU0sYVoAGoUB",
|
||||
"I+0DG8aCLJ8IoG2c1BCx9A4C4M6fWiVLb4rYCRFVmrStjM651sGE2coGHVpIlJjQj+TrDQ37W5cFKo1p",
|
||||
"T2HzyVXYfQDt8cMtbdhVIR18dknmWaYdfioxnSkmkuUxAxHBcuwpFnJp+l4Vclp1fwNiij2jvW2H4fq4",
|
||||
"4zp2r+I+A+6RbbAAO7yH+PCDrT4+6qFTqArMiwEg8+uCZ9uaNdgL8hVOqbrR4dY3nU1na+NWD+i1MqbN",
|
||||
"FFeuRBraoFEOntqApHeDmB0KQqFkglqjBje8Mtka/HRqFKTpq7ceqdyEa2pKJXpWNJ2AU1SacmKIZZ+7",
|
||||
"7r+t6S7zd0uEjBnUZreDAGTJ0ncYvqOhLuge15jCbO3fjELdhZd8H/9a4zTJeKXTwALD1HEBZrtbbgi0",
|
||||
"YLIDgZYp4i7eukx2YbAt45bJuvYJIf6O6ftdvM+YxqmfJ+024P1xsR5/nhSPLz+xPjQrqd2/SonOEIcH",
|
||||
"u9xk4ej4KNW9UYjHhPhuR2VJ11rfphg3tqr5Py7uEwJ6s3aPXNtp2y4LEf/qi+Uu2ZTrImPraW9OWXAz",
|
||||
"xZzxrPdt79ZMLrg4zLJbFndZaZFu89An2NciDWrtZO63eBzIDG3jYi5bBotma4V2qK/cJVF0PboeXbva",
|
||||
"HQUreDSJfhxdj36M3OjZ8j1mBR+zNOdi3HjCAq1Xknx2UnSTRpPon2huK6x1Ouofrq/PdivmTwhci92i",
|
||||
"WqGCJbLMLIGJFEqqPUe0dhM7OWxkbUsQInFlS323dAQfKNHYP15p4ClwDdxo0Pjwk7tb4O7+yg6MgWn4",
|
||||
"1+1//j1yf2pgc4MKPjJtruyTq5tf4LV0l0nu3UOJag0FUyxHg+oNMIWU0TJmUxnOpUKg6F5xZGdmXc3f",
|
||||
"GoUsd0da23liOpr8+hQR8qMlstS2Ag5lUYelKG5p/2DNsYk9Tct6Q9LKcySpbwehQtW/s9uVtmJ2sbLt",
|
||||
"LLu3pdYubmsHC5zanV4o2GbItpQ6hiVfLKkMqQZTMOdKG3hdT18XzGAMbiQZw3zN38RkYgEyS2mjXT+C",
|
||||
"f/CMrAKCKSUfLQioK3YQMBIY+ChPjbECBpTrX2kgsiG7f+TaWEZ7jL5loE4K6Vdhj3mbKd7RW6uB3TAX",
|
||||
"b08l+yhykWRlilM/HQ0xVc//B4BsfzwaPhG0Nxg7c5NdUH5iJllysXAAg0duloQGTrWsZ3QEv7gOThMy",
|
||||
"KEDHUAovr9u3C+dxR85C6gCy39tOXrt5jIKc4otjgwv7UJcz6u+5FCOoFitbb9ezD4pztNTfoIEUCRGx",
|
||||
"wTHN0EZf4Qc+FC2rMW49+dE2zJWiGiuE0O3HtRXAlfuC4O8yXZ8tm3TGwptuqjWqxM2LQw41RaUtMRoQ",
|
||||
"xNE7x1folJr/6oMSWv3uqNV/G7x6G45PxOJNutlXMzTiX7BsaOk4rFM7PLR+SHhtvPBYfW024VDs6/Iq",
|
||||
"fFm1RNtw2xdav1WadUVinzY/4SXVaCv3Hg0mLMvIxUuN9uskUkQZYNIVyJ7P87t0qwa/gEOfqpzKZb1y",
|
||||
"jvPYxqvag5kgAqgi+NyMDC4fvKoZ7IDQ9T7LqvpGVwDxGaorhGs8K8qXgUl3sDkIKW/PfXgfWAQ+Voo6",
|
||||
"A1bGT/7XgTjc1veFPOSA3F2Zzx1zay2cFnZD6hzXs99eZ7zz07rLe6Id2g2sIJo2w0oQE+SaPuVF6T/u",
|
||||
"KWOd57re27dJbhpmi89qHgZzJXO7phHZ34z45aOdutNRvnOf+l0i9rSGa88ceBxG+qMOKfLCBWUToPxn",
|
||||
"IHr85H8dCFDVx80XDFCdj6h79OSZvZCX1Ko4LUqFdDrmwqBSpbsGuxgLfY76QaTOS/3lk/3cetfrbiom",
|
||||
"e838w7OauVYaPDKKGf5bl9FzNVBBS/p+W/8hhrw1TBkNzEWKUolqbsF1NQgYQctDIC+1gRnakcCuwW9R",
|
||||
"pP77gAvF2e7XB4NC7fNizCpxyTT468AX2svbGmH8RP8cCNF10rxQfN6Xv1q569xB2Ul+WkTuKm9cXRkO",
|
||||
"8+BTDu5z3y+Yy1W3YKqrJCqZfDkEd817hYlUqX3rLh9AYc54IHr/7MX6IwFQ38aeBoXzeMehK6aWdrF9",
|
||||
"RcRA40O8dV/wmS0QZmsomNZcLKzpMqYNrYUZS+6B+f2j4N0AaWvvrdAZbnB6BvT264IOqZx953mZR5O3",
|
||||
"19fX9lNZ/+dJ10JnaJjcf/EY0DF9LcBIcJ+feKO9rpqIv1xfv3n5UacqJva2qrfVoudQfjc9Dutavd+4",
|
||||
"G4hKpC2feTmmOFRGia4gwEVvaNZINTQ3VSObF/YGhhvtbyLDJbUrfNr19PlrrO6Hec/czg6psahY7bZs",
|
||||
"L6a82mz+HwAA//8=",
|
||||
}
|
||||
|
||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||
|
||||
@@ -14,11 +14,10 @@ const maxNudges = 3
|
||||
const nudge = "You must call the report tool (mcp__byre__report) before ending your turn."
|
||||
|
||||
type Report struct {
|
||||
Done []string `json:"done"`
|
||||
OpenQuestions []string `json:"open_questions"`
|
||||
NextSteps []string `json:"next_steps"`
|
||||
Risks []string `json:"risks"`
|
||||
Deviations []string `json:"deviations"`
|
||||
Done []string `json:"done"`
|
||||
NextSteps []string `json:"next_steps"`
|
||||
Risks []string `json:"risks"`
|
||||
Deviations []string `json:"deviations"`
|
||||
}
|
||||
|
||||
type Gate struct {
|
||||
@@ -50,7 +49,6 @@ func (g *Gate) Tool() mcp.Tool {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"done": {"type": "array", "items": {"type": "string"}},
|
||||
"open_questions": {"type": "array", "items": {"type": "string"}},
|
||||
"next_steps": {"type": "array", "items": {"type": "string"}},
|
||||
"risks": {"type": "array", "items": {"type": "string"}},
|
||||
"deviations": {"type": "array", "items": {"type": "string"}}
|
||||
|
||||
+12
-24
@@ -145,54 +145,42 @@ func checkResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxStat
|
||||
if c.Text != nil {
|
||||
text = strings.TrimSpace(*c.Text)
|
||||
}
|
||||
question := item.Kind == api.InboxItemKindQuestion
|
||||
question := item.Kind == api.Question
|
||||
bad := func(format string, args ...any) (api.InboxState, *httpError) {
|
||||
return "", &httpError{http.StatusBadRequest, fmt.Errorf("item %s: "+format, append([]any{item.Id}, args...)...)}
|
||||
}
|
||||
|
||||
switch c.Type {
|
||||
case api.InboxResponseTypeOption:
|
||||
case api.Option:
|
||||
if !question {
|
||||
return bad("only questions have options")
|
||||
}
|
||||
if c.Option == nil || !hasOption(item, *c.Option) {
|
||||
return bad("option must be one of the question's options")
|
||||
}
|
||||
return api.Answered, nil
|
||||
|
||||
case api.InboxResponseTypeFreeForm:
|
||||
if !question {
|
||||
return bad("only questions take answers")
|
||||
}
|
||||
if text == "" {
|
||||
return bad("free_form needs text")
|
||||
if text != "" {
|
||||
return bad("option takes no text; use other")
|
||||
}
|
||||
return api.Answered, nil
|
||||
|
||||
case api.InboxResponseTypeQuestion:
|
||||
if !question {
|
||||
return bad("only questions take follow-up questions")
|
||||
case api.Other:
|
||||
if item.Kind == api.Report {
|
||||
return bad("reports are only dismissed")
|
||||
}
|
||||
if text == "" {
|
||||
return bad("question needs text")
|
||||
return bad("other needs text")
|
||||
}
|
||||
return api.Open, nil
|
||||
return api.Answered, nil
|
||||
|
||||
case api.InboxResponseTypeRedirect:
|
||||
if text == "" {
|
||||
return bad("redirect needs text")
|
||||
}
|
||||
return api.Redirected, nil
|
||||
|
||||
case api.InboxResponseTypeSnooze:
|
||||
case api.Snooze:
|
||||
if c.SnoozeUntil == nil || !c.SnoozeUntil.After(time.Now()) {
|
||||
return bad("snooze needs a future snooze_until")
|
||||
}
|
||||
return api.Open, nil
|
||||
|
||||
case api.InboxResponseTypeDismiss:
|
||||
case api.Dismiss:
|
||||
if question {
|
||||
return bad("questions need an answer or a redirect, not a dismissal")
|
||||
return bad("questions need an option or other, not a dismissal")
|
||||
}
|
||||
return api.Dismissed, nil
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ import (
|
||||
func TestCheckResponse(t *testing.T) {
|
||||
question := api.InboxItem{
|
||||
Id: "q1",
|
||||
Kind: api.InboxItemKindQuestion,
|
||||
Kind: api.Question,
|
||||
State: api.Open,
|
||||
Payload: map[string]any{"options": []any{"red", "blue"}},
|
||||
}
|
||||
report := api.InboxItem{Id: "r1", Kind: api.InboxItemKindReport, State: api.Open, Payload: map[string]any{}}
|
||||
report := api.InboxItem{Id: "r1", Kind: api.Report, State: api.Open, Payload: map[string]any{}}
|
||||
failure := api.InboxItem{Id: "e1", Kind: api.SessionError, State: api.Open, Payload: map[string]any{}}
|
||||
closed := question
|
||||
closed.State = api.Answered
|
||||
|
||||
@@ -30,21 +31,20 @@ func TestCheckResponse(t *testing.T) {
|
||||
state api.InboxState
|
||||
status int
|
||||
}{
|
||||
{"option", question, api.InboxResponseCreate{Type: api.InboxResponseTypeOption, Option: str("blue")}, api.Answered, 0},
|
||||
{"option with note", question, api.InboxResponseCreate{Type: api.InboxResponseTypeOption, Option: str("red"), Text: str("but darker")}, api.Answered, 0},
|
||||
{"unknown option", question, api.InboxResponseCreate{Type: api.InboxResponseTypeOption, Option: str("green")}, "", http.StatusBadRequest},
|
||||
{"missing option", question, api.InboxResponseCreate{Type: api.InboxResponseTypeOption}, "", http.StatusBadRequest},
|
||||
{"free form", question, api.InboxResponseCreate{Type: api.InboxResponseTypeFreeForm, Text: str("green")}, api.Answered, 0},
|
||||
{"empty free form", question, api.InboxResponseCreate{Type: api.InboxResponseTypeFreeForm, Text: str(" ")}, "", http.StatusBadRequest},
|
||||
{"follow-up question", question, api.InboxResponseCreate{Type: api.InboxResponseTypeQuestion, Text: str("why?")}, api.Open, 0},
|
||||
{"redirect question", question, api.InboxResponseCreate{Type: api.InboxResponseTypeRedirect, Text: str("stop")}, api.Redirected, 0},
|
||||
{"redirect report", report, api.InboxResponseCreate{Type: api.InboxResponseTypeRedirect, Text: str("stop")}, api.Redirected, 0},
|
||||
{"snooze", question, api.InboxResponseCreate{Type: api.InboxResponseTypeSnooze, SnoozeUntil: &future}, api.Open, 0},
|
||||
{"snooze into past", question, api.InboxResponseCreate{Type: api.InboxResponseTypeSnooze, SnoozeUntil: &past}, "", http.StatusBadRequest},
|
||||
{"dismiss report", report, api.InboxResponseCreate{Type: api.InboxResponseTypeDismiss}, api.Dismissed, 0},
|
||||
{"dismiss question", question, api.InboxResponseCreate{Type: api.InboxResponseTypeDismiss}, "", http.StatusBadRequest},
|
||||
{"option on report", report, api.InboxResponseCreate{Type: api.InboxResponseTypeOption, Option: str("red")}, "", http.StatusBadRequest},
|
||||
{"already answered", closed, api.InboxResponseCreate{Type: api.InboxResponseTypeOption, Option: str("red")}, "", http.StatusConflict},
|
||||
{"option", question, api.InboxResponseCreate{Type: api.Option, Option: str("blue")}, api.Answered, 0},
|
||||
{"option with text", question, api.InboxResponseCreate{Type: api.Option, Option: str("red"), Text: str("but darker")}, "", http.StatusBadRequest},
|
||||
{"unknown option", question, api.InboxResponseCreate{Type: api.Option, Option: str("green")}, "", http.StatusBadRequest},
|
||||
{"missing option", question, api.InboxResponseCreate{Type: api.Option}, "", http.StatusBadRequest},
|
||||
{"other", question, api.InboxResponseCreate{Type: api.Other, Text: str("green")}, api.Answered, 0},
|
||||
{"empty other", question, api.InboxResponseCreate{Type: api.Other, Text: str(" ")}, "", http.StatusBadRequest},
|
||||
{"other on error", failure, api.InboxResponseCreate{Type: api.Other, Text: str("retry")}, api.Answered, 0},
|
||||
{"other on report", report, api.InboxResponseCreate{Type: api.Other, Text: str("stop")}, "", http.StatusBadRequest},
|
||||
{"snooze", question, api.InboxResponseCreate{Type: api.Snooze, SnoozeUntil: &future}, api.Open, 0},
|
||||
{"snooze into past", question, api.InboxResponseCreate{Type: api.Snooze, SnoozeUntil: &past}, "", http.StatusBadRequest},
|
||||
{"dismiss report", report, api.InboxResponseCreate{Type: api.Dismiss}, api.Dismissed, 0},
|
||||
{"dismiss question", question, api.InboxResponseCreate{Type: api.Dismiss}, "", http.StatusBadRequest},
|
||||
{"option on report", report, api.InboxResponseCreate{Type: api.Option, Option: str("red")}, "", http.StatusBadRequest},
|
||||
{"already answered", closed, api.InboxResponseCreate{Type: api.Option, Option: str("red")}, "", http.StatusConflict},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
@@ -24,7 +24,7 @@ func priorityRank(p api.InboxItemPriority) int {
|
||||
|
||||
func DeliverableResponse(t api.InboxResponseType) bool {
|
||||
switch t {
|
||||
case api.InboxResponseTypeOption, api.InboxResponseTypeFreeForm, api.InboxResponseTypeRedirect, api.InboxResponseTypeQuestion:
|
||||
case api.Option, api.Other:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -116,7 +116,7 @@ func (s *Store) ListInbox(ctx context.Context, f InboxFilter) ([]api.InboxItem,
|
||||
args = append(args, now())
|
||||
}
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id FROM inbox_items WHERE `+strings.Join(where, " AND ")+` ORDER BY priority, created_at`, args...)
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id FROM inbox_items WHERE `+strings.Join(where, " AND ")+` ORDER BY created_at`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -195,9 +195,9 @@ func (s *Store) AwaitingHuman(ctx context.Context, sessionID string) (int, error
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*) FROM inbox_items i
|
||||
WHERE i.agent_session_id = ? AND i.kind = ? AND i.blocking AND i.state = ?
|
||||
AND NOT EXISTS (SELECT 1 FROM inbox_responses r WHERE r.item_id = i.id AND r.type IN (?, ?, ?, ?))
|
||||
`, sessionID, string(api.InboxItemKindQuestion), string(api.Open),
|
||||
string(api.InboxResponseTypeOption), string(api.InboxResponseTypeFreeForm), string(api.InboxResponseTypeRedirect), string(api.InboxResponseTypeQuestion)).Scan(&n)
|
||||
AND NOT EXISTS (SELECT 1 FROM inbox_responses r WHERE r.item_id = i.id AND r.type IN (?, ?))
|
||||
`, sessionID, string(api.Question), string(api.Open),
|
||||
string(api.Option), string(api.Other)).Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -209,9 +209,9 @@ type Pending struct {
|
||||
func (s *Store) Undelivered(ctx context.Context, sessionID string) ([]Pending, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT r.id, r.item_id FROM inbox_responses r JOIN inbox_items i ON i.id = r.item_id
|
||||
WHERE i.agent_session_id = ? AND r.delivered_at IS NULL AND r.type IN (?, ?, ?, ?)
|
||||
WHERE i.agent_session_id = ? AND r.delivered_at IS NULL AND r.type IN (?, ?)
|
||||
ORDER BY r.created_at
|
||||
`, sessionID, string(api.InboxResponseTypeOption), string(api.InboxResponseTypeFreeForm), string(api.InboxResponseTypeRedirect), string(api.InboxResponseTypeQuestion))
|
||||
`, sessionID, string(api.Option), string(api.Other))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -122,6 +122,9 @@ CREATE TABLE turn_queue (
|
||||
text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
`, `
|
||||
UPDATE inbox_responses SET type = 'other' WHERE type IN ('free_form', 'redirect', 'question');
|
||||
UPDATE inbox_items SET state = 'answered' WHERE state = 'redirected';
|
||||
`}
|
||||
|
||||
type Store struct {
|
||||
|
||||
Reference in New Issue
Block a user