Skip to content

feat(flows): workflow-builder agent-friendliness plumbing — schema-aware sandbox, unified draft handles, reference gates#4881

Merged
senamakel merged 9 commits into
tinyhumansai:mainfrom
senamakel:feat/flows-builder-plumbing
Jul 15, 2026
Merged

feat(flows): workflow-builder agent-friendliness plumbing — schema-aware sandbox, unified draft handles, reference gates#4881
senamakel merged 9 commits into
tinyhumansai:mainfrom
senamakel:feat/flows-builder-plumbing

Conversation

@senamakel

Copy link
Copy Markdown
Member

Summary

  • Transcript audit of live workflow_builder sessions showed ~half of all tool calls wasted on tooling friction, and neither build session ever achieved a clean dry-run. This PR fixes the seven root causes found.
  • Dry-run sandbox now synthesizes typed placeholders from output_parser.schema for plain agent nodes (the fix(flows): builder produces resolvable graphs (dry-run catches null args + correct envelope binding) #4586 fix only covered agent_ref nodes), eliminating the false output_parser: value failed schema validation failures.
  • Builder tools now share one handle model — draft_id | flow_id | graph on edit/validate/dry_run (+ draft_id source on save) — and edit_workflow { flow_id } seeds a real draft and returns draft_id + persisted: false instead of a dead-end proposal that reads like a save.
  • New hard gate rejects connection_refs whose toolkit doesn't match the slug or whose id isn't a real connection of that toolkit (an agent shipped a TikTok connection id on both Twitter and Gmail nodes with every gate green).
  • Earlier/kinder signals: runtime_gate warning on uncurated actions at get_tool_contract/search time, per-keyword fallback ranking for multi-word search_tool_catalog queries, per-op-index edit_workflow parse errors + node_id alias + ordering hint, and honest "unverifiable in sandbox" diagnostics for bindings to Composio tool_call outputs.
  • workflow_builder/prompt.md synced with the new surface; two pre-existing test breakages on main fixed (from feat(flows): Workflows page overhaul — canvas builder, list redesign, Runs/Discoveries sub-pages, privacy cleanup #4874 and feat(flows): agent-friendliness — editing, drafts, safety rails, wider tools #4876).

Problem

Live session transcripts (2026-07-15, staging) show the workflow_builder agent:

  1. Burning ~13 dry-run/propose cycles because agent nodes without agent_ref route to the plain MockLlm in the sandbox (vendor/tinyflows/src/nodes/integration/agent.rs run_turn), whose echo can never satisfy a declared schema — the SchemaAwareMockAgentRunner from fix(flows): builder produces resolvable graphs (dry-run catches null args + correct envelope binding) #4586 never fires for them. One session "fixed" it by inserting fallback-literal transform nodes that degrade the real runtime graph.
  2. Believing edit_workflow { flow_id } persisted its edit (the result is stamped with the real flow id/name, carries no handle and no persistence flag), then validate_workflow { flow_id } validated the old saved graph and returned ok: true, reinforcing the wrong belief — 6 of 9 calls wasted in that session.
  3. Shipping a graph with the TikTok connection id wired onto Twitter and Gmail nodes — no gate validates connection_ref at all.
  4. Building on TWITTER_USER_LOOKUP_ME for ~15 calls before the curated-actions gate finally rejected it at validate time.
  5. Losing 2 calls to edit_workflow op-shape guesses (node_id vs id; add-before-remove ordering).
  6. Concluding (and telling the user) "Gmail's email-fetching actions weren't found in the catalog" because multi-word search_tool_catalog queries return count: 0 with no fallback (GMAIL_FETCH_EMAILS exists).
  7. Re-wiring an already-correct binding 3 times because the preflight error for a required arg bound to a Composio tool_call's output (which the sandbox can never produce) says "wire each from an upstream node's output".

Solution

One commit per workstream:

  • fix(flows): sandbox mock LLM honors output_parser.schema for plain agent nodes — new SchemaAwareMockLlm (mirrors the runner's placeholder synthesis; vendored echo shape when no schema), wired into DryRunWorkflowTool and validate_required_arg_resolvability (which is why propose_workflow was also rejecting correct graphs).
  • feat(flows): unified draft_id|flow_id|graph handles + explicit persistence state — acceptance matrix unified with draft > flow > graph precedence; edit_workflow { flow_id } seeds a new draft via the same op the canvas uses, never mutates the flow, and returns draft_id/flow_id/persisted: false/a next hint; proposals always carry persisted: false; save_workflow takes draft_id as source.
  • feat(flows): connection_ref hard gate + early runtime-gate warningsvalidate_connection_refs in run_builder_gates (toolkit-mismatch check is I/O-free; existence check is scoped to the matching toolkit and fail-open when the connection list is unavailable); get_tool_contract puts a top-level runtime_gate warning on uncurated actions of curated toolkits; search_tool_catalog rows carry runtime_gated: true.
  • feat(flows): agent-friendly edit_workflow op errors — serde node_id/new_node_id aliases in the vendored GraphOp, element-by-element parsing with op index + expected shape in errors, ordering hint on add-node-exists. Bumps the vendor/tinyflows submodule (branch feat/edit-workflow-op-aliases, pushed).
  • feat(flows): per-token fallback ranking for search_tool_catalog — 0-hit multi-token queries fall back to per-keyword OR scoring (top 10, curated tiebreak) with an explanatory note; single-token behavior unchanged.
  • feat(flows): honest sandbox diagnostics for Composio-upstream bindings — null required args whose expression targets a Composio tool_call node are flagged unverifiable: true + upstream_tool_call with pointers to get_tool_contract output_fields / get_tool_output_sample; the propose/save gate no longer blocks on this class (dry-run remains the reporting surface).
  • docs(flows): sync workflow_builder prompt — sandbox table, handle matrix, per-op shapes, new gates (+59 lines, every claim verified against code).
  • Two test(...) commits fix pre-existing breakage on main: feat(flows): Workflows page overhaul — canvas builder, list redesign, Runs/Discoveries sub-pages, privacy cleanup #4874 added the list_all_runs controller without updating the schema-coverage assertions; feat(flows): agent-friendliness — editing, drafts, safety rails, wider tools #4876 widened workflow_builder's tool belt to 29 without updating the loader scope test. Both fail on a clean main checkout.

Tradeoffs: the dry run still returns ok: false on unverifiable Composio-upstream bindings (silently passing could hide typos) — only the propose/save hard gate downgrades them; the connection-existence check fails open when the connections backend is unreachable so offline/test runs don't false-positive.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy — ~30 new Rust tests across gates, tools, sandbox, and vendored graph_ops
  • Diff coverage ≥ 80% — changed lines are Rust-only; targeted suites green locally (flows:: + tinyflows::caps + agent_registry loader: 573 passed; vendored crate: 340 passed)
  • Coverage matrix updated — N/A: agent-tooling behavior change, no feature rows added/removed
  • All affected feature IDs from the matrix are listed in the PR description under ## RelatedN/A: no matrix rows affected
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated — N/A: no release-cut surface touched
  • Linked issue closed via Closes #NNNN/A: no tracking issue; driven by live transcript audit

Impact

  • Desktop + headless core (flows domain, agent tooling). No frontend changes.
  • vendor/tinyflows submodule pointer advances to e5327de (pushed to tinyhumansai/tinyflows branch feat/edit-workflow-op-aliases).
  • Behavior: propose/save gates get stricter on connection_refs and looser on sandbox-unverifiable bindings; edit_workflow { flow_id } now creates a draft (visible to the canvas) instead of returning a detached proposal.

Related

  • Closes:
  • Follow-up PR(s)/TODOs: flow_discovery agent sometimes ends without calling its terminal suggest_workflows tool (seen in the same transcript audit); semantic catalog ranking beyond the per-token fallback.

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/flows-builder-plumbing
  • Commit SHA: dc8fd6c

Validation Run

  • pnpm --filter openhuman-app format:check — N/A (no app changes)
  • pnpm typecheck — N/A (no TS changes)
  • Focused tests: cargo test --lib -- flows:: tinyflows::caps agent_registry::agents::loader → 573 passed / 0 failed; cargo test in vendor/tinyflows → 340 passed
  • Rust fmt/check (if changed): cargo fmt + GGML_NATIVE=OFF cargo check clean (pre-existing warnings only)
  • Tauri fmt/check (if changed): N/A (shell untouched)

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: builder tools validate more (connection_refs, curated actions), report state honestly (drafts, persistence), and stop false-failing correct graphs in the sandbox.
  • User-visible effect: workflow copilot builds/edits converge in far fewer tool calls; run-blocking misconfigurations are caught at propose time instead of first real run.

Parity Contract

  • Legacy behavior preserved: schema-less agent dry-run echo shape unchanged; missing connection_ref still allowed (prompts at first run); UI/RPC canvas save path stays permissive.
  • Guard/fallback/dispatch parity checks: connection existence check fails open offline; single-token catalog search identical; save_workflow inline-graph path unchanged.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this
  • Resolution (closed/superseded/updated): N/A

…ent nodes

The vendored agent node only routes to an AgentRunner when the node
carries an agent_ref; builder-generated agent nodes carry none, so in
the dry-run sandbox they hit the plain MockLlm whose echo can never
satisfy a declared output_parser.schema. The sub-port then failed with
'output_parser: value failed schema validation after auto-fix' on
correctly-built graphs (SchemaAwareMockAgentRunner from tinyhumansai#4586 covered
only the agent_ref path).

Add SchemaAwareMockLlm mirroring the runner's placeholder synthesis and
wire it into DryRunWorkflowTool and the propose/save gate sandbox
(validate_required_arg_resolvability), which was rejecting the same
correct graphs at propose_workflow time.
…tence state on builder tools

WS2 of the workflow_builder audit fix. The builder tools had an asymmetric,
broken state model: edit_workflow{flow_id} applied ops to the saved graph but
persisted nothing and returned no handle, so the edit was unreachable and read
as "written onto the flow"; validate/dry_run/save each accepted a different
subset of handles. An agent wasted 6 of 9 tool calls believing an edit was
saved when it wasn't.

Unified acceptance matrix (all with draft_id > flow_id precedence):

  tool               before                     after
  ----               ------                     -----
  edit_workflow      draft|flow|graph           draft|flow|graph (flow seeds a NEW draft)
  validate_workflow  flow|graph                 draft|flow|graph
  dry_run_workflow   draft|graph                draft|flow|graph
  save_workflow      flow_id + inline graph     flow_id (target) + draft|graph source

Core fix: edit_workflow{flow_id} now seeds a NEW draft from the flow's graph
(via flows_draft_create, linked to the flow), applies the ops, writes them back
to that draft, and returns draft_id + flow_id + persisted:false + a one-line
`next` hint pointing the agent at the draft to iterate on and save_workflow
{flow_id, draft_id} to persist. The saved flow is never touched.

Explicit persistence state: build_builder_proposal now stamps persisted:false
unconditionally and echoes optional draft_id/flow_id context; propose_workflow's
inline payload and save_workflow's output (persisted:true) match, so no proposal
can read as a save confirmation.

Tests: edit-by-flow seeds a retrievable draft marked unpersisted with the flow
untouched; dry_run/validate by the new handles; save by draft_id persists the
draft graph onto the flow; propose/revise proposals carry persisted:false.
72 builder_tools + tools_tests pass.
…n uncurated actions

Closes two transcript-audit validation gaps that let broken graphs pass every
author-time gate.

1. connection_ref was never validated. The user's connections were
   twitter → composio:twitter:ca_JX6QU88UfSk4, gmail → composio:gmail:ca_vX_WA8FsqNmE,
   tiktok → composio:tiktok:ca_LPCp3WQpaDma. The agent wired
   composio:twitter:ca_LPCp3WQpaDma and composio:gmail:ca_LPCp3WQpaDma (the TIKTOK
   id) onto the Twitter and Gmail tool_call nodes; dry_run/validate/propose all
   returned ok and it failed on the first real run. New hard gate
   `validate_connection_refs` (run right after the binding gate in
   `run_builder_gates`): for every Composio tool_call node with a connection_ref
   it parses composio:<toolkit>:<id>, rejects malformed refs, enforces the
   toolkit segment matches the slug's toolkit, and — when the live connection
   list is reachable — that the id names a real connected account OF THAT
   toolkit, naming the correct ref verbatim ("did you mean ...") or pointing at
   composio_connect when the toolkit has no connection. A missing connection_ref
   stays allowed (prompts at first run). Fail-open on connection-list I/O error:
   the id-existence check is skipped (tracing::debug) while the no-I/O
   toolkit-mismatch check still runs.

2. The curated-action blocker fired too late. get_tool_contract returned
   is_curated:false silently; the agent built and wired the node and only ~15
   tool calls later did validate_workflow reject it. get_tool_contract now
   attaches a top-level `runtime_gate` string (first in the payload) for a
   real-but-uncurated action of a toolkit that ships a curated catalog, and
   search_tool_catalog flags such rows with `runtime_gated: true` (only when
   true). Both reuse a new shared `toolkit_has_curated_catalog` helper so the
   author-time gate and the early warnings cannot drift.
Pre-existing breakage on main: tinyhumansai#4874 added the list_all_runs controller
but the schema-coverage tests (count + expected name lists) were not
updated, so flows::schemas::tests fails on a clean checkout. Fix the
assertions to include it.
…widened agent.toml

Pre-existing breakage on main: tinyhumansai#4876 widened workflow_builder's tool
belt from 18 to 29 tools (editing, drafts, node-kind introspection,
run debugging, create/duplicate) but the loader scope test still
asserted the old 18-tool list, so it fails on a clean checkout.
@senamakel senamakel requested a review from a team July 15, 2026 08:34
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1ea3f1a8-c94d-4f43-b762-8d90ca66b207

📥 Commits

Reviewing files that changed from the base of the PR and between 8800f1e and dc8fd6c.

📒 Files selected for processing (11)
  • src/openhuman/agent_registry/agents/loader.rs
  • src/openhuman/flows/agents/workflow_builder/prompt.md
  • src/openhuman/flows/builder_tools.rs
  • src/openhuman/flows/builder_tools_tests.rs
  • src/openhuman/flows/ops.rs
  • src/openhuman/flows/ops_tests.rs
  • src/openhuman/flows/schemas.rs
  • src/openhuman/flows/tools.rs
  • src/openhuman/flows/tools_tests.rs
  • src/openhuman/tinyflows/caps.rs
  • vendor/tinyflows

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

if flow.enabled {
if let Err(e) = ops::flows_set_enabled(&self.config, &flow.id, false).await {
tracing::warn!(target: "flows", flow_id = %flow.id, error = %e, "[flows] create_workflow: could not force-disable the new flow");
}

P2 Badge Fail if agent-created flows cannot be disabled

For manual/no-auto-trigger graphs flows_create returns an enabled flow, and this follow-up disable is what enforces the tool's "born disabled" safety contract. If flows_set_enabled(..., false) fails after creation, the code only logs a warning and still returns "enabled": false, leaving the saved flow enabled while telling the agent/user it is disabled; this should surface an error or otherwise avoid reporting success in that failure mode.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3307 to +3308
match ops::flows_draft_get(&self.config, id) {
Ok(outcome) => outcome.value.graph,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject drafts linked to a different flow

When save_workflow is called with a draft_id, this branch discards the loaded FlowDraft metadata and keeps only its graph, so the later flows_update(&flow_id, ...) writes that draft onto whichever flow_id the agent supplied. If a draft seeded from flow A is later saved with a stale or copied flow B id, B is silently overwritten with A's draft instead of rejecting the mismatch or using the draft's own flow_id.

Useful? React with 👍 / 👎.

Comment on lines +3307 to +3308
match ops::flows_draft_get(&self.config, id) {
Ok(outcome) => outcome.value.graph,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Carry the draft name when saving a draft

For the documented edit_workflowsave_workflow { draft_id, flow_id } path, edit_workflow can update the draft's name, but this save path only extracts outcome.value.graph and leaves name as the separate optional argument. If the agent renamed the draft during editing and then saves by draft_id without repeating name, the graph is persisted while the saved flow keeps its old name, unlike flows_draft_promote, which promotes draft.name.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant