klbr v2: Rust mechanisms, Python policies and lifecycle hooks #
Date: September 6, 2026
Status: proposed implementation contract; no replacement runtime or hook tests have been implemented or run.
Scope: amendment to the supplied implementation plan following the operator's clarification that Rust is an intentional reliability/performance foundation and Python hooks must expose core behavioral decisions.
Decision #
Retain Rust as the permanent owner of durable state, transport, resource accounting, lifecycle transitions, cancellation and recovery. Make ordinary policy extensible through typed Python hooks from the first runtime slice. Python should change not only the tools available to the model but how the harness prepares context, chooses work, selects model profiles, handles outcomes and participates socially.
This is a mechanism/policy split, not a claim that language choice proves reliability. Safe Rust excludes data races, but not arbitrary race conditions, deadlocks or incorrect business logic [H1]. Its ownership/resource model and lack of a mandatory tracing garbage collector make it a reasonable deliberate choice for the long-running host [H2]. No Rust-versus-Python benchmark of this harness has been performed. Neither HTTP/model latency nor external services become faster merely by moving orchestration into Rust.
Hooks cover meaningful lifecycle boundaries, not every private function. A documented hook is an extension contract with a specific input, result and failure policy. Internal allocation, storage mechanics and transport framing stay refactorable. The goal is that a new behavior composes existing domain operations, not that Python receives an untyped mutable reference to the entire agent.
1. Three hook contracts #
Decision slots #
A decision slot selects a plan before a state transition or operation. Exactly one selected top-level handler owns a slot in each scope/revision. Examples: attention selection, model request planning, continuation versus waiting, and effect admission.
The handler receives an immutable snapshot plus bounded read capabilities and returns a slot-specific closed result. It does not mutate host state directly. Depending on the slot, results include a validated plan, a delay with a persisted deadline, rejection, a request for explicitly scheduled follow-up work, or use of the slot's documented default. There is no universal truthy/falsey return convention.
Composition is ordinary Python: a selected handler can call helpers or an editable default implementation. Do not make implicit import order decide which competing policy wins. Reject conflicting top-level registrations at activation.
Ordered transforms #
A transform contributes a typed patch to a representation or proposal: context additions, a bounded display preview, a summarization request, or an outgoing message draft. Its patch names the base version it expects. Multiple transforms run in an explicit manifest order; invalid ordering or conflicts are diagnosed at publication or application.
Do not expose arbitrary JSON Patch over the database. Each domain defines legal modifications. Context contributions cannot alter archival originals, forge a receipt, reclassify Discord text as an operator instruction, or silently remove required provider protocol blocks. Outgoing-text transforms do not change the original scratchpad or a historical delivery record. Store the final requested payload and transformation provenance.
Post-commit observers #
Observers consume committed facts: an input was admitted, a cell completed, an effect was confirmed, a child finished, or a revision activated. They do not veto or retroactively change the event. Several independent observers may subscribe.
A durable observer returns bounded namespaced state updates and proposed commands. Rust validates and commits those together with that observer delivery's completion. Important effects use the existing outbox/job operations rather than an untracked network call from the callback. Long work is scheduled as a job; the observer is not an unbounded secondary agent loop.
Interactive event subscriptions are a separate convenience: the workbench can inspect or react to events while alive. They are best-effort, may disappear on restart, and are not used for core progress or durable obligations.
2. Execution placement: accessible from the kernel, independent of its availability #
Use the existing Python runtime package in two process roles:
- Workbench: the model's persistent namespace, foreground cells and explicitly ephemeral background tasks.
- Hook worker: a supervised persistent process that imports published handler entrypoints and answers bounded invocations. It has no model loop, source transport, scheduler or independently authoritative database.
One long-lived worker per needed compatible hook revision is the starting implementation; do not spawn a process per callback or eagerly create one per hook. Cap retained workers, lazily start a worker for a pinned revision when needed, and evict idle processes without removing their durable registrations. Current ingress policy and older in-flight work may temporarily require different revisions. Workers share the runner's framing, schema checks, diagnostics and supervisor rather than a new service stack.
This extra process has a concrete reason. A foreground cell may await a host send; the host may need to run a send-review hook. Queuing that review as another foreground cell in the waiting workbench creates a circular wait. A CPU-blocking cell can also prevent another asyncio task on its event loop from running [H3]. An independent hook worker breaks that dependency, but the host must also continue serving allowed read RPC while awaiting its answer and must not hold a session lock or SQL transaction across the invocation.
The hook worker has bounded request queues and deadlines. Short decision work has priority over observer backlog. Observers that need extended processing schedule a job. One process initially means one blocked hook can delay other hooks until its host deadline; that is explicit, not a claim of per-handler isolation. On an unresponsive worker, invalidate its generation, reject late results, kill/restart it, and resolve affected calls using their individual failure contracts. Split worker pools only if measured queueing or isolation requirements warrant it.
Workbench-only hooks such as kernel.initialize can populate globals during bootstrap, before the kernel becomes ready. Their failure affects readiness and follows the candidate/restart policy. They are not a backdoor to run a required ingress or delivery hook inside a busy cell.
3. The first public hook surface #
These names are proposed contract names, not existing APIs. Add a domain's hooks in the same slice that implements that domain. Do not build an empty implementation for every planned slot up front.
| Domain | Proposed extension points | Python can decide or contribute | Rust retains |
|---|---|---|---|
| Accepted input | input.accepted observer; attention.plan decision |
Tag/enrich projections, wake or batch ambient work, prioritize directed work, explicit defer/ignore dispositions | Admission before notification; immutable author/source classification; dedupe; canonical routing; no silent deletion |
| Turn lifecycle | turn.prepare transform; turn.next decision; turn.completed observer |
Task-specific guidance; continue/wait/propose a routine; summarize a result for later processing | Session state machine, durable wait/wakeup, operator preemption, no-progress ceilings and spending limits |
| Context/history | context.prepare transforms; history.rank optional ranking slot |
Relevant project instructions and scoped evidence, supplementary retrieval priorities, bounded display choices | Access scope; raw-history preservation; core chronological coverage; provider-valid sequences; final input budget |
| Model calls | model.plan decision; model.completed / model.failed observers |
Select an allowed model profile, purpose-specific parameters, bounded fallback/backoff plan | Credentials, actual provider invocation/serialization, accounting, configured limits and cancellation |
| Cell execution | execution.review decision; execution.completed observer; result.present transform |
Additional gate, explicit source rewrite with before/after provenance, outcome follow-up, model-visible preview | Execution identity, original code/result retention, generation checks, output caps, cancellation and supervision |
| Explicit delivery | delivery.prepare transforms; delivery.review decision; delivery.completed observer |
Formatting, smart-reply policy, delay, suppress or reject a proposed send; explicit next actions after a receipt | Original effect identity; allowed destination; current anchor validation; transport/rate limits; receipt-derived outcome |
| LCM | compaction.plan decision; summary.prepare transform; compaction.completed observer |
Early compaction trigger, choose among eligible ranges, adjust summarizer profile/prompt, schedule optional later analysis | Forced pressure relief, source relationships, legal contiguous ranges, bounded retries, atomic context swap |
| Jobs and children | job.plan decision; job.completed / job.failed observers |
Child profiles, suggested budgets, workspace presets, result handling, bounded retry proposals | Actual admission, shared resource ceilings, revision pinning, worktree ownership and durable outcomes |
| Runtime/revisions | runtime.started, kernel.reset, revision.activated observers; workbench kernel.initialize |
Initialization helpers, cleanup proposals, diagnostics, targeted reconstruction | Recovery boot, operator controls, last-good selection, candidate validation and process termination |
A lifecycle observer may run well after the fact; its event time and handling time are distinct. A completion observer must not be misrepresented as part of the operation's atomic commit. Proposed side effects can be rejected after validation without erasing the observed fact.
Do not add mandatory per-token Python round trips. Model transport and token animation remain Rust-owned; an optional stream subscription is batched and explicitly best-effort. Transform completed or bounded chunks outside the token transport loop. A persistent filter requiring additional latency would be a separately documented mode, not an invisible default.
4. Ship editable default policies #
Make existing compatible behavior the initial Python policy library, not a large inaccessible set of Rust decisions surrounded by notification callbacks. Good examples are Discord ambient batching, useful context additions, model-profile selection, smart reply preferences and deciding when an autonomous turn yields.
Default ordinary policies live in versioned Python source. Project/custom handlers can call them or replace their slot. The compatibility fixtures remain the authority on the intended initial experience, including explicit speech and ambient silence.
Rust keeps only the minimal unavailable-policy fallback and non-negotiable constraints. Do not maintain a second Rust implementation of every Python policy. A missing optional enrichment can mean no extra context. A missing ordinary attention policy can leave work pending or use the documented basic directed/ambient schedule. A missing required delivery guard means the send is held, not permitted by pretending the guard approved it.
What can change without Rust: batching rules, attention ranking within limits, model profile rules, task-specific context, result rendering, extra execution restrictions, reply preparation, child defaults and follow-up routines. What still needs a core change: a new durable primitive, new protocol authority, different archival invariants, or a transport capability absent from both current host services and the supported Python source/job interface.
5. Public Python experience #
The workbench gets discovery, dry-run and editing APIs. Durable registrations are module entrypoints inside an immutable behavior revision, never pickled closures or references to a cell's globals. A temporary callback is useful for experiments but has a visibly different lifetime.
Illustrative handler source, using the proposed SDK:
# klbr_hooks/social.py
from klbr.hooks import AttentionDecision, AttentionRequest, HookContext
from klbr_hooks.defaults import attention as default_attention
async def attention(req: AttentionRequest, ctx: HookContext) -> AttentionDecision:
# Wake means become eligible at a legal scheduling boundary, not cancel
# the current cell or force the agent to send a response.
if req.event.kind in {"discord.dm", "discord.mention"}:
return AttentionDecision.wake(priority="directed")
return await default_attention(req, ctx)
Illustrative workbench workflow:
from klbr import behavior, hooks
spec = await hooks.describe("attention.plan")
draft = await behavior.draft() # records its base activation/revision
# The handler file and its tests are edited in draft.path using normal Python.
await hooks.bind(
draft=draft.id,
slot="attention.plan",
handler="klbr_hooks.social:attention",
)
report = await hooks.test(draft=draft.id, fixtures="social_attention")
if not report.passed:
raise RuntimeError(report.summary)
# Publishing also runs host-owned compatibility/smoke validation; this
# Python branch is not the authority on whether the candidate is valid.
revision = await behavior.publish(draft.id)
await behavior.activate(revision.id)
hooks.describe reports input/result schemas, examples, phase, execution placement, ordering, fallback, deadline, supported SDK reads and required scope. hooks.list shows selected handlers and revision identities. hooks.trace explains a concrete invocation using source references, returned decision, validation, elapsed time and fallback. hooks.test runs in an isolated candidate with captured inputs and fake effects; it does not resend historical messages.
Python type hints are ergonomics, not a trust boundary. Validate wire input and returned variants in Rust. Centralize protocol schemas/fixtures, derive useful Python types and docs, and check compatibility. Use normal Python helpers for local composition, not another workflow language or generic hook-to-hook graph.
6. Invocation, commit and state contracts #
A host invocation records an ID, slot/schema version, logical handler ID, hook revision, scope/session, causative event or operation, source version, worker generation and deadline. Inputs are bounded snapshots or scoped artifact/history references. The host records the accepted decision or fallback needed to explain/recover an actual operation. Avoid duplicating entire prompts in operational logs.
The ordering for a decision is:
- Prepare the immutable decision input and record its operation identity.
- Release database transactions and domain mutation locks.
- Invoke the pinned worker with a host-owned deadline. Serve only the bounded reads permitted to that slot.
- Validate schema, scope, budget and current source version. Discard an answer from an expired invocation or stale worker. Reject stale changes, or resnapshot/retry within a separate bounded limit.
- Commit the chosen plan and its provenance with the appropriate domain transition. Perform external work afterward through the existing outbox/job model.
A read-dependent decision identifies relevant read versions. Commit must validate the versions that affect its correctness, not just the first input payload. Time is supplied as snapshot data when deterministic testing matters; an operation requiring fresh time is revalidated at the host.
Decision/transform SDKs are read-only apart from returning their proposal. They cannot recursively invoke the operation currently being gated. Model inference, network fetches and expensive analysis should be deferred to tracked jobs when not part of the declared bounded read contract. Do not let a model-planning hook call the same model-planning path recursively. Causation IDs, bounded chain depth and command-rate budgets prevent accidental feedback loops from consuming unlimited resources.
Durable observers are delivered at least once until success or an explicit paused/dead-letter disposition. Use per-observer delivery identities and checkpoints, with ordering stated per partition; there is no implicit total-order promise across independent sessions. A failed observer must not block unrelated subscriptions or ingress. A paused/dead-letter record is visible and retains the event for explicit recovery; it is not silent successful delivery.
For each observer delivery, atomically commit its validated namespaced state writes, proposed command records and acknowledgement. A crash after that commit does not enqueue the commands again. A crash before it permits retry. Stable command keys derive from logical observer ID, causative event ID and action key, not an attempt number. A handler revision alone does not make old events new work. Intentional historical reprocessing requires an explicit replay identity and effect policy; dry-run is the default.
Namespaced state has a schema version, owner and optimistic version checks. It holds small hook-owned data, not a duplicate authoritative inbox, context or receipt ledger. Migrations run at activation boundaries; use backward-compatible state while old revisions remain pinned, or pause/drain affected consumers before a breaking migration. Do not promise automatic rollback of arbitrary state transformations.
Ordinary Python can bypass an SDK in a trusted same-user process. Read-only callback conventions and declared commands do not prove purity or security. Strong enforcement requires OS isolation. Arbitrary filesystem/network effects made directly by a handler remain outside the atomic observer commit and may have unknown outcomes after failure.
7. Failure policies must be explicit per slot #
| Failure | Required behavior |
|---|---|
| Optional context addition fails or times out | Record degradation; continue with a provider-valid, budgeted base context. Do not fabricate the missing evidence. |
| Required execution/delivery review is unavailable | Hold the specific operation with an explicit reason. Do not convert failure into approval. Allow operator repair through the independent management path. |
| Attention policy fails | Keep accepted work; use its configured basic fallback or pause. Operator preemption and receipt/storage continue regardless. |
| Model profile is unsupported or over budget | Reject the plan; use an explicitly configured permitted fallback or pause. No silent switch to an arbitrary costly model. |
| LCM policy refuses compaction above a hard budget | Apply the host's bounded pressure-relief path. A hook cannot disable convergence or mutate sources. |
| Observer fails repeatedly | Back off, then pause/dead-letter that subscription with retained evidence. Do not poison the input journal or recursively notify itself forever. |
| Worker is CPU-bound, blocked or exits | Enforce deadline from Rust; invalidate generation; terminate/restart when necessary. A cooperative Python timeout is not the only protection. |
| Candidate hooks fail validation/startup | Keep old activation; surface diagnostics. Recovery does not require running broken candidate hooks. |
Separate a hook's stated preference from authority. An extension cannot give itself new credentials, lift its own hard spending ceiling, alter another principal's scope, or override operator recovery merely by returning a plan. These are validated host/administrative contracts; the deployment's OS trust model still matters.
8. Activation without unnecessary kernel resets #
Extend the existing single authoritative behavior activation record to reference component revisions: instructions, settings, skills/runtime environment, and hooks. This is one versioned activation manifest, not multiple mutable sources of truth.
A hook-only source edit with unchanged compatible skill/environment dependencies prepares a candidate hook worker and changes future hook invocations at a safe boundary. It need not discard the workbench's globals. Skill/environment edits still use a fresh workbench where required. If a shared imported module or dependency changes, explicitly replace all affected process roles; do not call it hook-only just because the edited file lives in a hook directory. Keep hook and workbench entrypoint dependencies separable and record their content/environment fingerprints.
Pin one behavior epoch to a model attempt and its effects. A mid-cell activation does not cause the latter half of that cell's send path to use a new mandatory guard unexpectedly. Global ingress decisions pin the ingress epoch selected for their decision after admission; children can remain on an older authorized epoch. Explicit emergency changes cancel or invalidate in-flight work rather than silently mutating its contract.
Observer activation records a cutover cursor per ordered subscription partition. Existing pending deliveries retain their assigned handler revision; later deliveries use the new one. A new subscription begins at its declared start cursor, normally activation, rather than secretly replaying all history. Old revisions remain available until their pinned work completes, is cancelled, or is explicitly migrated.
Boot recovery, last-good activation, operator disable/rollback and worker termination do not call mandatory user hooks. Hooks can observe those transitions afterward; a broken extension must not veto its own repair. Use fresh module processes for published revisions, not universal importlib.reload, whose documented reference/instance behavior does not provide an atomic old-to-new switch [H4].
9. Implementation changes to the original slices #
Slice 0: add characterization fixtures for attention, model/context preparation, explicit-send distinctions and hook failure outcomes. Preserve behavior, not incidental Rust placement.
Slice 2: add hooks/spec, invocation/result variants, epoch identity, declared capabilities and failure policies alongside the kernel/client contracts. Add hook-decision and observer-state schema in the relevant store migrations. Domain state machines must await external decisions without holding their authoritative lock/transaction.
Slice 3a, before the replacement turn loop: build hook-worker mode using the same Python runtime transport/supervisor, importable entrypoints, a bounded dispatcher and fake-host tests. Bootstrap static published hook manifests before exposing live editing. Verify a blocked workbench cannot block a hook and a blocked hook cannot block durable ingress or operator management.
Slice 4: wire turn, context/model and execution hooks into the actual replacement loop. Ship minimal editable default policies; do not implement a full inaccessible Rust policy and postpone extensibility indefinitely.
Slices 6–8: add Discord delivery/attention, LCM and result-presentation hooks with their owner domains. Every slot must document what can change and what remains host-owned. Put contextual code intelligence behind Python skills, not extra core hooks for parser internals.
Slice 9: extend candidate validation, discovery, dry-run, observer-state compatibility, component activation, revision pinning and rollback. Prove a hook-only edit leaves a compatible workbench running.
Slices 10–11: integrate durable observer commands with jobs and children. Enforce recursive-work budgets and explicit completion ordering. Hooks request these operations; they do not maintain another scheduler.
Slice 12: stress the combined system under blocked cells/hooks, input bursts, repeated reloads and reconnects. Record no-hook versus default-hook latency and memory before choosing process counts or claiming a performance result. Include observer backlog and pinned revision garbage collection in diagnostics/cutover.
10. Decisive acceptance tests #
- No circular wait: a workbench cell awaits a send; Rust invokes its required review in the independent hook worker; the decision and receipt return without a second foreground cell.
- Host remains available: block the workbench, then block the hook worker in a separate test. Incoming events are still persisted; cancellation/rollback commands remain serviceable; affected operations use the stated fallback or hold outcome.
- Typed boundaries: wrong variants, overlarge patches, forged sources, forbidden destinations, over-budget model plans and expired worker replies are rejected without partial domain mutation.
- Known failure, not accidental approval: a broken required send gate holds delivery; a broken optional context hook yields an explicit degradation and a valid base request.
- Observer crash windows: kill before/after committing observer acknowledgement/state/commands. Host commands are not duplicated locally, while genuinely uncertain external delivery remains represented as uncertain.
- Stable activation: edit a hook while a cell is running; that cell remains pinned, future work adopts the new revision, and a hook-only compatible change preserves the workbench. A bad candidate never becomes active.
- No phantom replay: update an observer's source, restart, and confirm already completed event deliveries are not repeated because a revision changed. Explicit replay is separately identified and dry-run by default.
- No self-amplification: an observer reacting to a completed job cannot recursively create unbounded jobs or hook-failure notifications. Causation filters and hard budgets end the cycle visibly.
- Behavior coverage without Rust edits: change ambient batching, model-profile selection, project context, outgoing formatting, an extra execution check and child-result handling entirely in Python. Core invariant/compatibility tests continue to pass.
- Measured overhead: compare no hooks, shipped defaults and intentionally slow hooks using latency distributions, idle CPU/RSS, per-turn serialization size, worker queue delay, restart counts and root responsiveness. Targets are selected from measured deployment behavior, not an invented throughput claim.
Sources and evidence limits #
The behavior compatibility facts and original source identities remain in behavior-contracts.md. The prior plan was read directly; this amendment changes its earlier recommendation to defer a policy boundary. No implementation claim is based on a newly tested klbr runtime.
H1 — Rustonomicon, data races versus general race conditions: https://doc.rust-lang.org/nomicon/races.html
H2 — Rust project, ownership/reliability and performance goals: https://rust-lang.org/
H3 — Python asyncio development documentation, scheduling and blocking code: https://docs.python.org/3/library/asyncio-dev.html
H4 — Python importlib documentation, reload semantics: https://docs.python.org/3/library/importlib.html
The supplied Lucid code skill and semantic-compression guide inform explicit ownership, legal state/result types and the public extension vocabulary. jyn's essay informs keeping policy independent from mechanism rather than optimizing only for file or line count. Ideonomy material was read as conceptual background, not installed or executed as a plugin and not used as evidence that the proposed design is correct.