Skip to content

fix(flows): stop the builder authoring memory steps that fabricate#5150

Merged
senamakel merged 1 commit into
tinyhumansai:mainfrom
graycyrus:flows-memory-research
Jul 24, 2026
Merged

fix(flows): stop the builder authoring memory steps that fabricate#5150
senamakel merged 1 commit into
tinyhumansai:mainfrom
graycyrus:flows-memory-research

Conversation

@graycyrus

@graycyrus graycyrus commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • The workflow_builder standing prompt told the builder an agent node "can also read and write the user's memory at run time". Both halves are false, and the failure mode is fabrication rather than an error.
  • Replaced it with the two mechanisms that actually reach memory from a running flow, including the correct downstream binding path for a native tool result.
  • States plainly that a workflow can never WRITE the user's memory, so the builder stops authoring remember/store steps and tells the user instead.
  • Deliberately does not steer to agent_memory — see Impact for the injection reason.
  • 3 regression tests.

Problem

A plain agent node (no agent_ref) is a single completion through OpenHumanLlm::complete — no tool loop. It cannot read memory and it cannot write memory.

The prompt said otherwise, so when a user asked for a memory-aware workflow the builder authored a plain agent node prompted to "recall the user's preference". The model has no retrieval path, so it invents the value. The node succeeds, the graph validates, the run completes — and the workflow acts on fabricated context.

That is worse than a broken step: a failure is visible, a fabrication is not. It has also been self-contradictory since #5120, whose own text four lines below states a plain agent node "cannot run code, browse the web, or reach any domain-specific tool".

Solution

Two mechanisms genuinely reach memory at flow run time, and the prompt now teaches both:

  1. tool_call node with config.slug = oh:memory_recall / oh:memory_hybrid_search — one deterministic read at a fixed point. A native tool result is a ToolResult ({ content: [{ type, text }], is_error }), so the downstream binding is =nodes.<id>.item.json.content[0].text, not the bare .item.json.<field> an agent/http_request output uses. Getting that path wrong reproduces the same silent-null class the =-binding rules exist to prevent, so it is spelled out.
  2. config.agent_ref = "context_scout" when the step must decide what to look up, or look up several things — that routes through run_via_harness (feat(flows): teach the workflow builder to pick specialist agents via agent_ref (B37) #5120/fix(agents): run custom registry agents with their real tools (flows + chat + tasks) #5121) to a real read-only tool loop with memory recall, transcript search, and thread reads.

context_scout was also added to the agent_ref worked examples, which previously listed only code_executor and researcher.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) — 3 tests: banned-wording regression guard, both working read paths + binding path, and the no-write/no-agent_memory contract
  • Diff coverage ≥ 80% — changed lines are prompt.md (data, include_str!-asserted by all 3 tests) and the tests themselves
  • Coverage matrix updated — N/A: behaviour-only change to agent prompt guidance
  • All affected feature IDs from the matrix are listed under ## RelatedN/A: no matrix feature row covers builder prompt copy
  • No new external network dependencies introduced
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: no release-cut surface touched
  • Linked issue closed via Closes #NNN — N/A: no issue filed; bug found while auditing the flows/memory integration

Impact

Behaviour-only change to builder guidance. No runtime, schema, or API change. Existing saved flows are untouched; newly authored ones stop containing no-op memory steps.

Security — why agent_memory is deliberately not recommended. It looks like the obvious pick: a registered builtin, sandbox_mode = "read_only", memory tools in its belt. But its memory_tree tool does not override permission_level(), so it inherits the trait default PermissionLevel::ReadOnly while dispatching an ingest_document mode that writes to the tree. The read-only tool filter in session/builder/factory.rs consults the argless permission_level(), so memory_tree survives that filter.

Flows run on trigger data a third party can influence (an inbound email, a webhook payload). Pointing the builder at agent_memory would therefore hand prompt-injected content a memory-write foothold — precisely the hole context_scout's own agent.toml documents refusing for the same reason. This PR routes around it; the underlying tool-permission gap is pre-existing, out of scope here, and worth its own issue.

Related

Follows #5120 (specialist selection via agent_ref) and #5121 (custom registry agents run with real tools), which together made the harness path the real mechanism this prompt now points at.

The standing workflow_builder prompt claimed an `agent` node "can also
read and write the user's memory at run time". Both halves were false.

A plain `agent` node (no `agent_ref`) is a single completion through
`OpenHumanLlm::complete` — no tool loop, so it can neither read nor write
memory. Told otherwise, the builder authored plain agent nodes prompted to
"recall the user's preference", and the model INVENTED one. The step
didn't fail; it fabricated context and the graph still looked correct,
which is strictly worse than not working.

Replace it with the two mechanisms that actually reach memory from a
running flow:

- a `tool_call` node with `oh:memory_recall` / `oh:memory_hybrid_search`
  for one deterministic read, including the correct downstream binding
  (`=nodes.<id>.item.json.content[0].text` — a native tool result is a
  `ToolResult`, so it dereferences `.content[0].text`, not `.<field>`)
- `agent_ref = "context_scout"` when the step must DECIDE what to look
  up, or look up several things — that routes to the harness (tinyhumansai#5120,
  tinyhumansai#5121) and gets a real read-only tool loop

Also state plainly that a workflow can never WRITE the user's memory, so
the builder stops authoring remember/store steps and says so instead.

Deliberately does NOT steer to `agent_memory`, despite it being a
registered `read_only` builtin with memory tools: its `memory_tree` tool
inherits the trait-default `PermissionLevel::ReadOnly` while dispatching
an `ingest_document` WRITE mode, and the read-only tool filter in
`session/builder/factory.rs` consults the argless `permission_level()`,
so it survives that filter. Flows run on trigger data a third party can
influence, so pointing them there would hand injected content a
memory-write foothold — the hole `context_scout`'s own agent.toml
documents refusing. Tracked separately.

Tests: 3 regression guards pinning the banned wording, both working read
paths + the binding, and the no-write/no-agent_memory contract.
@coderabbitai

coderabbitai Bot commented Jul 23, 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: 56 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: 24d05eaa-4f5b-47cc-a6da-ebc24affb7d9

📥 Commits

Reviewing files that changed from the base of the PR and between e9686e4 and 252ba79.

📒 Files selected for processing (2)
  • src/openhuman/flows/agents/workflow_builder/builder_prompt.rs
  • src/openhuman/flows/agents/workflow_builder/prompt.md

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

@senamakel
senamakel marked this pull request as ready for review July 24, 2026 00:20
@senamakel
senamakel requested a review from a team July 24, 2026 00:20
@senamakel
senamakel merged commit ef38f1c into tinyhumansai:main Jul 24, 2026
23 of 24 checks passed
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR corrects a fabrication-inducing bug in the workflow_builder standing prompt. The old text told the builder that a plain agent node could "read and write the user's memory at run time" — both halves were false, and the failure mode was silent fabrication rather than a visible error, making the bug strictly worse than a broken step.

  • prompt.md: Replaces the false memory claim with accurate guidance for the two mechanisms that actually work — a tool_call node with oh:memory_recall/oh:memory_hybrid_search (including the correct ToolResult binding path =nodes.<id>.item.json.content[0].text), and config.agent_ref = "context_scout" for decision-driven lookups. Explicitly bans memory writes and excludes agent_memory as an agent_ref, with a documented rationale for the security boundary (memory_tree's permission mismatch).
  • builder_prompt.rs: Adds three regression tests that pin the corrected prompt content via include_str! — one negative guard against the exact banned wording, one positive check for both working paths and their binding syntax, and a contract test for the no-write / no-agent_memory invariant.

Confidence Score: 4/5

Safe to merge — the change is entirely contained to a builder prompt and its regression tests, with no runtime, schema, or API surface touched. Existing saved flows are unaffected.

The prompt correction is accurate and the security rationale for excluding agent_memory is well-documented. One test assertion embeds a literal newline and specific indentation, making it more brittle than the file's existing pattern of resilient single-line substring checks — a future reformat of that paragraph would break CI without any semantic regression.

The only item worth a second look is the
embedded in the positive assertion in builder_prompt.rs; prompt.md itself is clean.

Important Files Changed

Filename Overview
src/openhuman/flows/agents/workflow_builder/prompt.md Corrects a fabrication-causing prompt bug: removes the false "agent node can read and write memory" claim and replaces it with accurate guidance for two real runtime-memory paths (oh:memory_recall/oh:memory_hybrid_search tool_call nodes and context_scout agent_ref), including the correct ToolResult binding path; explicitly bans memory writes and avoids steering to agent_memory for the documented security reason.
src/openhuman/flows/agents/workflow_builder/builder_prompt.rs Adds three regression tests that pin the prompt changes via include_str!: a negative guard for banned fabrication-inducing wording, a positive check for both working memory-read paths and the correct binding path, and a contract test that the prompt bans agent_memory. One positive assertion embeds a newline + specific indentation, making it brittle to future reformatting of prompt.md.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User asks for memory-aware workflow] --> B{Which memory mechanism?}

    B -->|Deterministic read at fixed point| C["tool_call node\nconfig.slug = oh:memory_recall\nor oh:memory_hybrid_search"]
    B -->|Needs to decide what to look up| D["agent node\nconfig.agent_ref = context_scout\n(real read-only tool loop)"]
    B -->|Write memory| E["Not possible\nWorkflow can never WRITE memory\nTell user plainly"]

    C --> F["Bind output downstream:\n=nodes.id.item.json.content[0].text\n(ToolResult envelope — NOT .item.json.field)"]
    D --> G["Returns context bundle\nFeed into following agent node\nvia input_context"]

    subgraph OLD ["OLD (fabrication bug)"]
        H["plain agent node\nprompted to recall preference"]
        H --> I["Model INVENTS answer\nGraph looks correct\nSilent fabrication"]
    end

    subgraph NEW ["NEW (this PR)"]
        C
        D
        E
        F
        G
    end
Loading

Reviews (1): Last reviewed commit: "fix(flows): stop the builder authoring m..." | Re-trigger Greptile

Comment on lines +782 to +786
assert!(
STANDING_PROMPT.contains("A plain `agent` node has NO\n memory access"),
"standing prompt must state outright that a plain agent node has no \
memory access, so the builder never authors a no-op recall step"
);

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 The positive assertion embeds a literal (newline + 3 spaces of indentation), tying the test to the exact current line-wrap position inside the Markdown list. If this paragraph is later reflowed, re-indented, or pulled out of the numbered list, the assertion fails even though the semantic constraint is still satisfied. The negative banned-phrase checks share the same fragility. Every other positive assertion in this test module uses a short, single-line substring that is robust to reformatting. Extracting just the key phrase — without the accidental line break — keeps the guard meaningful while surviving common edits to prompt.md.

Suggested change
assert!(
STANDING_PROMPT.contains("A plain `agent` node has NO\n memory access"),
"standing prompt must state outright that a plain agent node has no \
memory access, so the builder never authors a no-op recall step"
);
assert!(
STANDING_PROMPT.contains("A plain `agent` node has NO"),
"standing prompt must state outright that a plain agent node has no \
memory access, so the builder never authors a no-op recall step"
);

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 252ba79389

ℹ️ 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".

recall) or `oh:memory_hybrid_search` (keyword/lexical lookup). One
deterministic read at a fixed point in the graph. Its output is a native
tool result, so bind downstream off
`=nodes.<id>.item.json.content[0].text` — NOT `.item.json.<field>`.

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 Use a valid jq path for native tool output

