Skip to content

Plugin-style before_prompt_build hook for system context injection #3326

Description

@houko

Problem

LibreFang has no extension point in the system-prompt build path. Every piece of context that should be injected before the model sees the user turn — live memory recall, tool-usage guidance for newly enabled hands, dynamic state from runtime plugins (e.g. "phone-control is currently armed until 14:32") — has to be wired in by editing prompt_builder.rs directly.

This forces every cross-cutting feature to land as a kernel patch instead of as a hand / skill / extension. It also blocks the active-memory pattern (a memory sub-agent that runs before every eligible reply and injects its findings into system context), because the only way to invoke a memory agent today is for the main agent to know it should and call agent_send itself — which pollutes the system prompt and leaks the architecture to the LLM.

Reference: openclaw

OpenClaw plugins implement a before_prompt_build hook that calls prependSystemContext(...). Examples:

  • extensions/active-memory: runs a bounded blocking memory sub-agent and injects its result so the main agent never has to know memory exists.
  • extensions/diffs: prepends stable tool-usage guidance for the diffs tool every turn, kept out of user-prompt space.
  • extensions/phone-control: injects current arm/disarm state so the agent knows which high-risk tools are gated.

The hook contract is: "you get the prompt context as it is being built, you may prepend a labeled section, you may not mutate user / history / tool-result content."

Proposed approach

  1. Define trait PromptContextProvider in librefang-runtime:

    #[async_trait]
    pub trait PromptContextProvider: Send + Sync {
        fn name(&self) -> &str;
        async fn before_prompt_build(
            &self,
            ctx: &PromptBuildContext<'_>,
        ) -> Option<PrependedSection>;
    }

    PromptBuildContext exposes read-only agent_id, session_id, last_user_message, chat_type, recent_history_summary. Returning None is a no-op.

  2. Register providers on the kernel via KernelHandle::register_prompt_provider(Arc<dyn PromptContextProvider>). Section 15 (Live Context) in prompt_builder.rs already proves the slot is safe — extend it to fan out across all registered providers, in registration order.

  3. Each provider's output goes into its own section (## Live Memory, ## Diffs Guidance, …) with the provider name as the header so the LLM (and a debugging human) can attribute content.

  4. Hard caps: per-provider 8 KB, total injected 32 KB, 1 s wall budget for the whole fan-out. Time-out / over-budget providers drop with a WARN log, never block the turn.

  5. Hands and skills can register providers at activation; the kernel deregisters them at deactivation. No new TOML schema needed initially — programmatic registration only.

Acceptance criteria

  • PromptContextProvider trait + registration API on KernelHandle.
  • prompt_builder.rs calls registered providers concurrently with the 1 s deadline; results merge into the existing Section 15 area as labeled subsections.
  • Unit test: two providers register, both contribute, output appears in deterministic order with headers.
  • Unit test: a slow provider exceeds the deadline and is dropped without affecting the other provider's output.
  • Integration test: spawn an agent with a fake provider that injects "BTCUSD: 67000"; send a message; verify the model's recorded request contains the injected section.
  • Doc note in docs/architecture/ describing the hook contract and the budget caps.

This is the prerequisite for any future active-memory, diffs, or runtime-state injection feature; do not implement those until this lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/kernelCore kernel (scheduling, RBAC, workflows)area/runtimeAgent loop, LLM drivers, WASM sandboxdifficulty/mediumHalf day, some familiarity with the crateenhancementNew feature or requestfeature-parityPorted from OpenFanghas-prA pull request has been linked to this issueneeds-rfcRequires an RFC before implementationseverity/highSignificant functional, security, or performance impact

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions