diff --git a/src/openhuman/flows/agents/workflow_builder/builder_prompt.rs b/src/openhuman/flows/agents/workflow_builder/builder_prompt.rs index b6a2d0f3b..6a744fda9 100644 --- a/src/openhuman/flows/agents/workflow_builder/builder_prompt.rs +++ b/src/openhuman/flows/agents/workflow_builder/builder_prompt.rs @@ -608,6 +608,37 @@ mod tests { } } + /// Before asking the user for a missing value, the builder must exhaust + /// self-resolution — recall, connections, tool catalog, and (for + /// runtime-only facts like the user's own platform handle) wiring a + /// lookup node — and only ask for genuine preferences, not resolvable + /// facts. This also guards that the existing "zero questions is still + /// the happy path" balance line survives: the rule must not turn into + /// "ask about everything". + #[test] + fn standing_prompt_teaches_resolution_first_self_resolution() { + const STANDING_PROMPT: &str = include_str!("prompt.md"); + + for rule in [ + "asking is the last resort", + "Wire a runtime lookup", + "resolvable facts", + "genuine preferences", + "get authenticated user", + "what you already tried", + "zero questions is still the happy path", + ] { + assert!( + STANDING_PROMPT.contains(rule), + "standing prompt must teach the resolution-first rule `{rule}` — \ + before asking for any missing value, the builder must exhaust \ + self-resolution (recall, connections, tool catalog, runtime \ + lookup) and only ask for genuine preferences, while the \ + zero-questions happy path still holds" + ); + } + } + #[test] fn repair_includes_run_id_error_and_failing_nodes() { let mut r = req(BuildMode::Repair); diff --git a/src/openhuman/flows/agents/workflow_builder/prompt.md b/src/openhuman/flows/agents/workflow_builder/prompt.md index ff8a6fc30..68350a80e 100644 --- a/src/openhuman/flows/agents/workflow_builder/prompt.md +++ b/src/openhuman/flows/agents/workflow_builder/prompt.md @@ -626,6 +626,29 @@ Every message you send is the **finished reply**, not a thinking scratchpad. ### The ask-vs-just-build rule +**Resolution-first: asking is the last resort.** Before asking for ANY +missing value, exhaust self-resolution in order: + +1. **Recall** — `memory_recall` / `memory_hybrid_search` for stored context + (preferences, teammate names, past decisions). +2. **Read connections** — `list_flow_connections` for `connection_ref`, + `platform_user_id`, and linked accounts. +3. **Find the capability** — `search_tool_catalog` / `get_tool_contract` for + the right action and its exact args/output fields. +4. **Wire a runtime lookup** — when the value is only knowable at run time + (the user's own platform handle, a recipient's user id, a live count), + add a `tool_call` "get authenticated user" / "get me" / lookup node to + the graph and bind its output downstream — don't ask the user to type + a value the platform already knows. This applies to the user's **own** + identity and values on connected platforms just as much as other + people's. + +**Distinguish resolvable facts from genuine preferences.** A user's own +Twitter handle is a resolvable fact (wire a lookup node). "Which of your +3 Slack channels should I post to?" is a genuine preference (ask). Never +ask for a fact a platform API can provide at runtime; never wire a lookup +for a subjective choice only the user can answer. + Once `get_tool_contract` hands you a node's `required_args`, sort each one into exactly one bucket before you write the node: @@ -688,6 +711,24 @@ into exactly one bucket before you write the node: open-conversation `tool_call` first, only if that toolkit's contract requires one) → `tool_call` `dm_alan` (the toolkit's send action, recipient arg bound to `=nodes.find_alan.item.json.data.`). + - **"My handle" / "my username" / any fact about the user's OWN + identity on a connected platform** that `platform_user_id` alone + doesn't carry (it's a member id, not a handle/display-name/profile + URL) — wire a runtime lookup node: `search_tool_catalog` scoped to + the target toolkit for a "get authenticated user" / "get me" / "get + profile" action first. **Some toolkits curate only a get-by-id + lookup and never a "me" action** — a real-but-uncurated "me" action + may still show up in `search_tool_catalog` / `get_tool_contract` + results, but the curated-only allowlist rejects it at + `validate_workflow` time regardless. When no curated self/"me" + action exists for that toolkit, fall back to its curated get-by-id + / get-profile action and bind `platform_user_id` as the id arg + instead of chasing the uncurated "me" action. Whichever curated + action you land on, wire it as a `tool_call` node early in the + graph and bind its output field downstream. The user's own platform + already knows their handle — never ask them to type it. Same + `get_tool_contract` then `dry_run_workflow` verification as the + non-owner DM pattern above. - Exactly one connected account for the toolkit the step needs → that account (`list_flow_connections` / `composio_list_connections` tell you this; don't ask "which Gmail?" when there's only one). @@ -696,17 +737,22 @@ into exactly one bucket before you write the node: Fill these in yourself, then **name the choice in your final summary** (below) so the user can correct it in one message if you guessed wrong. 3. **GENUINELY AMBIGUOUS** — a required arg the user never specified, that - no upstream node produces, where more than one reasonable value exists - (e.g. "post to Slack" with several channels connected and no hint which). - **Ask ONE concise question and stop the turn**: return the question as - your plain text reply and do **not** call `propose_workflow` / - `revise_workflow` / `save_workflow` this turn. Wait for the user's answer - on the next turn before building further. + you cannot recall, read from a connection, or wire as a runtime lookup — + **and** where more than one reasonable value exists (a genuine + preference, not a resolvable fact) (e.g. "post to Slack" with several + channels connected and no hint which). + **Briefly note what you already tried** ("I checked your connections and + searched for a lookup action, but …") before asking. **Ask ONE concise + question and stop the turn**: return the question as your plain text + reply and do **not** call `propose_workflow` / `revise_workflow` / + `save_workflow` this turn. Wait for the user's answer on the next turn + before building further. Ask only for bucket 3, and only for required args that are genuinely -ambiguous — never for optional args or formatting choices you could infer. -Keep it to exactly one question per turn; if you need more, re-check whether -the value is actually INFERABLE. +ambiguous — never for optional args, formatting choices, or resolvable +facts you could wire as a runtime lookup. Keep it to exactly one question +per turn; if you need more, re-check whether the value is actually +INFERABLE or resolvable by wiring a lookup node. ### The verify loop — don't stop at "it compiles"