Skip to content

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

Open
graycyrus wants to merge 1 commit into
mainfrom
feat/memory-node
Open

feat(nodes): add memory node kind (recall/flavour/people + flow-scoped remember)#23
graycyrus wants to merge 1 commit 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 memory node supporting recall, search, flavour, people, remember, and forget operations.
    • Added host-managed memory capability integration with configurable scopes and execution modes.
    • Added validation for required settings and security restrictions on memory writes.
    • Added mock memory support for testing and development.
  • Documentation

    • Updated node documentation and catalog to include the memory node and its available options.

…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

📝 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
Adds MemoryProvider, optional capability storage, NodeKind::Memory, deterministic mock operations, and custom memory-provider injection.
Catalog and validation
src/catalog.rs, src/validate.rs, README.md
Documents the memory node and validates operations, scopes, required fields, and the restriction against user-scoped writes.
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#5226 — Directly covers the first-party memory node, provider, executor, catalog, validation, and mock wiring added here.

Suggested reviewers: senamakel

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 clearly matches the main change: adding the new memory node kind with its key operations and scoped remember behavior.
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 trait. It follows the established AgentRunner pattern — optional, Arc-wrapped, fail-fast on missing provider — and adds six operations (recall, search, flavour, people, remember, forget) with a validate-time security invariant blocking write operations from targeting user-scoped memory.

  • New MemoryProvider trait (caps/mod.rs) with five methods; MockMemory wired into mock_capabilities() by default with shaped returns so dry-runs work out of the box.
  • MemoryNode executor (nodes/integration/memory.rs) dispatches per-operation, supports per_item/once execution modes, and wraps results in the standard envelope.
  • validate_all additions (validate.rs) enforce operation-specific required fields and a hard security invariant rejecting remember/forget + scope: \"user\" at graph-build time.

Confidence Score: 4/5

Safe to merge with one fix: the validator needs to also block remember/forget + scope: "flows" alongside the existing scope: "user" guard.

The overall implementation is well-structured and thoroughly tested. The one gap is in validate.rs: the error message inside the "user" guard explicitly says "remember/forget may only write scope "flow"", and the catalog describes "flows" as read-only, but a graph with remember/forget + scope: "flows" passes validate_all today and reaches the provider unfiltered. No test covers that path.

Files Needing Attention: src/validate.rs — the remember/forget scope guard needs to cover "flows" in addition to "user".

Important Files Changed

Filename Overview
src/nodes/integration/memory.rs New executor for the memory node kind; well-structured per-item / once execution modes, per-operation dispatch, and solid in-file test coverage for all six operations.
src/validate.rs Adds memory-node validation including the security invariant for remember/forget writes, but the guard only rejects scope: "user"scope: "flows" (also documented as read-only) is accepted for write operations, inconsistent with the stated invariant and the error message text.
src/caps/mod.rs Adds MemoryProvider trait mirroring the existing AgentRunner precedent; five well-documented methods; Capabilities::memory added as Option<Arc<dyn MemoryProvider>>.
src/caps/mock.rs Adds MockMemory with shaped return values for all five trait methods; wired into mock_capabilities() by default; adds mock_capabilities_with_memory helper and tests.
src/catalog.rs Adds the memory node's catalog contract with all config fields, scope enum, example, and the hard-security-rule note; NODE_KINDS bumped to 13.
src/model/node_kind.rs Adds NodeKind::Memory variant with correct serde wire value "memory" and doc comment; wire-format round-trip test added.
src/nodes/mod.rs Dispatches NodeKind::Memory to MemoryNode; all_kinds() test helper updated to include Memory.

Comments Outside Diff (1)

  1. src/validate.rs, line 1221-1230 (link)

    P1 scope: "flows" write ops not blocked at validate time

    The hard-invariant check only rejects scope: "user" for remember/forget, but the "flows" scope is documented throughout this PR as "cross-flow read access — read-only". Both the validator error message ("remember/forget may only write scope "flow"") and the catalog note ("writes may only target scope "flow"") make it clear that "flows" should also be blocked. A workflow graph containing { "operation": "remember", "scope": "flows", "key": "k", "value": 1 } passes validate_all today and will call MemoryProvider::remember with scope = "flows", relying on the host to reject what the validator promised it would never see. There is also no test covering this path.

Reviews (1): Last reviewed commit: "feat(nodes): add memory node kind (recal..." | 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

