Skip to content

feat(nodes): add memory node kind (recall/flavour/people + flow-scoped remember) - #23

Merged
graycyrus merged 5 commits into
mainfrom
feat/memory-node
Jul 28, 2026
Merged

feat(nodes): add memory node kind (recall/flavour/people + flow-scoped remember)#23
graycyrus merged 5 commits into
mainfrom
feat/memory-node

Conversation

@graycyrus

@graycyrus graycyrus commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

What

Adds the 13th node kind, memory, as a delivery surface onto a new host-injected MemoryProvider capability. Lets a running workflow read memory (recall/search/flavour/people) and durably write flow-scoped memory (remember/forget) declaratively, in-graph.

Consumed by OpenHuman (tinyhumansai/openhuman#5226) — the host supplies the real MemoryProvider; this crate stays host-agnostic.

Design

  • NodeKind::Memory, wire value "memory". Additive — CURRENT_SCHEMA_VERSION stays 1.
  • MemoryProvider trait + Capabilities::memory: Option<Arc<dyn MemoryProvider>> — mirrors the existing agent: Option<Arc<dyn AgentRunner>> slot exactly (hosts without memory keep compiling; a memory node without a provider fails with EngineError::Capability, never a silent no-op).
  • Executor at nodes/integration/memory.rs, default execution mode PerItem (so split_out → memory fans out one call per item).

Config

Field Required Values
operation recall · search · flavour · people · remember · forget
scope recall/remember/forget user (read-only) · flow · flows (read-only)
query recall/search =-bindable
flavour flavour slug
key/value remember/forget =-bindable
limit,min_score optional

operation: search maps to recall() with opts.operation = "search" so a host can distinguish semantic recall from full-text search; a host that doesn't care ignores it.

Hard security invariant (validate-time)

operation: remember|forget + scope: "user" is rejected in validate_all — structurally, where the builder sees it, not mid-run. A workflow can never plant or erase durable facts about the user; writes are restricted to flow scope. Plus required-field + closed-enum-scope checks per the table.

Dry-run

MockMemory (shaped returns) wired into mock_capabilities() by default, so dry_run_workflow keeps working.

Tests

Node executor (recall/flavour/people/remember/forget against MockMemory), =-expression resolution, the validate-time user-write rejection (+ remember·flow passes), required-field errors, and the catalog contract (NODE_KINDS = 13, memory contract present).

Note

#![forbid(unsafe_code)] / #![warn(missing_docs)] honored — every new public item documented.

Summary by CodeRabbit

  • New Features
    • Added a host-managed memory node supporting recall, search, flavour, people, remember, and forget, with per_item and once execution modes.
    • Introduced a host-injected memory capability with scoped read/write behavior.
  • Validation
    • Enforced operation-specific required fields and a hard security rule blocking remember/forget for disallowed scope values.
  • Documentation
    • Updated the node catalog and README to document the memory node and its configuration.
  • Tests
    • Expanded mock memory and end-to-end coverage across all memory operations and execution modes, including error cases.

…d remember)

Adds the 13th node kind, `memory`, as a delivery surface onto a host-injected
`MemoryProvider` capability (recall/search/flavour/people reads, remember/forget
writes). Enforces the hard security invariant at validate time: a
remember/forget operation may never target scope "user" — writes are
restricted to flow-scoped memory, so a workflow can never plant or erase
durable facts about the user. Includes a shaped MockMemory (wired into
mock_capabilities by default) so dry-run keeps working, and node/validate/
catalog test coverage.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f5ea3cd3-eb8b-47ba-8108-9858cda7966b

📥 Commits

Reviewing files that changed from the base of the PR and between 7b3c72e and b1f62f8.

📒 Files selected for processing (2)
  • src/nodes/integration/memory.rs
  • src/validate.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/validate.rs
  • src/nodes/integration/memory.rs

📝 Walkthrough

Walkthrough

Adds a host-backed memory node with six operations, capability injection, configuration validation, catalog metadata, mock support, execution modes, and comprehensive tests.

Changes

Memory node capability and contracts

Layer / File(s) Summary
Capability interface and mock wiring
src/caps/mod.rs, src/caps/mock.rs, src/model/node_kind.rs, src/main.rs
Adds MemoryProvider, optional capability storage, NodeKind::Memory, deterministic mock operations, custom provider injection, and standalone capability configuration.
Catalog and validation
src/catalog.rs, src/validate.rs, README.md
Documents the memory node and validates operations, scopes, required fields, and restrictions on write scopes.
Execution and dispatch
src/nodes/integration/memory.rs, src/nodes/integration/mod.rs, src/nodes/mod.rs
Dispatches memory operations through the provider, supports per-item and once execution, wraps results, and registers the node executor.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkflowEngine
  participant MemoryNode
  participant MemoryProvider
  WorkflowEngine->>MemoryNode: Execute memory configuration
  MemoryNode->>MemoryProvider: Invoke selected operation
  MemoryProvider-->>MemoryNode: Return result or status
  MemoryNode-->>WorkflowEngine: Return integration output
