Make every question blocking and drop inbox priority

This commit is contained in:
Ian Gulliver
2026-10-16 08:35:49 -07:00
parent 1427814014
commit 78cca2fcbe
25 changed files with 268 additions and 322 deletions
+11 -28
View File
@@ -25,7 +25,6 @@ type askArgs struct {
Question string `json:"question"`
WhyBlocked string `json:"why_blocked"`
Attachments []api.Attachment `json:"attachments"`
Blocking *bool `json:"blocking"`
ThreadID string `json:"thread_id"`
}
@@ -43,8 +42,7 @@ func (m *Manager) askTool(l *live) mcp.Tool {
"its first line is a short name, and the lines indented beneath it say what it would concretely do, " +
"its example, code, commands and trade-offs, so the human never has to match a paragraph above to an option below. " +
"Text above the list covers only what all the choices share. " +
"If blocking is true, 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. " +
"End your turn after asking; the answer arrives as your next message. " +
"One call is one decision, answered by picking from its own choices. When you have several decisions, such as a design with several choices, " +
"never bundle them into one call that asks for approval of them all: call this once per decision, each with its own context and choices; they are answered together. " +
"To answer a follow-up question from the human, call this again with the thread_id they gave you.",
@@ -70,10 +68,9 @@ func (m *Manager) askTool(l *live) mcp.Tool {
},
"required": ["kind"]
}},
"blocking": {"type": "boolean"},
"thread_id": {"type": "string"}
},
"required": ["context", "question", "why_blocked", "blocking"]
"required": ["context", "question", "why_blocked"]
}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
return m.ask(ctx, l, raw)
@@ -99,11 +96,6 @@ func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string
thread = id
}
priority := api.Fyi
if *args.Blocking {
priority = api.Blocked
}
attachments := args.Attachments
if attachments == nil {
attachments = []api.Attachment{}
@@ -129,21 +121,17 @@ 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.Question, priority, *args.Blocking, store.ToPayload(payload)))
err = m.store.CreateInboxItem(ctx, m.newItem(l, id, thread, api.Question, store.ToPayload(payload)))
if err != nil {
return "", err
}
m.mu.Lock()
l.asked = true
l.askedBlock = l.askedBlock || *args.Blocking
m.mu.Unlock()
cancel := fmt.Sprintf("If the human answers it some other way, such as in a chat message, withdraw it with cancel_question and question_id %q.", id)
if *args.Blocking {
return fmt.Sprintf("Question filed in the human's inbox. question_id: %s. It is blocking: end your turn now. The answer will arrive as your next message. %s", id, cancel), nil
}
return fmt.Sprintf("Question filed in the human's inbox. question_id: %s. Continue with work that doesn't depend on it; the answer will arrive as a later message. %s", id, cancel), nil
return fmt.Sprintf("Question filed in the human's inbox. question_id: %s. End your turn now. The answer will arrive as your next message. "+
"If the human answers it some other way, such as in a chat message, withdraw it with cancel_question and question_id %q.", id, id), nil
}
type cancelArgs struct {
@@ -233,9 +221,6 @@ func (m *Manager) validateAsk(ctx context.Context, l *live, args askArgs) error
if radios == len(choices) && len(choices) > 0 && recommendedRadios != 1 {
problems = append(problems, "mark exactly one pick-one choice as recommended with `- (*) choice`")
}
if args.Blocking == nil {
problems = append(problems, "blocking is required")
}
for i, a := range args.Attachments {
if !slices.Contains(attachmentKinds, a.Kind) {
problems = append(problems, fmt.Sprintf("attachments[%d].kind %q is not one of %v", i, a.Kind, attachmentKinds))
@@ -262,7 +247,7 @@ func (m *Manager) validateAsk(ctx context.Context, l *live, args askArgs) error
func (m *Manager) fileError(l *live, msg string) {
id := store.NewID()
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.SessionError, api.Blocked, true, map[string]any{"error": msg}))
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.SessionError, map[string]any{"error": msg}))
if err != nil {
slog.Error("file session error", "session", l.id, "err", err)
}
@@ -271,13 +256,13 @@ 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.RateLimited, api.Blocked, true, payload))
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.RateLimited, payload))
if err != nil {
slog.Error("file rate limit", "session", l.id, "err", err)
}
}
func (m *Manager) newItem(l *live, id, thread string, kind api.InboxItemKind, priority api.InboxItemPriority, blocking bool, payload map[string]any) api.InboxItem {
func (m *Manager) newItem(l *live, id, thread string, kind api.InboxItemKind, payload map[string]any) api.InboxItem {
return api.InboxItem{
Id: id,
ProjectId: l.projectID,
@@ -285,8 +270,6 @@ func (m *Manager) newItem(l *live, id, thread string, kind api.InboxItemKind, pr
AgentSessionId: l.id,
ThreadId: thread,
Kind: kind,
Priority: priority,
Blocking: blocking,
Payload: payload,
State: api.Open,
CreatedAt: time.Now().UTC(),
@@ -329,7 +312,7 @@ func (m *Manager) Deliver(ctx context.Context, sessionID string) error {
return err
}
if waiting > 0 {
slog.Info("deliver held: blocking question waiting", "session", sessionID, "waiting", waiting, "responses", ids)
slog.Info("deliver held: question waiting", "session", sessionID, "waiting", waiting, "responses", ids)
return nil
}
@@ -399,11 +382,11 @@ func (m *Manager) responsesMidTurn(ctx context.Context, l *live) string {
waiting, err := m.store.AwaitingHuman(ctx, l.id)
if err != nil {
slog.Error("count blocking questions", "session", l.id, "err", err)
slog.Error("count waiting questions", "session", l.id, "err", err)
return ""
}
if waiting > 0 {
slog.Info("deliver held mid-turn: blocking question waiting", "session", l.id, "waiting", waiting, "responses", ids)
slog.Info("deliver held mid-turn: question waiting", "session", l.id, "waiting", waiting, "responses", ids)
return ""
}