fix(flows): resolve-before-you-ask — wire runtime lookups instead of asking (#5012)

This commit is contained in:
Cyrus Gray
2026-07-17 18:31:13 +05:30
committed by GitHub
parent 106729f1d0
commit 2fd56ee198
2 changed files with 86 additions and 9 deletions
@@ -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);
@@ -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.<id_field>`).
- **"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"