Loading

Possibly related issues

  • tinyhumansai/openhuman issue 5226 — Directly covers the memory node, provider, executor, catalog, validation, and mock wiring added here.

Poem

I’m a bunny with memory, hopping in code,
Six little pathways now carry the load.
I recall and remember, forget when I’m told,
With scopes checked safely before they unfold.
The mock garden blooms,
And the catalog zooms!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding the new memory node kind with recall/flavour/people and flow-scoped write support.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds the 13th node kind, memory, as a declarative delivery surface onto a new host-injected MemoryProvider capability. The design mirrors the existing optional AgentRunner slot — hosts without a memory store leave Capabilities::memory as None and get a capability error at runtime rather than a silent no-op.

  • Six operations (recall, search, flavour, people, remember, forget) are dispatched through a single call_provider function in src/nodes/integration/memory.rs, with per_item/once execution modes matching the tool_call precedent.
  • Hard security invariant in validate_all rejects remember/forget with both scope: "user" and scope: "flows" at graph-validation time (before any run), with a defense-in-depth re-check in the executor itself.
  • MockMemory is wired into mock_capabilities() by default so any workflow containing a memory node dry-runs out of the box; tests cover all six operations, =-expression resolution per item, the once-mode collapse, and the write-restriction error paths.

Confidence Score: 5/5

Safe to merge — the write-scope invariant is correctly enforced at both validate time and executor level, and the change is purely additive.

The remember/forget write restriction now correctly blocks both scope:"user" and scope:"flows" at validate time, with matching executor-level defense-in-depth. The new capability slot is optional and additive — existing hosts recompile without changes and no existing code path is affected. Test coverage spans all six operations, expression resolution, execution modes, and every error path.

Files Needing Attention: src/catalog.rs — the HARD SECURITY RULE note only names scope "user" explicitly; scope "flows" is also rejected but not called out in the note.

Important Files Changed

Filename Overview
src/nodes/integration/memory.rs New memory node executor; handles all six operations with correct per-item/once dispatch, defense-in-depth scope guard for write ops, and comprehensive tests.
src/validate.rs Memory validation block correctly rejects both scope:"user" and scope:"flows" for write ops, enforces per-operation required fields, and is covered by 14+ new tests including the flows-write rejection.
src/caps/mod.rs Adds MemoryProvider trait (5 methods, async_trait) and optional memory field to Capabilities; mirrors the AgentRunner Optional pattern exactly.
src/caps/mock.rs Adds shaped MockMemory (not a naive echo), wires it into mock_capabilities by default, and provides mock_capabilities_with_memory for custom test overrides.
src/catalog.rs Adds memory contract with six operations, scope enum, and a HARD SECURITY RULE note; note names only scope:"user" as the hard reject without explicitly calling out scope:"flows", though "writes may only target scope "flow"" is present.
src/model/node_kind.rs Adds Memory variant to NodeKind enum with doc comment referencing MemoryProvider and the write invariant.
src/nodes/mod.rs Wires NodeKind::Memory to MemoryNode in executor_for dispatch; adds Memory to all_kinds test helper.
src/main.rs Adds memory: None to standalone CLI capabilities; correct since the CLI has no memory store.

Sequence Diagram

sequenceDiagram
    participant G as WorkflowGraph
    participant V as validate_all
    participant E as MemoryNode executor
    participant P as MemoryProvider (host)

    G->>V: graph with memory node
    V->>V: check operation enum
    V->>V: "check scope enum (user|flow|flows)"
    alt remember/forget + scope user or flows
        V-->>G: ValidationError (hard reject)
    else valid config
        V-->>G: Ok(())
    end

    G->>E: NodeContext (config, input items)
    E->>E: execution_mode (per_item / once)
    loop per item (or once)
        E->>E: "resolve_config (expand =expr)"
        E->>E: check caps.memory is Some
        alt "operation = recall | search"
            E->>P: recall(scope, query, opts)
            P-->>E: Value (results)
        else "operation = flavour"
            E->>P: flavour(slug)
            P-->>E: Value (traits)
        else "operation = people"
            E->>P: people(query?)
            P-->>E: Value (people list)
        else "operation = remember"
            E->>E: "guard scope == flow"
            E->>P: remember(scope, key, value)
            P-->>E: Ok(())
        else "operation = forget"
            E->>E: "guard scope == flow"
            E->>P: forget(scope, key)
            P-->>E: Ok(())
        end
        E->>E: envelope::wrap(result)
    end
    E-->>G: NodeOutput (items)
Loading

Reviews (4): Last reviewed commit: "fix(memory): reject flow-write to read-o..." | Re-trigger Greptile

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/caps/mod.rs (1)

249-254: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Adding a public field to Capabilities breaks hosts that build it with a struct literal.

Capabilities has no #[non_exhaustive], so every host constructing it directly must now add memory: None. Worth calling out in release notes (or adding a constructor/Default-style builder) so the compile break is expected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/caps/mod.rs` around lines 249 - 254, Update the public Capabilities API
to avoid an unannounced struct-literal compatibility break from the memory
field: add #[non_exhaustive] to Capabilities or provide and adopt a
constructor/Default-style builder that centralizes the new field, and document
the required migration in release notes if applicable. Preserve the optional
memory behavior exposed by the memory field.
src/validate.rs (1)

179-200: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Worth a comment that the scope enum check is what makes the invariant unbypassable.

The user-scope ban only holds because line 191 rejects any non-literal scope (an "=expr" binding fails the enum check), so a runtime-resolved scope can never reach provider.remember/forget. A future change allowing bindable scope would silently reopen that hole — worth stating explicitly next to the invariant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/validate.rs` around lines 179 - 200, The validation around the scope
check in validate.rs should explicitly document that restricting scope to the
literal enum values prevents bindable or runtime-resolved scopes from bypassing
the user-scope prohibition for remember/forget operations. Add a concise comment
beside the invariant or enum validation, referencing the relevant validation
logic without changing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/nodes/integration/memory.rs`:
- Line 70: Update scope handling in the integration executor for recall,
remember, and forget so write operations do not default a missing or non-string
scope to an empty string. Require a valid scope for these operations and return
the documented Capability error when it is absent or invalid, while preserving
the existing operation-specific behavior.
- Around line 124-143: Run cargo fmt --all to apply rustfmt formatting
throughout the affected memory integration code, including the remember/forget
EngineError constructors, cfg.get(...).and_then(...).ok_or_else(...) chain,
opts.insert call, and assert! statement. Do not change behavior.

---

Nitpick comments:
In `@src/caps/mod.rs`:
- Around line 249-254: Update the public Capabilities API to avoid an
unannounced struct-literal compatibility break from the memory field: add
#[non_exhaustive] to Capabilities or provide and adopt a
constructor/Default-style builder that centralizes the new field, and document
the required migration in release notes if applicable. Preserve the optional
memory behavior exposed by the memory field.

In `@src/validate.rs`:
- Around line 179-200: The validation around the scope check in validate.rs
should explicitly document that restricting scope to the literal enum values
prevents bindable or runtime-resolved scopes from bypassing the user-scope
prohibition for remember/forget operations. Add a concise comment beside the
invariant or enum validation, referencing the relevant validation logic without
changing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c8e31cc-0dc4-44d2-a3ba-3f9f82eb4f89

📥 Commits

Reviewing files that changed from the base of the PR and between fa7a31b and 48565b1.

📒 Files selected for processing (9)
  • README.md
  • src/caps/mock.rs
  • src/caps/mod.rs
  • src/catalog.rs
  • src/model/node_kind.rs
  • src/nodes/integration/memory.rs
  • src/nodes/integration/mod.rs
  • src/nodes/mod.rs
  • src/validate.rs

Comment thread src/nodes/integration/memory.rs
Comment thread src/nodes/integration/memory.rs
Comment thread src/validate.rs Outdated
…ect-drive writes

- validate: remember/forget now reject scope "flows" (read-only), not just
  "user" — closes the write-restriction bypass (Greptile P1). Documents that
  the literal-enum scope check is what makes the invariant unbypassable (CodeRabbit).
- executor: remember/forget hard-error unless scope=="flow", so a direct
  drive (bypassing validate) can never silently write an empty/read-only scope
  (CodeRabbit). Adds validate + executor regression tests.
@graycyrus

Copy link
Copy Markdown
Collaborator Author

Review comments addressed — pushed in b1f62f8

@greptileai P1 (security) — scope:"flows" bypasses the write-restriction ✅ Fixed. validate_all now rejects remember/forget for both read-only scopes (user and flows), not just user — only flow is a valid write target. Previously flows-writes passed validation and were caught only by the host adapter's defense-in-depth; now the engine's own invariant is complete. Regression tests: memory_rejects_remember_flows_scope / memory_rejects_forget_flows_scope.

@coderabbitai — write ops silently fall back to empty scope (memory.rs) ✅ Fixed. The executor now hard-errors unless scope == "flow" for remember/forget, so a direct drive (bypassing the validator) can never silently write to an empty or read-only scope — matching the module doc's "defends against malformed config with a Capability error" claim. Test: write_to_non_flow_scope_is_a_capability_error_even_when_driven_directly (covers flows and absent-scope, for both ops).

