Render Markdown, add ask_human context, simplify inbox responses, group inbox by task

This commit is contained in:
Ian Gulliver
2026-10-14 10:37:04 -07:00
parent e9fd7de556
commit ca2644374d
25 changed files with 950 additions and 283 deletions
+25 -19
View File
@@ -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 {