This example includes an array index but still uses the bare nodes shorthand. The prompt's own expression rules say paths that continue into jq syntax like [...] must start from the jq root, so builders following this will author =nodes.<id>.item.json.content[0].text, which evaluates as invalid/null instead of reading the native tool result. For memory-recall flows this makes the newly recommended path fail to feed downstream nodes; use the jq form such as =.nodes["<id>"].item.json.content[0].text.

Useful? React with 👍 / 👎.

Comment on lines +344 to +350
**A workflow can never WRITE the user's memory.** There is no
remember/store step, and no `agent_ref` that grants one — a flow runs on
trigger data that a third party can influence (an inbound email, a webhook
payload), so writing that into the user's memory is deliberately not
possible. If the user asks for a workflow that "remembers" something
across runs, say plainly that memory writes are not available to workflows
yet and build the rest of the flow without that step.

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 Enforce the no-memory-write guarantee

This now states that workflows can never write memory, but that guarantee is not enforced by the runtime: a saved/imported flow can still use a native tool_call with slug: "oh:memory_store", which goes through OpenHumanTools::invoke to runtime_node::execute_tool, and the runtime registry includes MemoryStoreTool. In supervised/full autonomy, trigger-derived args can therefore still be persisted to the user's memory while the prompt/tests assume the surface is impossible; either block memory-write native tools for flows or weaken this to builder guidance.

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.

2 participants