@coderabbitaicargo fmt --check fails / CI red ✅ Already resolved in af16eb0 (stable rustfmt) — both Rust SDK jobs are green.

@coderabbitai — document that the literal-enum scope check makes the invariant unbypassable (validate.rs) ✅ Added — the invariant's block comment now explains that an =expr binding can never satisfy the user|flow|flows enum check, so a runtime-resolved scope can't smuggle a write past the gate, and flags that making scope bindable would reopen it.

@coderabbitai#[non_exhaustive] on Capabilities (nitpick) ⏭️ Skipped, with reason. #[non_exhaustive] would forbid all struct-literal construction of Capabilities from downstream crates — including OpenHuman's own build_capabilities and the standalone main.rs — forcing a constructor/builder refactor across every host. That's a larger API change than this PR warrants (and CodeRabbit rated it Trivial / Low-value). The compile-break from adding memory is a one-time memory: None addition, already handled in the consuming PR. Worth revisiting as a standalone Capabilities::builder() cleanup if the field set keeps growing.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Current commit and reported commit =='
git rev-parse --short HEAD
git show --no-patch --oneline b1f62f8 2>&1 || true

printf '%s\n' '== Relevant file outlines =='
ast-grep outline src/validate.rs --items all --type function 2>&1 || true
ast-grep outline src/nodes/integration/memory.rs --items all --type function,impl 2>&1 || true

printf '%s\n' '== Scope-validation and direct-write guard contexts =='
rg -n -C 12 -P 'remember|forget|scope|HARD SECURITY|read-only|write' \
  src/validate.rs src/nodes/integration/memory.rs

printf '%s\n' '== Reported regression tests =='
rg -n -C 8 -P 'memory_rejects_remember_flows_scope|memory_rejects_forget_flows_scope|write_to_non_flow_scope_is_a_capability_error_even_when_driven_directly' \
  src/validate.rs src/nodes/integration/memory.rs

printf '%s\n' '== Formatting check availability =='
if command -v rustfmt >/dev/null 2>&1; then
  rustfmt --check src/validate.rs src/nodes/integration/memory.rs
else
  echo 'rustfmt unavailable in sandbox'
fi

Length of output: 67156


@graycyrus Verified on b1f62f8:

  • validate_all now rejects both user and flows for remember/forget, while preserving flow as the sole writable scope.
  • The literal scope-enum check and its binding rationale are present.
  • MemoryNode independently rejects every non-flow direct write, including absent scope.
  • The reported regression tests are present and cover the stated cases.

The #[non_exhaustive] deferral is reasonable given the downstream struct-literal impact.

One minor documentation follow-up remains: src/nodes/integration/memory.rs Lines 22–23 still describes only scope: "user" as forbidden. It should say writes are flow-only (with both user and flows read-only) to match the implemented invariant.

I could not independently run rustfmt --check because rustfmt is unavailable in this sandbox; the reported green CI remains the available verification for formatting.


✏️ Learnings added
Learnt from: graycyrus
URL: https://github.com/tinyhumansai/tinyflows/pull/23

Timestamp: 2026-07-28T15:09:57.617Z
Learning: For Rust memory nodes, `remember` and `forget` writes may target only the literal `scope: "flow"`; `user` and `flows` are read-only scopes. `src/validate.rs` enforces this structurally, and `src/nodes/integration/memory.rs` repeats it as a direct-execution defense-in-depth guard.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

All four fixes verify correctly against the code.

validate.rs — the invariant now covers both read-only scopes:

if matches!(operation, "remember" | "forget")
    && matches!(scope, Some("user") | Some("flows"))

And the block comment explicitly documents that the literal-enum scope check is what makes the invariant unbypassable — including the warning that making scope bindable would reopen it.

memory.rs — both write arms hard-error on anything that isn't "flow":

if scope != "flow" {
    return Err(EngineError::Capability(...));
}

This covers the empty-scope default case (unwrap_or("")) and the "flows" case in the same guard, no special-casing needed.

Test coveragememory_rejects_remember_flows_scope / memory_rejects_forget_flows_scope close the validator gap; write_to_non_flow_scope_is_a_capability_error_even_when_driven_directly covers the executor directly for "flows" and absent-scope for remember (and by the identical code path, forget absent-scope hits the same guard). The only gap is forget + absent scope isn't explicitly iterated in the parameterized test, but since the guard is scope != "flow" with no op-specific branching, the code coverage is effectively complete.

#[non_exhaustive] skip — the reasoning is sound. The struct-literal construction pattern is load-bearing in all consuming hosts; forcing a Capabilities::builder() refactor is out of scope here. Worth a follow-up if the field count keeps growing.

Everything looks good. LGTM.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@graycyrus
graycyrus merged commit ff99501 into main Jul 28, 2026
6 checks passed
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