Self-Evolution System
Traditional AI applications are frozen at release unless a developer updates them by hand. Profy experts are different: they propose improvements from real usage, those proposals pass a code-enforced score gate, and the creator decides whether to accept. This page covers how the system works. If you are a creator wanting to know how to read a suggestion, see Expert Evolution.Boundary first: this is not RSI
Recursive self-improvement refers to a system improving the process that builds the model, recursively raising its own capability. That is not what Profy does.
Profy modifies the document describing how the expert does things, not the brain it thinks with. That limit is a design choice rather than a shortfall — it confines every change to an object that can be reviewed in full.
Three layers of evolution
Three independent background pipelines exist. The names blur together but they govern entirely different things:
The rest of this page covers the third layer only.
Counter storage decisions
The three layers use two different storage strategies, and the reasoning is worth stating:
The in-process dict is capped at
_COUNTER_CAP = 10,000 sessions, evicting the oldest to bound memory. When Redis is unavailable, the evolution counter degrades to the in-process dict — best-effort, reset on restart.
The full path of one evolution run
1
Core selects candidates
The scheduler filters eligible experts by cooldown (
EVOLUTION_COOLDOWN_HOURS, default 24h), failure backoff (EVOLUTION_FAILURE_BACKOFF_HOURS, default 6h), and batch size (EVOLUTION_BATCH_SIZE, default 10).2
Backlog check
Experts whose pending suggestions have reached
EVOLUTION_PENDING_MAX (default 5) are skipped without an invoke. This is the most common reason for “no new suggestions”.3
Assemble the payload
Takes
EVOLUTION_RECENT_MESSAGE_LIMIT (default 40) messages from the last EVOLUTION_RECENT_SESSION_LIMIT (default 5) sessions, plus the expert’s skill list, persona / soul / agent content, and run_stats.Each skill entry’s scope is mandatory: the runtime’s inject_user_skills silently skips any item whose scope is not user or expert, leaving only a warning log.4
Internal call to agent-runtime
POST /api/agent/evolution, authenticated with X-Internal-Secret. is_internal_invoke is always true and is not caller-configurable. The timeout is EVOLUTION_TIMEOUT_SEC, default 900 seconds, enforced by asyncio.timeout.5
Run in evolution mode
The
profy-evolution plugin activates with exclusive: true — no other plugin loads. Three tools are available.6
Submit the suggestion
The model calls
evolution_log; on passing the dual ratchet it writes to expert_evolution_log in pending state. At most one per invoke.7
Creator decides
The suggestion appears in the Studio evolution panel. Acceptance runs in a transaction; ignoring only changes status.
The 900-second timeout is not arbitrary
The default matches measurement: Stage 4 evolution runs took 721–1003 seconds. 900 lets most runs finish while cutting off runaway ones. The configurable range is 30–1800 seconds.Isolation of evolution mode
This is the core of the safety boundary. Evolution runs in an exclusive internal invoke:exclusive: true is that no other plugin loads in evolution mode. This is not a prompt asking the model politely not to use other tools — those tools are simply absent from the tool list.
A single write point
evolution_log is the only tool in evolution mode that can change external state, and the only thing it can do is write a pending suggestion. It cannot modify a skill directly — that happens in a Core-side transaction after the creator clicks accept.
This structure completely separates “what the model can do” from “what actually takes effect”, with a human gate between them.
Recursion guard
Evolution mode produces no user-facing output and does not trigger further background review — otherwise one evolution would spawn the next.is_internal_invoke serves both as the activation condition and as the basis for this guard.
The dual-ratchet gate
Ten dimensions in two groups, weighted 0.70 for quality and 0.30 for efficiency. Acceptance requires both:- The weighted quality score does not decrease (after ≥ before)
- The weighted efficiency score strictly increases (after > before)
context_footprint scoring curve live on the Expert Evolution page. This section covers the design motivation.
Why not “the total must rise”
A single weighted-total gate has a structural hole: any dimension can be paid for by another. A change scoring +15 on quality and −10 on efficiency passes easily under a total-score rule — and that is precisely the most common degeneration, where the document grows thicker and more “thorough”, every run costs more, and every quality metric climbs. Splitting into groups with two conditions closes that route.Why one dimension must be computed in code
If all ten dimensions were self-scored, the gate’s evidence and the thing being gated would share a source, and it would pass whenever it wanted to.context_footprint is computed directly from the skill document’s character count (≤3,000 → 100, ≥24,000 → 0, linear between). Any value the model passes is discarded. It is the one anchor on the chain the model cannot fabricate. Its weight is only 0.08, but because the efficiency group has just three dimensions and must strictly rise, a large document expansion usually drags the whole group down enough to block the change.
The thresholds are grounded in measurement: the Stage 4 seed skill was 429 characters and evolved to 12,109; 24,000 is roughly twice that.
Both design decisions answer the same question: a gate’s evidence cannot be supplied by the thing it guards. The first prevents dimensions from excusing one another; the second prevents the model from writing its own report card. Most proposals in the current discussion of reward-hacking-resistant evaluation are missing at least one of the two.
run_stats: measured cost as ground truth
The rubric still depends on model judgement for nine of ten dimensions. To keep that judgement anchored, the evolution prompt carries a<run_stats> block listing the measured cost of up to 20 past runs: token counts, LLM turns, tool calls, the skill document length at the time, and whether the run succeeded.
The interpretation rules are mandatory:
- Tokens rose after the skill grew → the previous direction was wrong; this round must compress
- Tokens fell and success held → the direction was right; continue
- Never argue against run_stats with theory
run_stats is a strict whitelist Pydantic model with per-field bounds (total_tokens ≤ 10,000,000, tool_calls ≤ 500, skill_version_note ≤ 120 chars, and so on) because it is injected verbatim into the prompt. Without validation it would both bloat context and open a channel for smuggled instructions.
Transactional acceptance
When a creator clicks accept, Core:1
Validates structure
validateSkillImprovementContent failure returns 400.2
Validates ownership
A non-creator gets 403.
3
Locates the skill
Looks up
expert_skill by expertIdentifier, the shared-scope user id, and skill name. A miss returns 404 — the skill was deleted after the suggestion was generated.4
Three writes in one transaction
Update
skillMdContent → mark the suggestion accepted → increment expert.evolutionCount. One transaction, so a half-applied state of “skill changed but not counted” cannot occur.skill_improvement types such as knowledge_update take a status-only path with a count increment and do not write skills.
Ignoring changes only the suggestion status and never touches expert_skills.
Compared with traditional ML
The decisive difference is not effectiveness but reviewability: you can read every word that is about to take effect, and veto it before it does.
Related pages
Expert evolution (creator view)
Full rubric, threshold tables, and how to read a suggestion
Temporal memory
The memory pipeline, orthogonal to evolution
Agent Runtime
What evolution invokes run on top of
Skills
The three skill scopes: builtin / user / expert
Verified 2026-08-11. Sources:
services/agent-runtime/src/plugins/builtin/evolution/{plugin.json,tools/evolution_log.py,skills/evolution/SKILL.md,prompts/EVOLUTION.md,hooks/background_review.py}, services/agent-runtime/src/models/evolution.py, services/core/src/db/service/expert-evolution.ts, services/core/src/config/env.ts.
