feat: adaptive learning system — skill synthesis, user profiles, session search#1187
feat: adaptive learning system — skill synthesis, user profiles, session search#1187smkrv wants to merge 9 commits intonearai:mainfrom
Conversation
Add V13 PostgreSQL and libSQL migrations with three new tables: - session_summaries (FTS + vector search for session history) - user_profile_facts (AES-256-GCM encrypted user profile storage) - synthesized_skills (audit log with approval workflow) Add standalone store traits (SessionSearchStore, UserProfileStore, LearningStore) injected via Arc<dyn T> through DatabaseHandles — NOT added to Database supertrait to preserve backward compatibility. Implement both PostgreSQL and libSQL backends with full dialect parity including CHECK constraints, FTS5 triggers, composite indexes. Security: IDOR protection (user_id filtering on all get/update methods), FTS5 query sanitization, proper error propagation (no unwrap_or). Add LearningConfig and UserProfileConfig with resolve() pattern, integrated into Config::build() and Config::for_testing().
New src/learning/ module with: - PatternDetector: identifies synthesis candidates (complex tool chains, novel combinations, user-requested, high quality completions) - SkillValidator: structural + safety validation of generated SKILL.md (parser, threat scanning, size limits, description/name checks) - SkillSynthesizer: trait + LLM-powered implementation with system/user message separation and wrap_external_content on all user-origin data - LearningEvent: typed event for background worker integration Security additions to ironclaw_safety crate: - scan_content_for_threats(): regex-based threat pattern scanner with real NFKC unicode normalization (unicode-normalization crate) and zero-width character stripping - Patterns: prompt injection, credential exfiltration, data theft, deception, SSH backdoor, destructive commands, secret references, role manipulation, prompt delimiter injection 19 learning tests + 90 safety tests pass. Clippy 0 warnings.
New src/user_profile/ module with: - UserProfile, ProfileFact, FactCategory, FactSource types with prompt formatting (BTreeMap sorted categories, char-boundary safe truncation, capitalized headings) - EncryptedProfileEngine: trait + AES-256-GCM implementation via SecretsCrypto (HKDF per-fact salt, safety scan on key+value) - ProfileDistiller: LLM-powered fact extraction from conversations with system/user message separation, wrap_external_content on both user messages AND existing profile facts, key format validation (alphanumeric+underscore, max 64), value length limit (512 chars), per-run fact cap (5), byte limit on input (4KB), threat scanning Security: existing profile facts wrapped before LLM injection, first message always included even if exceeding byte limit, fact key/value validation prevents injection through stored data. 11 tests (types roundtrip, truncation, parsing, safety rejection). Clippy 0 warnings.
New tools for the adaptive learning system: - session_search: FTS search over past conversation summaries - skill_list_pending: list synthesized skills awaiting approval - skill_approve: approve/reject synthesized skills with file write Security hardening: - Path traversal prevention: is_safe_skill_name() validation + starts_with() defense-in-depth check on resolved path - PROTECTED_TOOL_NAMES: all 3 tools registered to prevent shadowing - ApprovalRequirement::Always only for "accept", "reject" is safe - tokio::fs for non-blocking file I/O (not std::fs) - Errors propagated to user (no silent failure on disk write) Added register_learning_tools() to ToolRegistry.
SkillSource::Synthesized variant for auto-synthesized skills: - discover_all() scans installed_skills/auto/ with Installed trust - validate_remove() allows removal of synthesized skills - CLI format_source() displays "synthesized" label Profile management tools (profile_view, profile_edit, profile_clear): - View all learned facts with category/key/value/confidence - Manually add facts with Explicit source and max confidence - Remove specific facts by category/key Dispatcher integration: - AgentDeps extended with learning_tx, profile_engine, user_profile_config - Profile injection into system prompt via Reasoning::append_system_context() - Double safety: sanitize_tool_output + scan_content_for_threats on composed text - All AgentDeps construction sites updated (main, testing, dispatcher tests, test rigs) PROTECTED_TOOL_NAMES updated with all 6 new tools. Clippy 0 warnings. 3042 tests pass.
Background worker (spawn_learning_worker): - Bounded mpsc channel (capacity 32) for LearningEvent reception - PatternDetector evaluation → LLM synthesis → SkillValidator check - Skills failing validation are discarded (not written to DB) - Graceful shutdown on sender drop Heuristic quality_score (no LLM call required): - FNV-1a 64-bit hash for content deduplication - Base 50/20 + diversity/turn/volume bonuses, capped at 100 Full wiring in main.rs and app.rs: - AppComponents extended with learning store handles - learning_tx spawned when LEARNING_ENABLED=true - profile_engine created when USER_PROFILE_ENABLED=true + master key Security hardening: - skill_approve: blocks skills with safety_scan_passed=false - skill_approve: re-validates content before writing to disk - format_for_prompt: strips newlines from fact values - profile_edit: redacts value from ToolOutput (SSE/log safe) - FTS query length limit (512 chars) prevents DoS 4 unit tests for quality scoring. Clippy 0 warnings.
…tcher events SkillStatus enum (replaces string literals): - Typed enum with Pending/Accepted/Rejected variants - as_str()/from_str_opt() for DB serialization - All trait signatures, implementations, tools, and worker updated libSQL transaction safety: - upsert_session_summary: INSERT + SELECT-back wrapped in transaction - upsert_profile_fact: INSERT + SELECT-back wrapped in transaction Dispatcher integration: - LearningEvent sent after successful turn via try_send (non-blocking) - Tool names extracted from reasoning context tool_calls - ProfileDistiller auto-called in background tokio::spawn task - Profile facts extracted and stored after each turn PostgreSQL FTS query validation (parity with libSQL): - Empty query rejection and 512-char length limit Clippy 0 warnings. 3044 tests pass.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a significant 'level-up' to the project by integrating an adaptive learning system. This system empowers the agent to autonomously learn from its interactions, personalize user experiences through encrypted profiles, and efficiently retrieve past conversational context. The changes are designed to make the agent smarter and more adaptable, while rigorously maintaining security and privacy standards through multiple layers of validation and encryption. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This is an impressive and ambitious pull request that introduces a sophisticated adaptive learning system. The architecture is well-designed with clear separation of concerns between skill synthesis, user profiles, and session search. The focus on security is particularly noteworthy, with multiple layers of defense including content wrapping, threat scanning, structural validation, and strong encryption. The code is high quality, well-documented, and includes good test coverage. I've identified a couple of potential race conditions that could lead to data corruption or bypassing resource limits, but overall this is an excellent contribution.
Gemini Code Assist correctly identified that the 8-character hash prefix used for auto-skill directory names could collide. Using the skill's UUID (guaranteed unique) as the directory name eliminates this risk. The skill's activation name is read from SKILL.md frontmatter, not the directory name. Removed is_safe_skill_name() — no longer needed since UUID paths are inherently safe (no traversal possible).
|
Re: TOCTOU in Good catch on the race condition — this is a real concern at scale. Here's the current mitigation and why a per-user mutex isn't the right fit yet: Current state:
Why not per-user mutex:
What we do have:
Happy to add the atomic DB-level constraint as a follow-up if the team thinks the current guardrails aren't sufficient. |
zmanian
left a comment
There was a problem hiding this comment.
Review: REQUEST CHANGES
Strong implementation quality and well-thought-out security architecture across ~4,500 lines. Three independent subsystems (skill synthesis, user profiles, session search), all disabled by default. But functional gaps need addressing.
What's Good
- Architecture: Standalone store traits (
SessionSearchStore,UserProfileStore,LearningStore) rather than extending theDatabasesupertrait -- excellent decision, avoids breaking existing implementations. - Dual-backend: Both PostgreSQL + libSQL correctly implemented with proper dialect handling (FTS5 with sync triggers vs tsvector GENERATED ALWAYS).
- Security defense-in-depth:
wrap_external_content()on user data,scan_content_for_threats()with NFKC normalization + zero-width stripping, per-fact HKDF-derived AES-256-GCM encryption,ApprovalRequirement::Alwaysprevents LLM self-approval,SkillSource::SynthesizedforcesInstalledtrust level. - Code quality: No
.unwrap()in production code, properthiserrortypes, good unit test coverage (46+ tests).
Critical
C1: profile_clear is N+1 queries
ProfileClearTool::execute() loads all facts then calls remove_fact() one-by-one. With 100 facts (the max), that's 101 DB round-trips. Add a delete_profile_facts_by_category() method to UserProfileStore.
C2: store_fact TOCTOU on max_facts limit
Count check and insert aren't atomic. Concurrent distillation tasks (via tokio::spawn) could race past the limit. Fix with a per-user lock or DB-level constraint.
Important
I1: Session summaries are never populated
upsert_session_summary() exists in both backends but nothing calls it anywhere in the diff. The session_search tool will always return empty results. Either add the summary writer (e.g., on session end/compaction) or remove the tool and note it as "coming in a follow-up."
I2: Dead code from path traversal fix
is_safe_skill_name() and old auto_dir.join(&skill.skill_name) remain after switching to UUID-based directory names. Clean up.
I3: FNV-1a content hash isn't collision-resistant
Used for skill dedup unique index (idx_synthesized_skills_dedup). Two different skill contents could collide and silently fail to record. Replace with SHA-256 (already a transitive dep).
I4: No DB integration tests
Good unit tests but no round-trip tests for the 3 new store traits. Project convention (src/db/CLAUDE.md) calls for integration tests using LibSqlBackend::new_memory().
I5: Unbounded tokio::spawn for distillation
No JoinHandle tracking or cancellation token. Orphaned on agent shutdown (bounded by 120s LLM timeout, but still a leak).
Suggestions
Cargo.lockcontains unrelatedwindows-sysversion downgrades -- split out or confirm intentional..gitignoreaddingdocs/plans/would affect all maintainers -- discuss whether this is desired.- Consider splitting: session search (incomplete) could be a separate PR from skill synthesis + profile engine.
- Add
clear_profile(user_id, agent_id)method toUserProfileStorefor GDPR "forget me" support.
Hey folks, don't think I've lost my mind here, but I'd like to propose this level-up PR with an adaptive learning mechanism that will make the whole project a bit smarter and the routine a bit easier.
I've tried to test this as thoroughly as I could - ran it through 30+ full review-and-fix iterations across Opus and Codex (not code generation - testing, reviewing, and fixing), with multi-agent review panels (Architecture, Code Style, Security, Product Owner, Lead Dev, UI/UX, CPO, and others - all Blocking/High/Critical findings resolved), and also tested locally on my own agents in my own environment.
But I could have missed things (I definitely have :D), including edge cases that only show up under real-world conditions at scale.
Fresh eyes are very welcome.
Would really appreciate help with testing and reviewing all of this - no matter how good LLM-powered code generation and review gets, pulling something like this off solo is tough :) Would be awesome to see this feature get some life and evolve further as a real level-up for the project. And yes,
I swear I'm not crazy — I just went way too deep down this rabbit hole and at some point there was no turning back, so here we are.
What this does
This PR adds an Adaptive Learning System — three independently testable subsystems that let IronClaw autonomously learn from experience while architecturally guaranteeing that learned knowledge cannot leak secrets.
The Three Pillars
1. Autonomous Skill Synthesis (
src/learning/)The agent watches its own successful interactions. When it completes a complex multi-tool task (3+ tool calls with quality score ≥75, diverse tool combinations with ≥2 unique tools, or high quality completions scoring 90+), a background worker synthesizes a reusable SKILL.md from the interaction — via LLM, not templates. The generated skill passes through 6 layers of security validation before the user even sees it. The user must explicitly approve before it goes live. No auto-deployment, ever.
2. Encrypted User Profile Engine (
src/user_profile/)An evolving user model built from conversations. The
ProfileDistillerextracts durable facts (timezone, expertise, communication style) via LLM, encrypts them with AES-256-GCM (per-fact HKDF-derived keys, random 32-byte salt, master key from OS keychain orSECRETS_MASTER_KEYenv var — same crypto as credential storage), and injects them into the system prompt on next turn. The agent remembers you across sessions without storing anything in plaintext.3. Session Search (
src/db/libsql/session_search.rs,src/db/postgres.rs)FTS5 (libSQL) / tsvector (PostgreSQL) full-text search over conversation summaries with vector embedding support for future hybrid search. The
session_searchtool lets the agent recall prior work, decisions, and context from previous conversations.Architecture Diagrams
Full Architecture Pipeline
Skill Lifecycle State Machine
Runtime Sequence Flow
Security Architecture
Database Schema (V13)
Security Architecture (6 defense-in-depth layers)
All learned data flows through IronClaw's existing security primitives:
wrap_external_content()on every untrusted input before it reaches an LLM promptscan_content_for_threats()with NFKC unicode normalization, prompt injection patterns, role manipulation detectionSkillValidatorenforces YAML frontmatter, 16 KiB size limit, 256-char description limit, name/content/description threat scanningSkillSource::SynthesizedforcesInstalledtrust (read-only tools, treated as suggestions), path traversal preventionApprovalRequirement::Alwaysonskill_approvefor accept action — the LLM cannot self-approve its own generated skills (reject is safe and doesn't require approval)Architecture Decisions
SessionSearchStore,UserProfileStore,LearningStore) — NOT added toDatabasesupertrait. Injected viaArc<dyn T>. This keeps the existing DB implementations untouched and lets the system start without these features.Files Changed
src/learning/(7 files),src/user_profile/(5 files)session_search,skill_list_pending,skill_approve,profile_view,profile_edit,profile_clearsrc/db/mod.rswith implementations for both PostgreSQL and libSQLdispatcher.rs: profile injection viaappend_system_context()after system prompt is set, learning event emission after turn completionSkillSource::Synthesizedvariant insrc/skills/registry.rswith forcedInstalledtrust