| index 7ba3399..951af4c 100644 |
| --- a/internal/bot/brain.go |
| +++ b/internal/bot/brain.go |
| @@ -129,7 +129,9 @@ func (r *Runtime) systemPrompt() string { |
| "(domains, code, payments) with the service_op tool — its description is the complete list of " + |
| "what each service can do. Writes always go through the member's confirmation tap; nothing runs " + |
| "from your proposal alone. If they haven't connected the service, the tap will tell them so — " + |
| - "proposing is still fine, or point them at Integrations (+ New chat → Integrations) to connect." |
| + "proposing is still fine, or point them at Integrations (+ New chat → Integrations) to connect. " + |
| + "You cannot fetch, pull up, check, or list anything from a service yourself — NEVER say you will; " + |
| + "the only way data moves is the member tapping a service_op chip you attach in that same reply." |
| if r.appGuide != "" { |
| s += "\n\nYou CAN build small personal mini-apps for members — private tools like trackers, " + |
| "lists, counters, tiny games — via the create_app tool. When someone asks for an app (or " + |
| @@ -169,7 +171,7 @@ func (r *Runtime) systemPrompt() string { |
| // instance serves the authoring guide. Everything except create_app becomes a |
| // CONSENT CHIP — the client whitelists, and nothing runs until a tap. |
| func (r *Runtime) replyTools() []anthTool { |
| - tools := append(chipTools(), serviceOpTool()) |
| + tools := retryTools() |
| if r.appGuide != "" { |
| tools = append(tools, createAppTool()) |
| } |
| @@ -179,6 +181,16 @@ func (r *Runtime) replyTools() []anthTool { |
| return tools |
| } |
| |
| +// retryTools is the promised-button forced retry's palette: every CONSENT |
| +// CHIP the model may attach — the basic chips AND service_op (the live stall |
| +// this fixes: "let me pull up the open issues" retried into a palette that |
| +// couldn't propose the op). Never the builders: a forced retry must not be |
| +// able to trigger a build or mint a bot, but forcing a chip is safe — nothing |
| +// runs without the member's tap. |
| +func retryTools() []anthTool { |
| + return append(chipTools(), serviceOpTool()) |
| +} |
| + |
| // serviceOpTool is the connected-services proposal tool. Its description IS |
| // the embedded registry — the closed list of ops the manifests declare is the |
| // closed list the model can propose, and actionsFromToolUse re-checks every |
| @@ -258,7 +270,13 @@ func (r *Runtime) buildContext(tc *ThreadCtx) ([]anthMessage, error) { |
| return out, nil |
| } |
| |
| -var promisedButton = regexp.MustCompile(`(?i)\b(tap|button|chip)\b`) |
| +// promisedButton catches a reply that PROMISES an action instead of attaching |
| +// one: the classic "tap the button" with no tool call, and the fetch-promise |
| +// family ("Great! Let me pull up the open issues for you." — the live stall |
| +// that shipped) — the model cannot fetch anything itself, so a promise with |
| +// no tool call is always wrong. Matching generously is cheap insurance: the |
| +// forced retry only ATTACHES a consent chip; nothing runs without the tap. |
| +var promisedButton = regexp.MustCompile(`(?i)(\b(tap|button|chip)\b|\b(let me|i'?ll|i will|one moment while i|one sec while i)\s+(just\s+)?(pull|fetch|grab|check|look|get|list|query)\b)`) |
| |
| // modelReply is the conversational path: build context, ask Claude (adaptive |
| // thinking, chip tools), self-heal the promised-button case, return the reply. |
| @@ -375,8 +393,7 @@ func (r *Runtime) modelReply(tc *ThreadCtx) (Reply, bool) { |
| // Forced tool_choice requires thinking off; omitting the field is |
| // "off" and every model accepts it (an explicit thinking type here |
| // would 400 on Haiku, which is exactly the bug this path must avoid). |
| - // Chip tools only: a forced retry must never trigger a build. |
| - Tools: chipTools(), |
| + Tools: retryTools(), |
| ToolChoice: map[string]any{"type": "any"}, |
| Messages: context, |
| }) |