feat(agent): multi-agent personalities with scoped memory and delegation#2895
Conversation
📝 WalkthroughWalkthroughThis PR implements a multi-personality agent system: extended AgentProfile and Thread types, personality-scoped PromptContext and roster, personality file resolution (SOUL/MEMORY) and memory subdir helpers, isolated per-personality UnifiedMemory support, thread personality persistence, personality-aware prompt rendering, a DelegateToPersonality tool, and comprehensive tests. ChangesMulti-personality agent foundation
Sequence Diagram(s)sequenceDiagram
participant Client
participant Tool as DelegateToPersonalityTool
participant Store as AgentProfileStore
participant Registry as AgentDefinitionRegistry
participant Subagent
participant Memory as UnifiedMemory
Client->>Tool: execute(personality_id, prompt, context?)
Tool->>Store: resolve active_profile & lookup personality profile
Store-->>Tool: AgentProfile
Tool->>Tool: PersonalityContext::from_profile (resolve SOUL/MEMORY)
Tool->>Registry: get(agent_id)
Registry-->>Tool: AgentDefinition
Tool->>Subagent: run_subagent(combined context, model_override)
Subagent-->>Tool: output
Tool->>Client: ToolResult (prefixed)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
tests/personality_e2e.rs (1)
1-728: 🏗️ Heavy liftSplit this E2E suite into smaller Rust test modules/files.
This new test file is large enough that follow-up maintenance and review will get harder quickly. Please split by domain (for example: profile lifecycle, memory isolation, prompt behavior, integration filtering, thread binding) to keep each unit focused.
As per coding guidelines:
**/*.{ts,tsx,rs}: Keep React component files and Rust modules at ≤ ~500 lines; split growing modules into multiple files.🤖 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 `@tests/personality_e2e.rs` around lines 1 - 728, The test file is too large and should be split by domain (profile lifecycle, memory isolation, prompt behaviour, integration filtering, thread binding); extract shared helpers (make_profile, empty_prompt_context, FakeIntegration) into a single shared helper file and have each new test file include it. Concretely: create smaller test files (e.g. personality_profile_lifecycle.rs, personality_memory_isolation.rs, personality_prompt.rs, personality_integration.rs, personality_thread_binding.rs) and move the corresponding test functions (backwards_compat_old_profiles_json_deserializes_cleanly, three_personalities_get_sequential_memory_suffixes, deleted_suffix_is_reused_by_next_personality, personality_fields_roundtrip_through_upsert, default_profile_memory_suffix_cannot_be_overridden into the profile file; two_personalities_have_isolated_sqlite_stores, personality_memory_persists_across_reopens into the memory file; resolve_personality_* and subdir_helpers and PersonalityContext tests into prompt/memory resolution file; IdentitySection/UserFilesSection/PersonalityRosterSection tests into personality_prompt.rs; filter_integrations tests into personality_integration.rs; thread_personality_* tests into personality_thread_binding.rs), move shared imports and helper functions into tests/personality_helpers.rs and include them into each new test file using include!("personality_helpers.rs") (or place helpers into a common library module if preferred), and ensure each new test file imports the same symbols used (e.g., AgentProfileStore, UnifiedMemory, PersonalityContext, IdentitySection, filter_integrations, ConversationStore, ensure_thread, update_thread_title) so the tests compile and stay under the ~500-line guideline.src/openhuman/agent/prompts/mod.rs (2)
1352-1376: ⚡ Quick winConsolidate duplicated truncation/injection logic.
inject_inline_contentandinject_snapshot_contentcurrently implement the same logic; keep one implementation to prevent future divergence.♻️ Suggested refactor
fn inject_snapshot_content(prompt: &mut String, label: &str, content: &str, max_chars: usize) { - let trimmed = content.trim(); - if trimmed.is_empty() { - return; - } - let _ = writeln!(prompt, "### {label}\n"); - let truncated = if trimmed.chars().count() > max_chars { - trimmed - .char_indices() - .nth(max_chars) - .map(|(idx, _)| &trimmed[..idx]) - .unwrap_or(trimmed) - } else { - trimmed - }; - prompt.push_str(truncated); - if truncated.len() < trimmed.len() { - let _ = writeln!( - prompt, - "\n\n[... truncated at {max_chars} chars — use `read` for full file]\n" - ); - } else { - prompt.push_str("\n\n"); - } + inject_inline_content(prompt, label, content, max_chars); }Also applies to: 1382-1406
🤖 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/openhuman/agent/prompts/mod.rs` around lines 1352 - 1376, Both inject_inline_content and inject_snapshot_content duplicate the same truncation/injection logic; extract that logic into a single helper (e.g., truncate_and_inject or format_and_append_content) and have both inject_inline_content and inject_snapshot_content call it. The helper should accept (prompt: &mut String, label: &str, content: &str, max_chars: usize), perform the trim/empty check, write the "### {label}\n" header, truncate by character count using char_indices (preserving the current boundary behavior), append the truncated content, and append the same trailing message ("\n\n[... truncated at {max_chars} chars — use `read` for full file]\n" or "\n\n") so behavior remains identical. Ensure you remove the duplicated code blocks and update tests/usages to call the new helper.
425-431: 🏗️ Heavy liftSplit prompt sections into sibling modules to keep file size bounded.
This module is already well beyond the repository’s preferred size limit for Rust files, and new section logic continues to accrete here.
As per coding guidelines
**/*.{ts,tsx,rs}/{src,app/src,app/src-tauri}/**/*.{rs,ts,tsx}: “Keep React component files and Rust modules at ≤ ~500 lines; split growing modules into multiple files.”🤖 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/openhuman/agent/prompts/mod.rs` around lines 425 - 431, The file is too large and the PersonalityRosterSection logic should be split into a sibling module: create a new module containing the PersonalityRosterSection struct and all its associated impls/functions that reference PromptContext::personality_roster and related helpers (move everything that renders the "Available Personalities" section into that file), then in the original prompts mod add `pub mod personality_roster_section;` and re-export the symbol with `pub use personality_roster_section::PersonalityRosterSection;`; update any internal references/imports in the moved code to use the new module path and run cargo check to fix any missing use statements.
🤖 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/openhuman/agent/personality_paths.rs`:
- Around line 42-44: The current join of workspace_dir with profile.soul_md_path
permits absolute paths and “..” traversal; before calling
std::fs::read_to_string(&path) validate and sanitize soul_md_path by rejecting
absolute paths and any parent-component traversal (e.g., ensure
Path::is_absolute is false and none of path.components() are ParentDir), then
resolve the combined path (canonicalize or join then canonicalize) and confirm
the resulting canonical path has workspace_dir as a prefix (e.g., use
strip_prefix) before reading; update the logic around profile.soul_md_path /
path / workspace_dir to perform these checks and return an error if validation
fails to prevent escaping the workspace.
In `@src/openhuman/agent/profiles.rs`:
- Around line 234-254: When upserting an AgentProfile, don't always regenerate a
new memory_dir_suffix when the incoming profile.memory_dir_suffix is None —
first look up an existing profile in state.profiles by profile.id and reuse its
memory_dir_suffix if present; only generate a new unique suffix (the loop that
produces "-{n}") when no existing profile is found and profile.id !=
DEFAULT_PROFILE_ID. Also normalize empty-string suffixes by treating Some("") as
None so non-default profiles cannot collide with the default memory/ namespace;
apply the same logic change in the other mirrored block (lines referenced
572-573) and ensure you construct the returned AgentProfile with the reused or
newly generated memory_dir_suffix accordingly.
In `@src/openhuman/agent/prompts/mod.rs`:
- Around line 454-456: The current truncation uses byte-index slicing
(&summary[..200]) after checking summary.len(), which is bytes and can split
UTF-8 characters; update the logic around the truncated variable to use a
character-aware approach: check character count (via summary.chars().count()
rather than summary.len()) and construct the truncated string from the first 200
characters using the chars iterator or find the byte boundary with char_indices
before slicing, replacing the unsafe &summary[..200] usage while keeping the
existing summary/truncated variable names.
In `@src/openhuman/memory_store/unified/init.rs`:
- Around line 45-52: The constructor new_with_memory_dir currently joins
workspace_dir with memory_subdir without validating memory_subdir, allowing
absolute paths, parent-traversal segments, or multi-component values that can
escape the workspace; update new_with_memory_dir to validate memory_subdir
before join by rejecting empty strings, any path that is absolute, contains '..'
components, or contains path separators (so it must be a single normal
component), or alternatively enforce an allowlist like exact "memory" or prefix
"memory-"; on validation failure return an anyhow::Error indicating invalid
memory_subdir and only then perform workspace_dir.join(memory_subdir) to compute
memory_dir and namespaces_dir.
In `@src/openhuman/tools/impl/agent/delegate_to_personality.rs`:
- Around line 135-179: PersonalityContext created by
PersonalityContext::from_profile (personality_ctx) is never passed into the
delegated run because SubagentRunOptions currently sets context: None; update
SubagentRunOptions in this function to supply the personality-scoped context
(e.g., personality_ctx.context or an appropriate reference/value from
PersonalityContext) so that run_subagent receives the personality execution
state, ensuring memory/identity/tooling are applied during delegation (adjust
any required ownership/cloning of personality_ctx when constructing
SubagentRunOptions and calling run_subagent).
---
Nitpick comments:
In `@src/openhuman/agent/prompts/mod.rs`:
- Around line 1352-1376: Both inject_inline_content and inject_snapshot_content
duplicate the same truncation/injection logic; extract that logic into a single
helper (e.g., truncate_and_inject or format_and_append_content) and have both
inject_inline_content and inject_snapshot_content call it. The helper should
accept (prompt: &mut String, label: &str, content: &str, max_chars: usize),
perform the trim/empty check, write the "### {label}\n" header, truncate by
character count using char_indices (preserving the current boundary behavior),
append the truncated content, and append the same trailing message ("\n\n[...
truncated at {max_chars} chars — use `read` for full file]\n" or "\n\n") so
behavior remains identical. Ensure you remove the duplicated code blocks and
update tests/usages to call the new helper.
- Around line 425-431: The file is too large and the PersonalityRosterSection
logic should be split into a sibling module: create a new module containing the
PersonalityRosterSection struct and all its associated impls/functions that
reference PromptContext::personality_roster and related helpers (move everything
that renders the "Available Personalities" section into that file), then in the
original prompts mod add `pub mod personality_roster_section;` and re-export the
symbol with `pub use personality_roster_section::PersonalityRosterSection;`;
update any internal references/imports in the moved code to use the new module
path and run cargo check to fix any missing use statements.
In `@tests/personality_e2e.rs`:
- Around line 1-728: The test file is too large and should be split by domain
(profile lifecycle, memory isolation, prompt behaviour, integration filtering,
thread binding); extract shared helpers (make_profile, empty_prompt_context,
FakeIntegration) into a single shared helper file and have each new test file
include it. Concretely: create smaller test files (e.g.
personality_profile_lifecycle.rs, personality_memory_isolation.rs,
personality_prompt.rs, personality_integration.rs,
personality_thread_binding.rs) and move the corresponding test functions
(backwards_compat_old_profiles_json_deserializes_cleanly,
three_personalities_get_sequential_memory_suffixes,
deleted_suffix_is_reused_by_next_personality,
personality_fields_roundtrip_through_upsert,
default_profile_memory_suffix_cannot_be_overridden into the profile file;
two_personalities_have_isolated_sqlite_stores,
personality_memory_persists_across_reopens into the memory file;
resolve_personality_* and subdir_helpers and PersonalityContext tests into
prompt/memory resolution file;
IdentitySection/UserFilesSection/PersonalityRosterSection tests into
personality_prompt.rs; filter_integrations tests into
personality_integration.rs; thread_personality_* tests into
personality_thread_binding.rs), move shared imports and helper functions into
tests/personality_helpers.rs and include them into each new test file using
include!("personality_helpers.rs") (or place helpers into a common library
module if preferred), and ensure each new test file imports the same symbols
used (e.g., AgentProfileStore, UnifiedMemory, PersonalityContext,
IdentitySection, filter_integrations, ConversationStore, ensure_thread,
update_thread_title) so the tests compile and stay under the ~500-line
guideline.
🪄 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: a04fb8b5-39e1-4690-bf05-7e3253a803f6
📒 Files selected for processing (54)
app/src/types/agentProfile.tsapp/src/types/thread.tssrc/openhuman/agent/agents/archivist/prompt.rssrc/openhuman/agent/agents/code_executor/prompt.rssrc/openhuman/agent/agents/critic/prompt.rssrc/openhuman/agent/agents/crypto_agent/prompt.rssrc/openhuman/agent/agents/help/prompt.rssrc/openhuman/agent/agents/integrations_agent/prompt.rssrc/openhuman/agent/agents/loader.rssrc/openhuman/agent/agents/markets_agent/prompt.rssrc/openhuman/agent/agents/mcp_setup/prompt.rssrc/openhuman/agent/agents/morning_briefing/prompt.rssrc/openhuman/agent/agents/orchestrator/prompt.rssrc/openhuman/agent/agents/planner/prompt.rssrc/openhuman/agent/agents/researcher/prompt.rssrc/openhuman/agent/agents/skill_creator/prompt.rssrc/openhuman/agent/agents/summarizer/prompt.rssrc/openhuman/agent/agents/tool_maker/prompt.rssrc/openhuman/agent/agents/tools_agent/prompt.rssrc/openhuman/agent/agents/trigger_reactor/prompt.rssrc/openhuman/agent/agents/trigger_triage/prompt.rssrc/openhuman/agent/debug/mod.rssrc/openhuman/agent/harness/harness_gap_tests.rssrc/openhuman/agent/harness/session/turn.rssrc/openhuman/agent/harness/subagent_runner/ops.rssrc/openhuman/agent/harness/test_support_test.rssrc/openhuman/agent/memory_loader.rssrc/openhuman/agent/mod.rssrc/openhuman/agent/personality_paths.rssrc/openhuman/agent/profiles.rssrc/openhuman/agent/prompts/mod.rssrc/openhuman/agent/prompts/mod_tests.rssrc/openhuman/agent/prompts/types.rssrc/openhuman/agent/triage/evaluator.rssrc/openhuman/channels/providers/telegram/remote_control.rssrc/openhuman/learning/prompt_sections.rssrc/openhuman/memory_conversations/bus.rssrc/openhuman/memory_conversations/store.rssrc/openhuman/memory_conversations/store_tests.rssrc/openhuman/memory_conversations/types.rssrc/openhuman/memory_store/unified/helpers.rssrc/openhuman/memory_store/unified/init.rssrc/openhuman/memory_store/unified/mod.rssrc/openhuman/memory_tools/prompt.rssrc/openhuman/subconscious/schemas.rssrc/openhuman/threads/ops.rssrc/openhuman/threads/ops_tests.rssrc/openhuman/threads/welcome_migration.rssrc/openhuman/tools/impl/agent/delegate_to_personality.rssrc/openhuman/tools/impl/agent/mod.rssrc/openhuman/tools/impl/agent/spawn_subagent.rssrc/openhuman/tools/impl/agent/spawn_worker_thread.rssrc/openhuman/tools/ops.rstests/personality_e2e.rs
…and delegation Each AgentProfile now carries personality config — avatar, voice, SOUL.md (inline or file-based), composio integration allowlist, and a memory directory suffix that scopes the SQLite store per personality (memory/, memory-1/, memory-2/, …). The default profile is the master agent and can delegate to other personalities via the new delegate_to_personality tool. Threads carry an optional personality_id for per-personality chat routing. Key changes: - AgentProfile extended with 8 personality fields (backwards-compatible) - UnifiedMemory.new_with_memory_dir() for personality-scoped storage - personality_paths.rs: path helpers, PersonalityContext, filter_integrations - PromptContext gains personality_soul_md, personality_memory_md, and personality_roster; IdentitySection/UserFilesSection respect overrides - PersonalityRosterSection injects available personalities into master prompt - delegate_to_personality tool registered in global tool list - ConversationThread/CreateConversationThread carry personality_id - Frontend AgentProfile and Thread TS interfaces updated
…zation
Adds tests/personality_e2e.rs covering 23 scenarios across the personality
feature: profile lifecycle (backwards compat, suffix auto-assignment,
field roundtrip, default invariants), memory isolation (separate SQLite
stores, persistence across reopens), file resolution (soul fallback chain,
personality MEMORY.md, PersonalityContext aggregation), prompt sections
(IdentitySection, UserFilesSection, PersonalityRosterSection with
truncation), integration filtering (None/allowlist/empty/case-insensitive),
and thread-personality binding (persistence, None defaults, title-update
preservation, free-function APIs).
Two bugs surfaced and fixed:
1. write_markdown_doc hardcoded "memory/namespaces/..." in the sidecar
relative path, so personality-scoped writes (memory-1/) silently
targeted the default memory directory. Now uses the actual memory
subdir name from self.memory_dir.
2. normalise_state did not restore is_master/memory_dir_suffix invariants
on the default profile when loading an old agent_profiles.json. Now
forces is_master = true and memory_dir_suffix = Some("") for the
default entry so upgrades pick up the correct defaults automatically.
…hread search tests After rebasing onto upstream/main, four new CreateConversationThread literals landed in store_tests.rs (cross-thread search coverage from tinyhumansai#2756 / tinyhumansai#2849) without the personality_id field added in this PR. Add personality_id: None to each so the lib test target compiles.
31b4e22 to
6157066
Compare
graycyrus
left a comment
There was a problem hiding this comment.
@senamakel CI is still pending on this PR (all checks just kicked off), so I'll hold off on a formal approve/request-changes until those land. That said, I spotted a few things while reading through the code:
1. No is_master guard in DelegateToPersonalityTool
DelegateToPersonalityTool is registered globally in tools/ops.rs alongside every other tool, which means any personality agent — including delegated sub-agents — can call it. The design intent per the PR body is that only the master agent should delegate, but the tool itself does nothing to enforce that. A delegated sub-agent could delegate further, and with no depth tracking (see below) that's a silent loop until context is exhausted.
Fix: at the top of execute(), resolve the calling agent's profile from the parent context and return an early error if !profile.is_master.
2. No delegation depth limit
Related to the above — run_subagent can invoke tools, including delegate_to_personality. There's no depth counter anywhere in the call chain. SpawnSubagentTool presumably has its own recursion guard; DelegateToPersonalityTool bypasses it entirely. A cycle like A→B→A (or A delegating to itself) will run until the process OOMs or the model context window fills up, with no useful error surfaced to the caller.
Fix: thread a delegation depth counter through the parent context (or a task-local), increment on entry, and return ToolResult::error when it exceeds a reasonable cap (3–5 levels).
3. AgentProfileStore::new() on every delegation call
delegate_to_personality.rs:190 constructs a fresh AgentProfileStore (which reads agent_profiles.json from disk) on every tool call. In a session with multiple delegations this means repeated disk I/O where none is needed. The parent context already carries workspace state — share the store through there or cache it at the session level.
CodeRabbit already caught the path traversal issues in soul_md_path and memory_subdir, the suffix churn bug in profiles.rs, the UTF-8 slicing in the roster section, and the dropped PersonalityContext in SubagentRunOptions. Those are the blockers — fix those first.
One more thing: the TS types in agentProfile.ts and thread.ts shipped without pnpm typecheck passing (pushed with --no-verify). I understand it's a pre-existing breakage in an unrelated file, but it means the new fields haven't been type-checked against the rest of the frontend. Worth unblocking that check in a follow-up or confirming the new fields aren't referenced in any existing TS that could silently break.
Apply the actionable items from the PR tinyhumansai#2895 review pass: 1. **soul_md_path traversal**: reject absolute paths, `..`, root/prefix components in `resolve_personality_soul` so profiles can't read files outside the workspace. Falls back to `soul_md` inline when rejected. 2. **memory_subdir validation**: `UnifiedMemory::new_with_memory_dir` now requires a single normal path component — no empty string, no `/`, no `..`. 3. **suffix preservation on re-upsert**: re-upserting an existing personality without `memory_dir_suffix` now reuses the stored suffix instead of allocating a fresh one (which would silently orphan the personality's existing SQLite store). 4. **empty memory_dir_suffix filtered to None**: `normalise_profile` drops empty-string suffixes so non-default profiles can't share the default's `memory/` directory by accident. The default profile still gets its `Some("")` invariant restored in `normalise_state`. 5. **PersonalityRosterSection char-safe truncation**: switch from byte slicing (`&summary[..200]`) to `summary.chars().take(200)` so multibyte characters don't panic. 6. **delegate_to_personality is_master guard**: only the master personality may delegate; otherwise the tool errors out. Prevents sub-agent recursion since cross-personality delegation has no built-in depth counter. 7. **delegate_to_personality personality_ctx no longer silently dropped**: the resolved `PersonalityContext` is now serialised into the sub-agent's `context` blob — identity, SOUL.md (capped 800 chars), MEMORY.md (capped 800 chars), and the user-provided caller context. Runtime memory-store and integration-allowlist swapping is a documented follow-up (the sub-agent currently inherits the parent's `ParentExecutionContext` wholesale).
graycyrus
left a comment
There was a problem hiding this comment.
@senamakel the fixes in this commit land cleanly — the three major issues I flagged are addressed.
Resolved from review 1:
is_masterguard is in place (delegate_to_personality.rs:276). Non-master callers get an error return, which also covers the recursion/depth-limit case since delegated sub-agents can't haveis_master: true.PersonalityContextis no longer dropped. Soul and memory overrides are injected into the sub-agent's context string. The comment at line 325 is honest about the limitation (memory writes still go to the parent store) and that's acceptable as a phase boundary.- Path traversal on both
soul_md_pathandmemory_subdiris properly validated inpersonality_paths.rsandunified/init.rs.
One new issue spotted:
In src/openhuman/threads/ops.rs, both thread_upsert and thread_create_new hardcode personality_id: None instead of forwarding request.personality_id. The CreateConversationThread type now has the field, and the store handles it correctly, but the RPC entry points silently drop it. Any thread created or upserted through the normal RPC path will lose its personality binding — only direct ensure_thread calls (in bus.rs, store paths, etc.) carry it through.
Fix is straightforward:
// thread_upsert (~line 168)
personality_id: request.personality_id,
// thread_create_new (~line 198)
personality_id: request.personality_id,CI is still running so I'll hold on the final approval until it's green. Once that and the ops.rs forwarding are sorted, this is good to go.
graycyrus flagged that personality_id was hardcoded to None in thread_upsert and thread_create_new (src/openhuman/threads/ops.rs), silently dropping the field even though the type and store both support it — so personality-thread binding wouldn't work for any thread created via the normal RPC path. Fixed by: - Adding `personality_id: Option<String>` to UpsertConversationThreadRequest and CreateConversationThreadRequest in memory/rpc_models.rs. - Adding `personality_id: Option<String>` to ConversationThreadSummary so the field round-trips back to the UI on listings. - Forwarding `request.personality_id` into CreateConversationThread in both upsert and create_new paths. - Mapping `thread.personality_id` into ConversationThreadSummary in thread_to_summary.
graycyrus
left a comment
There was a problem hiding this comment.
@senamakel hey! the fix looks correct — personality_id is now properly wired through both CreateConversationThreadRequest and UpsertConversationThreadRequest, forwarded in thread_upsert and thread_create_new, and included in the thread_to_summary response path. serde(default) keeps it backwards compat and skip_serializing_if = "Option::is_none" is the right call on the summary type.
All prior findings are addressed. CI is still pending — once those checks come back green i'll approve this.
graycyrus
left a comment
There was a problem hiding this comment.
CodeRabbit-style Review — PR #2895 (Multi-Agent Personalities)
Overall: Well-engineered Phase 1 scaffold. Backwards-compatible serde, strong path guards, 23 E2E tests. However, the core advertised features (personality context in live turns, memory isolation during delegation) are not wired up yet — they only work in tests, not at runtime. 2 blockers, 3 major, 2 refactor, 4 nitpicks.
Verified / Looks Good
UnifiedMemory::new_with_memory_dirpath guard:components().count() == 1+Normalonly — tight- Backwards compat: all new fields
#[serde(default)]; old JSON deserializes cleanly (tested) normalise_statecorrectly re-appliesis_master=true+memory_dir_suffix=Some("")for default profile- Suffix slot reuse after deletion works (tested)
thread_index_unlockedcorrectly capturespersonality_idfrom Upsert log entriesfilter_integrationscase-insensitive match correctly tested- No new JS injection in CEF webviews, no dynamic imports
Questions
delegate_to_personalityreads active profile from storedactive_profile_id— during sub-agent invocation, is this always the parent session's selection?resolve_personality_memory_mdhard-codespersonalities/{id}/MEMORY.mdwhilesoul_md_pathsupports arbitrary relative paths. Is the asymmetry intentional?
There was a problem hiding this comment.
🚫 Blocker: Personality context hardcoded to None in the live agent turn
PromptContext hardcodes personality_soul_md: None, personality_memory_md: None, personality_roster: vec![] unconditionally. This means:
- A user who creates a personality and starts a session bound to it gets the default SOUL.md/MEMORY.md regardless of config
PersonalityRosterSectionis never added to anySystemPromptBuilder— the master agent never learns which personalities existIdentitySectionSOUL.md override andUserFilesSectionMEMORY.md override are dead code from the user's perspective
This is the core contract the PR advertises. Without it, personalities are profile-level metadata only with no behavioral effect in the main chat loop.
Fix: Load the active personality context and wire it into PromptContext. The workspace_dir is already available in self.
There was a problem hiding this comment.
🚫 Blocker: Sub-agent inherits parent's memory store — isolation guarantee broken
The PR says "each personality has its own scoped memory," but delegate_to_personality passes the parent's ParentExecutionContext.memory to run_subagent. The personality gets its own SQLite DB (correct plumbing), but the delegation code bypasses it entirely.
The test two_personalities_have_isolated_sqlite_stores verifies the plumbing exists, but the runtime delegation path doesn't use it.
At minimum, this needs:
- A prominent `// TODO(#ISSUE): memory isolation not yet enforced during delegation` comment linked to a tracking issue
- Same treatment for
composio_integrationsallowlist — users who configure it expecting tool scoping get all tools regardless - Update the PR description to clarify this is Phase 1 (data model only, runtime wiring is follow-up)
There was a problem hiding this comment.
⚠️ Major: `is_master` is user-controllable on non-default profiles
For non-default profiles, `upsert` preserves the raw `is_master` from the caller. A client can POST `is_master: true` on a custom profile via `agent.profile_upsert`, passing the master-guard in `delegate_to_personality`.
Fix: Force `is_master: false` for non-default profiles:
```rust
} else {
AgentProfile {
built_in: profile.built_in || ...,
is_master: false, // only DEFAULT_PROFILE_ID may be master
..profile
}
};
```
There was a problem hiding this comment.
⚠️ Major: `is_safe_relative_path` does not guard against symlink traversal
Rejects `..` and absolute paths at the component level, but doesn't protect against a symlink at a safe path pointing outside the workspace. Since the agent can write files via `file_write`, a prompt-injection attack could create a symlink then upsert a `soul_md_path` pointing to it, leaking arbitrary files into the personality's SOUL context.
Fix: After resolving the path, canonicalize both workspace and target, then verify `canonical_path.starts_with(&canonical_workspace)`.
There was a problem hiding this comment.
⚠️ Major 5 + Refactors + Nitpicks
Major 5: `normalise_profile` erases `memory_dir_suffix = Some("")` for non-default profiles. Document that `Some("")` is a sentinel only for the default profile, or change filter logic.
Refactor 6: `delegate_to_personality` calls `store.resolve` twice (two file reads). Fold into one load with two lookups from the returned state.
Refactor 7: `inject_inline_content` doc comment is mangled — split across two functions with orphaned fragment. Clean it up.
Nitpicks:
- `profiles.rs` — suffix assignment loop duplicated for existing-without-suffix and new-profile paths. Extract to `fn next_available_suffix()`.
- `delegate_to_personality.rs:327` — `let _ = state;` is a no-op. Use `let (_, active_profile) = ...` instead.
- `agentProfile.ts:17` — `isMaster` lacks `| null`. If Rust skips serializing, TS gets `undefined` not `false`.
- `personality_paths.rs:155` — synchronous fs reads in async context. Add a comment noting workspace is always local.
…al, TODO markers, doc cleanup
- profiles.rs: force is_master:false on non-default profiles in upsert so
callers cannot promote themselves to master (Major 3)
- profiles.rs: extract next_available_suffix() helper, eliminating the two
near-identical loop blocks (Refactor 6 / nitpick)
- profiles.rs: document Some("") sentinel on memory_dir_suffix in
normalise_profile (Major 5)
- personality_paths.rs: add canonicalize-based symlink traversal guard in
resolve_personality_soul after the component-level check (Major 4)
- personality_paths.rs: add comment explaining synchronous reads are
intentional for local workspace files (nitpick)
- delegate_to_personality.rs: add prominent TODO(phase-2) comment on memory
isolation gap — composio allowlist and per-personality SQLite not yet wired
through run_subagent (Blocker 2)
- delegate_to_personality.rs: fold double store.resolve() into one — look up
target profile from the already-loaded state.profiles, drop let _ = state
(Refactor 6)
- turn.rs: add TODO(phase-2) comments on personality_soul_md/memory_md/roster
fields, explaining the wiring gap (Blocker 1)
- prompts/mod.rs: fix mangled doc comment on inject_inline_content (Refactor 7)
- agentProfile.ts: add | null to isMaster type (nitpick)
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/openhuman/tools/impl/agent/delegate_to_personality.rs (1)
159-171:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftDelegation path still violates personality isolation guarantees.
Line 159 explicitly confirms delegated runs still use the parent memory store and unfiltered integrations, so personality-scoped memory/access is not enforced on the actual execution path. This is a major behavior gap for this feature and should be wired before release (or the tool should be gated until phase-2 lands).
🤖 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/openhuman/tools/impl/agent/delegate_to_personality.rs` around lines 159 - 171, The delegation path currently uses the parent's memory and unfiltered integrations; fix by constructing a personality-scoped UnifiedMemory via UnifiedMemory::new_with_memory_dir using personality_ctx.memory_suffix and pass that new UnifiedMemory instance into run_subagent by adding a field to SubagentRunOptions (e.g., memory_override) and wiring it through SubagentRunOptions creation; additionally apply personality_ctx.composio_allowlist when building SubagentRunOptions.toolkit_override so integrations are filtered for the delegated sub-agent (ensure run_subagent consumes toolkit_override and the new memory_override).
🤖 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.
Duplicate comments:
In `@src/openhuman/tools/impl/agent/delegate_to_personality.rs`:
- Around line 159-171: The delegation path currently uses the parent's memory
and unfiltered integrations; fix by constructing a personality-scoped
UnifiedMemory via UnifiedMemory::new_with_memory_dir using
personality_ctx.memory_suffix and pass that new UnifiedMemory instance into
run_subagent by adding a field to SubagentRunOptions (e.g., memory_override) and
wiring it through SubagentRunOptions creation; additionally apply
personality_ctx.composio_allowlist when building
SubagentRunOptions.toolkit_override so integrations are filtered for the
delegated sub-agent (ensure run_subagent consumes toolkit_override and the new
memory_override).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a2c619cc-3691-4250-a5ed-b595657fb494
📒 Files selected for processing (8)
app/src/types/agentProfile.tssrc/openhuman/agent/harness/session/turn.rssrc/openhuman/agent/personality_paths.rssrc/openhuman/agent/profiles.rssrc/openhuman/agent/prompts/mod.rssrc/openhuman/memory/rpc_models.rssrc/openhuman/threads/ops.rssrc/openhuman/tools/impl/agent/delegate_to_personality.rs
✅ Files skipped from review due to trivial changes (1)
- app/src/types/agentProfile.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/openhuman/agent/harness/session/turn.rs
- src/openhuman/agent/personality_paths.rs
- src/openhuman/agent/profiles.rs
There was a problem hiding this comment.
Deferred — tests/personality_e2e.rs (file size, ~500-line guideline)
CodeRabbit flags this 728-line file as over the repo's ~500-line guideline and suggests splitting into domain-focused files: personality_profile_lifecycle.rs, personality_memory_isolation.rs, personality_prompt.rs, personality_integration.rs, personality_thread_binding.rs with shared helpers in tests/personality_helpers.rs. This is valid but is a follow-up cleanup — splitting tests mid-PR adds churn without behavioral change. Recommend doing this in a dedicated cleanup PR before the next feature lands on this file.
Deferred — src/openhuman/agent/prompts/mod.rs (duplicate truncation logic)
CodeRabbit suggests deduplicating inject_inline_content and inject_snapshot_content (they share identical truncation/injection logic). The fix: make inject_snapshot_content delegate to inject_inline_content. Safe and mechanical — recommend as a focused cleanup commit.
Blocker (Phase-2, must fix before release) — delegate_to_personality.rs:159
Memory isolation and composio allowlist filtering are not yet applied during delegation. The TODO(phase-2) comment documents the gap. To fix:
- Add
memory_override: Option<Arc<UnifiedMemory>>toSubagentRunOptions; construct viaUnifiedMemory::new_with_memory_dirusingpersonality_ctx.memory_suffix. - Apply
personality_ctx.composio_allowlisttoSubagentRunOptions.toolkit_override.
Until phase-2 lands, the delegate_to_personality tool should either be gated (feature flag or guarded at the RPC layer) or the PR description should explicitly note the isolation gap for users/reviewers. Currently a delegated personality has full read/write access to the parent memory store and all parent integrations.
Summary
AgentProfilenow carries personality config — avatar, voice,SOUL.md(inline or file-based), composio integration allowlist, and an auto-assigned memory directory suffix (memory/,memory-1/,memory-2/...).delegate_to_personalitytool lets the master agent (default profile) delegate tasks to other personality agents, each with its own scoped memory and identity.ConversationThreadcarries an optionalpersonality_idso independent chat threads can be bound to a specific personality.PersonalityRosterSectioninjects the available personalities into the master agent's system prompt with truncated memory summaries.memory/(silently misrouting personality writes);normalise_statedidn't restore default profile invariants on legacy JSON load.Problem
Today every agent session shares a single workspace identity: one
SOUL.md, oneMEMORY.md, onememory/SQLite DB, one voice, one set of composio integrations. The existingAgentProfilesystem lets the UI switch between agent archetypes (orchestrator, researcher, planner, critic) with model/temperature/tool overrides, but profiles share all workspace state.This change gives each profile its own personality with backwards compatibility — existing single-agent setups work unchanged (the default profile keeps
memory/as-is and is automatically promoted tois_master: trueon load).Solution
Phase 1 —
AgentProfileextension (profiles.rs): 8 new optional fields (avatar_url,voice_id,soul_md,soul_md_path,composio_integrations,memory_dir_suffix,is_master,sort_order). Suffix auto-assignment fills the lowest available slot (-1,-2, ...) and reuses freed slots after deletion.Phase 2 — Memory scoping:
UnifiedMemory::new_with_memory_dir()accepts a subdir name so each personality gets its own SQLite DB.personality_paths.rsexposes path helpers (memory_subdir_for_suffix, etc.),PersonalityContextaggregating per-session overrides, and the genericfilter_integrationshelper.PromptContextgainspersonality_soul_md,personality_memory_md,personality_roster;IdentitySection/UserFilesSectionhonour overrides and fall back to workspace files when absent.Phase 3 — Master delegation (
delegate_to_personality.rs): follows theSpawnSubagentToolpattern, resolves the target profile from the store, runs a sub-agent turn with the personality's context.PersonalityRosterSectioninjects available personalities into the master agent's prompt.Phase 4 — Thread binding:
personality_idpropagates throughConversationThread,CreateConversationThread,ThreadLogEntry::Upsert,ThreadIndexEntry, and survives title/label updates.Phase 5 — Frontend types: TypeScript
AgentProfileandThreadinterfaces updated.Tests: 23 E2E scenarios in
tests/personality_e2e.rscover profile lifecycle, memory isolation, file resolution, prompt sections, integration filtering, and thread binding (all passing, ~0.2s).Submission Checklist
tests/personality_e2e.rsplus the in-crate unit tests added inprofiles.rs,personality_paths.rs, andunified/init.rs.Impact
agent_profiles.jsonfiles deserialize cleanly; the default profile is auto-promoted tois_master: trueandmemory_dir_suffix: ""on load.delegate_to_personalityis registered globally but only useful when the active profile hasis_master: trueand other personalities exist.Related
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
Validation Run
Validation Blocked
Behavior Changes
Parity Contract
Duplicate / Superseded PR Handling
Summary by CodeRabbit
New Features
Tests