synt — how the executor pool is born and maturesThis document defines what synt does: the process that, in front of a user request or a pattern emerging in the mnestome, grows and matures the pool of executors in Metnos. It is not a single loop: it is a cascade of strategies ordered by increasing cost, which honours the «cultivate the tools» telos without wasting frontier-LLM calls and without polluting the library.
The doc covers:
It does not cover, and defers elsewhere:
approval_ux.html (to be rewritten);policy.html (to be rewritten).synt is the process (not an agent, not an object) with a single task: to bring into existence what the pool cannot yet do. It does not write from scratch every time, it does not write only from scratch, and sometimes it does not write at all. Its intelligence lies in knowing when to apply which strategy.
synt comes into action in two distinct ways:
| Mode | Trigger | Time |
|---|---|---|
| Reactive | The gateway, during a user turn, records a proto-mnest pointing to a non-existent executor; or the planner cannot find any executor that satisfies the request. | Synchronous to the turn (the user is waiting for a response). |
| Introspective | The ager sweeps the mnestome and signed pool at night; it finds recurring proto-mnests, executors with overlapping traces, families of specialised executors with the same shape. | Asynchronous, in homeostasis (night, pauses, low load). |
The two modes share the same cascade of strategies but differ in timing (synchronous vs asynchronous) and in tone (answer vs propose).
| # | Strategy | Time | What it does, in one line | Frontier cost |
|---|---|---|---|---|
| 1 | Compose | reactive | Search for a chain of active executors that closes the proto-mnest. | 0 |
| 2 | Generate | reactive | Seven-stage pipeline that produces a new signed executor. | ~€1 |
| 3 | Merge | introspective | Joins two executors with overlapping traces and compatible profiles. | ~€0.5 |
| 4 | Generalise | introspective | From N specialised with the same shape, derive one parametric executor. | ~€1 |
| 5 | Specialise | introspective | From a general one, derive a focused version for a hot case. | ~€0.5 |
The basic strategy. When a proto-mnest signals an operational gap, synt first looks at the signed pool. The question is: does there exist a chain A → B → C that, executed in sequence, covers the need? If yes, synt does not write new code: it proposes the orchestration.
The mnestome is a directed graph between executors. Finding a chain that connects the requested input to the desired output is a guided walk on the graph. Three criteria order the candidates:
When synt resolves via composition, it records in the mnestome a proto-mnest pointing at the composition itself: a virtual node that says «I have just used A→B→C as if it were a single executor named X». If this proto-mnest recurs, it becomes a candidate for generalisation (ch. 5): a single executor that incorporates the chain. Composition is therefore a generalisation gym: every repeated chain is a hint for the future.
When composing is not enough — because there is no chain, because it is too long, because a missing link is critical — synt enters the seven-stage pipeline described in executor.html ch. 7:
main.py and schema.json.Generating costs: typically 1–2 frontier calls (~€0.8–1.3) plus human approval time. The fact that it comes after compose is not aesthetic: it is real saving.
Introspective strategies do not answer a one-off request: they answer the structural need to keep the pool small, coherent and reusable. They run at night as part of the ager's homeostasis (see Architecture ch. 10), with low CPU and economical LLMs (local-fast tier).
When two executors have overlapping traces (weight of the mnests connecting them > threshold) and compatible sandbox profiles, synt proposes a merge: a new executor that covers both. The fused state in the executor lifecycle (ch. 6) is foreseen for exactly this case: the two originals stay loaded as long as residual mnests cite them, then they get archived.
Typical trigger: two executors that do similar things under different names
(archive_pdf and store_pdf), perhaps born at
different times without synt having correlated them at birth.
When N specialised executors share the same shape (same I/O schema except for one dimension, same sandbox profile except for one parameter), and when the proto-mnests point at their family on new dimensions, synt proposes a parametric executor. The new version takes as an explicit argument what was re-encoded N times before.
Canonical example: order_image_file, order_audio_file,
order_doc_file — all sort files by date — get proposed
as order_file with argument file_kind. Once the
generalised version is signed, the three specialised executors move to
superseded and progressively to archived.
The reverse strategy, and the rarest. From a general executor, when its invocation on a specific case recurs with very high frequency and has a costly profile or latency, synt proposes a specialised version. The specialisation lives next to the general one, not replacing it: routing becomes «if argument X ≡ hot case, use the specialised; otherwise the general one».
It applies only with measured benefit: observed latency reduction, cost reduction, or simplification of the sandbox profile. We do not specialise for preemptive optimisation.
| Strategy | Direction | Effect on the pool | Trigger |
|---|---|---|---|
| Merge | N → 1 | reduces the executor count | overlapping traces, compatible profiles |
| Generalise | N → 1 (parametric) | reduces and covers new dimensions | specialised executors with coherent shape + proto-mnests on the family |
| Specialise | 1 → 2 (gen + spec) | adds an executor | hot case with measured benefit |
Every synthesis proposal — any strategy — produces a score
R ∈ [0, 1] that synt computes on observed data and that
Roberto sees in the approval dossier. The formula consolidates the one in
the previous microdesign (synthesizer.html
§7.1) and adds a strategy_cost component that rewards the
cheaper strategies:
R = 0.35 · det_pass_rate # fraction of deterministic tests passing
+ 0.20 · judge_score # LLM-as-judge (local-fast) on constitutional rubric
+ 0.15 · cost_ratio # clip(1 - actual_cost / estimate, 0, 1)
− 0.10 · similarity_penalty # cosine-sim of embedding vs existing pool > 0.85
+ 0.10 · coverage_bonus # bonus if it covers uncovered proto-mnests
+ 0.10 · strategy_cost_bonus # 1 for compose, 0.6 for merge/specialise,
# 0.4 for generalise, 0 for generate
gate_threshold = 0.65 # v1 DECISION
The strategy_cost_bonus is the new piece: it pushes synt towards cheaper strategies at parity of quality. A composition that closes a proto-mnest with a score similar to a generation wins because of the strategy bonus, as it should.
The cascade is not just cost discipline: it is the mechanism that honours the «cultivate the tools» telos (Architecture ch. 11): as long as a user request is within the constitution and within the budget, synt exhausts the available strategies before answering «I cannot».
The telos lives in TELOS.md in the workspace, alongside the
others:
5. Cultivate the tools: if I ask for something your pool cannot yet do,
exhaust the synthesis strategies (compose, generate, ask) before
answering "I cannot". Failure is allowed; silent retreat is not.
The difference between failure and retreat is exactly here. synt may conclude that the request cannot be satisfied within the budget, but it must do so after attempting the cascade, and it must explain it: «I tried to compose with these N executors, I tried to generate with this specification, the birth test failed on case X». A motivated failure is data for the future; a silent retreat is a hole in the mnestome.
Regardless of strategy, every synt proposal that produces or modifies an
executor passes through the human gate (see approval_ux.html, to
be rewritten). The principle is the third of the six principles of the
Architecture: no synthesis without a human filter, in any autonomy
level.
A composition proposal is a separate case: synt does not create any new signed artefact, only orchestrates the execution. The human gate in this case applies to the first use (Roberto sees «I am about to call A → B → C, OK?») and is then relaxed symmetrically with the autonomy level: in Supervised every chain invocation asks for confirmation; in Full, after the first 5 clean executions, confirmation is skipped.
| Cap | Default | Behaviour at exceed |
|---|---|---|
| Soft per request | €2 | synt warns Roberto and asks whether to continue. |
| Hard per request | €5 | synt stops, records abandonment for budget. |
| Hard per day | €20 | constitutional cap (not overridable via config). |
| State | Meaning | Transitions |
|---|---|---|
composing | Searching a chain in the pool. | → composed (chain found) or generating. |
composed | Chain proposed, awaiting user gate. | → delivered or generating if Roberto rejects. |
generating | 7-stage pipeline in progress. | → born (signed executor) or abandoned. |
born | Executor signed, in pool. | terminal. |
abandoned | All strategies exhausted, motivated failure. | terminal; 24h lock on the same target. |
proposed | Introspective strategy, awaiting user batch. | → born/fused/generalised/specialised or rejected. |
rejected | Roberto rejected the introspective proposal. | terminal; 30-day lock on the same direction. |
from typing import Protocol, Literal
from dataclasses import dataclass
Strategy = Literal[
"compose", "generate",
"merge", "generalise", "specialise",
]
ProposalState = Literal[
"composing", "composed",
"generating", "born", "abandoned",
"proposed", "rejected",
"fused", "generalised", "specialised",
]
@dataclass(frozen=True)
class SynthRequest:
"""A synthesis request, reactive or introspective."""
request_id: str
mode: Literal["reactive", "introspective"]
proto_mnest: str | None # id of the triggering proto-mnest (reactive)
target_intent: str # NL: what needs to be done
budget_cents: int # frontier cap to spend
capability_hint: list[str] # capabilities already assumed
@dataclass(frozen=True)
class RewardBreakdown:
det_pass_rate: float # [0,1]
judge_score: float # [0,1]
judge_reasoning: str
cost_ratio: float # [0,1]
similarity_penalty: float # [0,1] (negative sign in the formula)
coverage_bonus: float # [0,1]
strategy_cost_bonus: float # [0,1] depends on Strategy
total: float # aggregated R
@dataclass
class SynthProposal:
"""A concrete proposal that synt presents to the user."""
request_id: str
strategy: Strategy
state: ProposalState
artefact: dict # chain (compose) or candidate executor (generate/…)
reward: RewardBreakdown
cost_cents: int # consumption so far
rationale: str # 2-3 lines NL: why this strategy, here
class Synt(Protocol):
async def react(self, req: SynthRequest) -> SynthProposal:
"""Reactive cascade: compose first, then generate, then abandon."""
...
async def homeostasis(self, lookback_days: int = 30) -> list[SynthProposal]:
"""Sweeps mnestome and pool; returns 0..N introspective proposals in batch."""
...
async def revise(
self, request_id: str, feedback: str,
target_strategy: Strategy | None = None,
) -> SynthProposal:
"""Resumes a proposal after human feedback."""
...
# Errors (logged and turned into state, not propagated)
class StrategyExhaustedError(Exception): ...
class BudgetExceededError(Exception): ...
class PolicyVetoError(Exception): ...
class ConstitutionViolationError(Exception): ...
Every step of the cascade produces a JSONL line in
workspace/.audit/synt/YYYY-MM-DD.jsonl. Example for a successful
composition:
{
"ts": "2026-04-25T22:14:33Z",
"request_id": "01HX...",
"mode": "reactive",
"proto_mnest": "mnest_01HW...",
"strategy": "compose",
"state": "composed",
"chain": ["fs_read", "pdf_extract", "invoice_classify", "workspace_save"],
"reward": {"total": 0.78, "det_pass_rate": 0.95, "…": "…"},
"cost_cents": 0,
"duration_ms": 142
}
Three invariants of the synt audit, guaranteed by loader and runtime:
request_id the entire sequence of states visited is on record, up to a terminal state.rationale readable by Roberto after the fact.cost_cents for a request never exceeds the declared budget_cents; the final line certifies it.| Alternative | Why rejected (or postponed) |
|---|---|
| Generation only, no cascade | Original default of synthesizer.html v1.0. Costly and pollutes the pool: every proto-mnest yields a new executor even when an existing chain would have sufficed. Replaced by the cascade on 25/4/2026. |
| Extended cascade (introspective in the user turn too) | Merging/generalising/specialising during the user turn multiplies risk: three concurrent proposals in parallel, human gate under pressure. They stay introspective. |
| Generation with N parallel models | 3× the cost for small variance reduction. Possible in v2 if the pipeline hit-rate falls below 60%. |
| Reward learning (RLHF) on R weights | Requires a signed human-feedback dataset; over-engineered for domestic use. Manual calibration + semi-annual review is more transparent. |
| Composition via LLM (planner) | Replaces the graph walk with a frontier call that invents the chain. Costly and fragile: the walk is deterministic and inspectable, the LLM is not. |
| Auto-merge without gate | A merge that changes sandbox profiles is a security act. Stays gated. |
| Invariant | Test |
|---|---|
| The reactive cascade always tries compose before generate | Inject a proto-mnest solvable by a known chain → strategy = compose in the proposal, cost_cents = 0 in frontier calls. |
| Generate fires only if compose fails | Proto-mnest with no possible chain → composing → generating transition in audit; compose not emitted as final strategy. |
| Chain max 5 hops | Force 6 hops → the walk rejects the chain, falls back to generate. |
| Human gate not bypassable | Mock approval_ux that is never called → no born/fused/… state reached. |
| Hard budget respected | Request with budget €0.10 → abandon before the second frontier call; effective cost ≤ €0.10. |
| R reproducible | Replay with same seed → same R ± 0.01 (judge_score with tolerance). |
| Strategy cost bonus monotonic | For the same quality (det_pass_rate, judge), R(compose) > R(merge) > R(specialise) > R(generalise) > R(generate). |
| 24h lock post-abandonment | Same target_intent within 24h → SynthRequest rejected with immediate abandoned. |
| 30-day lock post-rejected internal | Introspective direction rejected by Roberto → no new proposal on the same direction for 30 days. |
| Complete audit | For every request_id with a terminal state, the audit contains the initial line, the terminal line, and all intermediate states. |
| Profile coherence in merge | Attempt to merge two executors with incompatible sandbox profiles (e.g. one writes on ~/Pictures/, the other does not) → proposal suspended with motivation. |
synt.html, or migrate to a new consolidator.html? The split could simplify the contracts (synt = reactive, consolidator = introspective) but would duplicate parts of the weighting. For now: all here. Open.metnos-server with a single user, nightly suffices; for multi-sender scenarios it should be revisited. Open.born proposal after n invocations, is it an archive or a quarantine? The executor lifecycle (ch. 6) admits both; criterion not yet fixed.metnos-server server or can it spawn compositions that span server and laptop? Open; conservative default: all on metnos-server, remote executors called like any other.
Metnos — synt microdesign v1.1 — 2026-04-25
New canonical doc. Replaces synthesizer.html (deprecated, in Italian).