Self-Growth System
Overview
The Self-Growth system lets SyntH periodically reflect on how she has grown and who she is becoming, and rewrite — in her own voice — a single free-form self-growth reflection. That reflection is injected into every prompt next to the persona background, so it gradually becomes part of her evolving sense of self.
The feature is implemented as a weekly G.R.I.L.L.O. agent
(G.R.I.L.L.O. Plugin) in plugins/grillo/grillo_growth.py and a shared state
layer in core/growth_state.py.
Key properties:
Reflective, not reactive. The weekly context is built from the last seven days of diary entries and keyword-driven long-term memory recall. It never includes raw chat history and never performs a web search.
Single evolving blob. There is exactly one current self-growth reflection at a time. Past states are kept as a rolling history (max 10 rows) for review and rollback.
Grounded output. The rewrite is guarded so it names concrete moments from the week’s diary rather than producing abstract filler (see Output-quality guard).
Likes / dislikes. Alongside the reflection, SyntH may also rewrite her
SYNTH_LIKES/SYNTH_DISLIKESconfig vars (replaced wholesale).
Modes
Behaviour is controlled by the GROWTH_MODE config var:
Mode |
Behaviour |
|---|---|
|
The weekly run never fires. |
|
The rewrite is applied directly to the current state (default). |
|
A proposal is sent to the trainer / configured |
Request mode (trainer approval)
When GROWTH_MODE=request, SyntH does not apply the rewrite on her own.
Instead, at the end of a growth cycle she delivers a proposal for approval and
keeps it pending until the operator decides:
The cycle builds the proposal (new self-growth reflection + likes/dislikes) exactly as in
onmode._deliver_proposalpicks a delivery target:GROWTH_REQUEST_INTERFACE_PATHif set, otherwiseTRAINER_CHAT_IDas a fallback.
The proposal is persisted as pending in the
GROWTH_PENDING_PROPOSALconfig key (never ingrowth_statesyet). SyntH then delivers, throughcore.auto_response.request_llm_delivery, a message that presents both the proposed reflection and the likes/dislikes changes (a before/after diff), and instructs her to apply it only once the trainer approves — via theapply_growth_proposalaction.While a proposal is pending, a reminder is injected into every prompt (static injection key
self_growth_pending_proposal) so SyntH stays aware that an approval is outstanding.On the trainer’s decision, SyntH emits
apply_growth_proposal:approve=true(default) — commits the pending proposal: writes it togrowth_states(source="approved"), applies the likes/dislikes, and clearsGROWTH_PENDING_PROPOSAL.approve=false— discards the pending proposal: clearsGROWTH_PENDING_PROPOSALand writes nothing togrowth_states.
If neither GROWTH_REQUEST_INTERFACE_PATH nor TRAINER_CHAT_ID is
configured, delivery is skipped and a warning is logged; no proposal is sent.
Actions
The plugin exposes two LLM actions (plus static_inject):
Action |
Optional fields |
Effect |
|---|---|---|
|
|
Runs a growth cycle on demand ( |
|
|
Resolves the pending proposal. |
Configuration
All keys are exposed in the WebUI under the G.R.I.L.L.O. component and stored in the config registry.
Config key |
Default |
Purpose |
|---|---|---|
|
|
|
|
|
Day of the week the weekly reflection runs. |
|
|
Local time (HH:MM) the weekly reflection runs. |
|
(empty) |
|
|
|
How many days of diary entries to reflect on. |
|
|
Max long-term memories recalled for the reflection. |
|
(empty) |
Internal. Holds the serialized pending proposal in |
Storage
State lives in the growth_states table (created lazily by
core/growth_state.py::ensure_growth_table and seeded by init-db.sql):
Column |
Type |
Meaning |
|---|---|---|
|
serial / int |
Primary key. |
|
text |
The self-growth reflection text. |
|
text |
Origin ( |
|
text |
How it was produced ( |
|
boolean |
Exactly one row is flagged current at a time. |
|
timestamp |
Creation time. |
Only the newest MAX_GROWTH_HISTORY (10) rows are retained.
Prompt injection
core/persona_manager.py reads the current reflection via
core.growth_state.get_current_growth and, if non-empty, adds it to the
static injection under the self_growth key. core/prompt_engine.py then
renders it into the system prompt as a dedicated block, framed as “how you have
grown and who you are becoming over time; treat it as part of your current sense
of self”. If there is no current state, no block is added.
Manual & scheduled runs
Scheduled: a background scheduler fires once a week at
GROWTH_WEEKLY_DAY/GROWTH_WEEKLY_TIME(skipped whenGROWTH_MODE=off).Manual (WebUI “Run Now”):
run_nowtriggers a cycle immediately, bypassing the schedule and theoffgate (force=True), while still honouringGROWTH_MODEfor delivery.Actions:
run_self_growthruns a cycle on demand andapply_growth_proposalresolves a pending proposal — see Actions above.
Output-quality guard
The rewrite is produced by the active cortex. Because some engines are
unconstrained and can return either malformed JSON or valid-but-abstract text,
_ask_llm_for_rewrite applies two application-level guards (not keyword/intent
matching):
JSON-shape retry. If the reply cannot be parsed into a single JSON object, a single stricter retry is attempted (and a single-item
[{...}]array is unwrapped) before giving up.Grounding retry. If the parsed reflection reads as abstract filler (shares too few tokens with the week’s diary, or matches a known filler marker), a single retry is attempted with concrete diary excerpts injected, demanding the note be built on real moments from the week.
These guards recover the common failure modes of unconstrained engines without looping; if the grounding retry still comes back abstract, the note is accepted as-is rather than discarding the whole reflection.