let operation = cfg.get("operation").and_then(Value::as_str).ok_or_else(|| {
EngineError::Capability("memory node: missing `operation` in config".to_string())
})?;
let scope = cfg.get("scope").and_then(Value::as_str).unwrap_or("");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Write operations silently fall back to an empty scope.

scope defaults to "" when absent or non-string, and that empty string is passed straight to remember/forget. The validator requires scope for those ops, so this only bites when the executor is driven directly — but the module doc (Lines 22-27) claims this executor defends against malformed config with a Capability error, and a write to an unspecified scope is worse than a hard error. Consider requiring scope for recall/remember/forget here rather than defaulting.

Also applies to: 137-137, 149-149

🤖 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/nodes/integration/memory.rs` at 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.

Comment on lines +124 to +143
let key = cfg.get("key").and_then(Value::as_str).ok_or_else(|| {
EngineError::Capability("memory node: `remember` operation requires `key`".to_string())
})?;
let value = cfg.get("value").cloned().ok_or_else(|| {
EngineError::Capability(
"memory node: `remember` operation requires `value`".to_string(),
)
})?;
tracing::debug!(
node = %ctx.node.id,
key,
"{LOG_PREFIX} calling provider.remember"
);
provider.remember(scope, key, value).await?;
serde_json::json!({ "ok": true, "operation": "remember", "key": key })
}
"forget" => {
let key = cfg.get("key").and_then(Value::as_str).ok_or_else(|| {
EngineError::Capability("memory node: `forget` operation requires `key`".to_string())
})?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔴 Critical | ⚡ Quick win

cargo fmt --check fails on this file — CI is red.

Both CI jobs report formatting diffs (the EngineError::Capability(...) one-liners here at Lines 125 and 142 exceed the width rustfmt would wrap at, plus the cfg.get(...).and_then(...).ok_or_else(...) chain, the opts.insert call, and the assert! around Line 294). Run cargo fmt --all.

🔧 Proposed formatting fix for the two flagged constructors
             let key = cfg.get("key").and_then(Value::as_str).ok_or_else(|| {
-                EngineError::Capability("memory node: `remember` operation requires `key`".to_string())
+                EngineError::Capability(
+                    "memory node: `remember` operation requires `key`".to_string(),
+                )
             })?;
             let key = cfg.get("key").and_then(Value::as_str).ok_or_else(|| {
-                EngineError::Capability("memory node: `forget` operation requires `key`".to_string())
+                EngineError::Capability(
+                    "memory node: `forget` operation requires `key`".to_string(),
+                )
             })?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let key = cfg.get("key").and_then(Value::as_str).ok_or_else(|| {
EngineError::Capability("memory node: `remember` operation requires `key`".to_string())
})?;
let value = cfg.get("value").cloned().ok_or_else(|| {
EngineError::Capability(
"memory node: `remember` operation requires `value`".to_string(),
)
})?;
tracing::debug!(
node = %ctx.node.id,
key,
"{LOG_PREFIX} calling provider.remember"
);
provider.remember(scope, key, value).await?;
serde_json::json!({ "ok": true, "operation": "remember", "key": key })
}
"forget" => {
let key = cfg.get("key").and_then(Value::as_str).ok_or_else(|| {
EngineError::Capability("memory node: `forget` operation requires `key`".to_string())
})?;
let key = cfg.get("key").and_then(Value::as_str).ok_or_else(|| {
EngineError::Capability(
"memory node: `remember` operation requires `key`".to_string(),
)
})?;
let value = cfg.get("value").cloned().ok_or_else(|| {
EngineError::Capability(
"memory node: `remember` operation requires `value`".to_string(),
)
})?;
tracing::debug!(
node = %ctx.node.id,
key,
"{LOG_PREFIX} calling provider.remember"
);
provider.remember(scope, key, value).await?;
serde_json::json!({ "ok": true, "operation": "remember", "key": key })
}
"forget" => {
let key = cfg.get("key").and_then(Value::as_str).ok_or_else(|| {
EngineError::Capability(
"memory node: `forget` operation requires `key`".to_string(),
)
})?;
🧰 Tools
🪛 GitHub Actions: CI / 1_Rust SDK.txt

[error] 139-139: cargo fmt --check failed (formatting diff detected). Run cargo fmt --all to apply the suggested formatting.

🪛 GitHub Actions: CI / Rust SDK

[error] 139-139: cargo fmt --all -- --check failed due to formatting differences (EngineError Capability string/constructor multiline formatting). Run cargo fmt --all to apply rustfmt.

🤖 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/nodes/integration/memory.rs` around lines 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.

Source: Pipeline failures

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