Skip to content

feat(context-engine): add preassemble() hook for per-turn rewrite - #24949

Closed
100yenadmin wants to merge 1 commit into
NousResearch:mainfrom
100yenadmin:feat/context-engine-preassemble
Closed

feat(context-engine): add preassemble() hook for per-turn rewrite#24949
100yenadmin wants to merge 1 commit into
NousResearch:mainfrom
100yenadmin:feat/context-engine-preassemble

Conversation

@100yenadmin

Copy link
Copy Markdown

Summary

Adds an additive, non-breaking ABC method ContextEngine.preassemble(messages, budget_tokens=None) -> list[Message] (default no-op) with a single call site in run_agent.py right after sanitization and before the retry loop. Lets pluggable context engines (LCM-style, etc.) substitute the message list every turn — not just on overflow — without breaking session lineage or rotating the SQLite session ID.

Diff: +110 / 0, 3 files. 22/22 tests pass.

Why

Today ContextEngine.compress() only fires when should_compress() returns True (overflow). Plugin engines that maintain a lossless conversation pyramid (raw + summaries DAG) need to substitute evicted raw turns with summary stubs on every prompt build to keep the prompt under budget while preserving long-range recall — not just at the overflow boundary.

The existing pre_llm_call hook can't do this: it concatenates plugin output into the current user message and never rewrites the message list (intentional, to preserve the prompt cache prefix). The new preassemble() hook gives engines explicit, structured per-turn rewrite capability while keeping pre_llm_call semantics unchanged.

This is independent of and complementary to #22929 (Wire `on_pre_compress` into the context compression pipeline): `on_pre_compress` would fire at compress-time; `preassemble` fires at every API call.

What

  • `agent/context_engine.py`: add `preassemble()` method to the ABC. Default returns `messages` unchanged.
  • `run_agent.py`: call `self.context_compressor.preassemble(api_messages, budget_tokens=threshold_tokens)` after surrogate sanitization and before the retry loop. Exception-safe — failures log a warning and fall back to unmodified messages. Fires once per turn at the API-call boundary (not inside the retry loop).
  • `tests/agent/test_context_engine.py`: 3 new tests covering default no-op behavior (with and without `budget_tokens`) and a subclass-override case where `preassemble` substitutes a stub for middle turns while preserving the system message at index 0 and the final user message.

API design

```python
def preassemble(
self,
messages: List[Dict[str, Any]],
budget_tokens: int = None,
) -> List[Dict[str, Any]]:
"""Per-turn rewrite hook called immediately before each model API request.

Engines may return a substituted message list (e.g., replace evicted
raw turns with summary stubs while preserving the assistant/tool-call
pairing invariant). Default is a no-op: return ``messages`` unchanged.
...
\"\"\"
return messages

```

System message at index 0 (if present) is documented as the engine's responsibility to preserve.

Impact

  • Additive only. Default no-op. `ContextCompressor` (the built-in engine) keeps the no-op default; zero behavior change for non-plugin users.
  • Test suite: 22/22 pass (3 new + 19 existing) on `tests/agent/test_context_engine.py`.
  • Blast radius (independent verification): GitNexus code-graph impact analysis on `ContextEngine` shows LOW risk; 4 direct callers (`ContextCompressor`, `hermes_cli/plugins.py`, `agent/context_compressor.py`, `plugins/context_engine/init.py`); 0 affected processes.
  • Exception-safety: a misbehaving plugin engine's `preassemble` can't break the agent loop — failures are logged and the original `api_messages` is sent unchanged.
  • Retry loop: hook fires once per turn, BEFORE the retry loop. Retries on transient API errors reuse the substituted messages (intended — we don't want to re-substitute on every retry, that would invalidate the prompt cache).

Context

Drafted as part of electricsheephq/lossless-hermes — a community port of Lossless Claw to Python/Hermes. Full design rationale and the ADR-documented fallback (if this isn't merged) live at docs/adr/010-always-on-assembly.md. The fallback is workable but breaks session lineage (forces `should_compress=True` every turn → SQLite session ID rotation → memory-provider lineage warnings). This patch eliminates that compromise.

The docstring at `agent/context_engine.py:5` already names LCM as a planned tenant of the plugin slot, so this lands on the surface the ABC was designed to support.

Test plan

  • `pytest tests/agent/test_context_engine.py -x -q` → 22 passed
  • Hermes maintainer review (this PR)
  • Smoke test in CI on the upstream matrix

Reversibility

100% reversible: revert the 3-file commit. Zero schema changes, zero external contract changes, zero new dependencies.

Adds an additive, non-breaking ABC method `ContextEngine.preassemble(
messages, budget_tokens=None) -> list[Message]` (default no-op) with a single
call site in `run_agent.py` right after sanitization and before the retry
loop. Lets pluggable context engines (LCM, etc.) substitute the message
list every turn — not just on overflow — without breaking session lineage
or rotating the SQLite session ID.

## Why

Today `ContextEngine.compress()` only fires when `should_compress()` returns
True (overflow). Plugin engines that maintain a lossless conversation
pyramid (raw + summaries DAG) need to substitute evicted raw turns with
summary stubs on EVERY prompt build to keep the prompt under budget while
preserving long-range recall — not just at the overflow boundary.

The `pre_llm_call` hook can't do this: it concatenates plugin output into
the current user message and never rewrites the message list (intentional,
to preserve prompt cache prefix). The new `preassemble()` hook gives
engines explicit, structured per-turn rewrite capability while keeping
`pre_llm_call` semantics unchanged.

## What

- `agent/context_engine.py`: add `preassemble()` method to the ABC.
  Default returns `messages` unchanged.
- `run_agent.py`: call `self.context_compressor.preassemble(api_messages,
  budget_tokens=threshold_tokens)` after surrogate sanitization and before
  the retry loop. Exception-safe — failures log a warning and fall back to
  unmodified messages.
- `tests/agent/test_context_engine.py`: add 3 tests covering default no-op
  behavior (with and without `budget_tokens`) and a subclass-override case
  where preassemble substitutes a stub for middle turns while preserving
  the system message at index 0 and the final user message.

## Impact

- **Additive only.** `ContextCompressor` (the default engine) keeps the
  no-op default; zero behavior change for non-plugin users.
- **Test suite**: 22/22 pass (3 new + 19 existing).
- **Blast radius**: GitNexus impact analysis on `ContextEngine` shows LOW
  risk; 4 direct callers (ContextCompressor, hermes_cli/plugins.py,
  agent/context_compressor.py, plugins/context_engine/__init__.py); 0
  affected processes.

## Context

Drafted as part of [electricsheephq/lossless-hermes](https://github.com/electricsheephq/lossless-hermes)
— a community port of Lossless Claw to Python/Hermes. Related upstream
work: NousResearch#22929 (Wire `on_pre_compress` into the context compression
pipeline). This patch is independent of and complementary to that issue:
`on_pre_compress` would fire at compress-time, `preassemble` fires at
every API call.

Fallback path documented if this isn't merged:
https://github.com/electricsheephq/lossless-hermes/blob/main/docs/adr/010-always-on-assembly.md
100yenadmin pushed a commit to electricsheephq/lossless-hermes that referenced this pull request May 13, 2026
Lands the four Wave-0 deliverables described in ROADMAP.md before Wave 1
(Epic 00 Scaffolding) dispatches the first Issue Executor agent.

## State files (operational backbone)

- `STATUS.md` — cached projection of wave/milestone state. Git wins on drift.
- `BLOCKERS.md` — append-only decision queue.
- `LEDGER.md` — wave cost + velocity ledger.
- `docs/upstream/` — 4 patch tracking files (one per ADR-015 patch).

These three plus the upstream/ directory implement ADR-024's operational
backbone for AI-driven execution. Resumability protocol now in STATUS.md.

## Schema-diff CI scaffold (closes ADR-025 open Q#2)

- `scripts/schema_diff.sh` — orchestrator with three modes
  (`--refresh-reference`, `--verify`, `--check-reference`).
- `scripts/extract_lcm_schema.ts` — Node helper that runs
  `runLcmMigrations()` from `Martian-Engineering/lossless-claw` at
  commit 1f07fbd and dumps schema via `sqlite_master`.
- `scripts/extract_python_schema.py` — Python counterpart that runs
  `lossless_hermes.db.migration.run_lcm_migrations` (returns exit 2
  with a clear "not yet implemented" message pre-Wave-2; will activate
  when Epic 01 lands).
- `tests/fixtures/lcm_reference_schema.sql` — golden reference with
  92 schema objects extracted from LCM 1f07fbd. Used by `--verify` as
  the byte-compat target. Re-extracted by `--refresh-reference` when
  the LCM pin bumps.
- `tests/fixtures/lcm_reference_meta.txt` — provenance metadata.
- `.github/workflows/schema-diff.yml` — CI runs `--verify` on every PR
  touching `db/migration*` paths, plus a weekly cron + manual-dispatch
  `--check-reference` job that re-extracts TS schema and catches a
  stale committed reference.

The scaffold is validated end-to-end: `pnpm install` in lossless-claw
succeeded, the extract script produced a 92-object reference, and the
fixture is committed. Wave 2 first issue will be the first time
`--verify` runs against real Python migrations.

## Upstream Hermes preassemble() PR (closes ADR-010 R01)

Filed NousResearch/hermes-agent#24949 — adds an additive
`ContextEngine.preassemble(messages, budget_tokens=None)` ABC method
plus a single call site in `run_agent.py` (post-sanitization,
pre-retry-loop). 22/22 tests pass (3 new + 19 existing).

Updated `docs/upstream/001-preassemble-abc.md`:
- `status: drafted` → `filed`
- `pr_url: https://github.com/NousResearch/hermes-agent/pull/24949`
- Transition log entry with diff stats and impact analysis.

Fallback path (ADR-010 Option A) remains documented if the PR is
rejected; we don't block lossless-hermes execution on the upstream merge.

## Wave 0 exit gate status

- [x] Schema-diff CI scaffold green on empty schema ✅
- [x] Reference extracted (92 objects, commit 1f07fbd) ✅
- [x] Upstream PR filed (#24949) ✅
- [x] State files committed ✅
- [x] LCM index responding (openclaw-code-index MCP server, repo
      `lossless-claw`, 7382 nodes, 6836 embeddings, file:line refs work) ✅
- [ ] Dry-run on 00-01 (Issue Executor + Pair Reviewer end-to-end) —
      next.

## Next

Dispatch first Issue Executor agent on
`epics/00-scaffolding/issues/00-01-pyproject-and-package-skeleton.md`,
followed by a Pair Reviewer on the resulting PR. If both produce
shippable work, Wave 0 gate closes and Wave 1 fan-out begins.
@Chuchito768549 Chuchito768549 mentioned this pull request May 13, 2026
@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have labels May 13, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused ContextEngine extension. The need is still present on current main, but this implementation needs a redesigned salvage rather than a direct transplant.

Problems

  • The hunk is stale: run_agent.py:5775-5795 now only forwards to agent/conversation_loop.py; current request assembly and retry handling are at agent/conversation_loop.py:787-1161.
  • The proposed call occurs after cache-control and validation. Current main applies cache control at agent/conversation_loop.py:889-894 and request sanitizers through :952; accepting an arbitrary replacement list after those passes bypasses them. The PR test in d07bd3dae1a7a0c26ef75ba61cbeb0c85e5bb8fd also constructs system → user → user, contrary to the role-alternation invariant.
  • AGENTS.md:19-23 makes prompt-cache stability an invariant. A per-request rewrite needs an explicit cache contract before it becomes a host API.

Suggested changes

This is an automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 13, 2026
@100yenadmin

Copy link
Copy Markdown
Author

Closing in favor of #51226 / RFC #36765. The select_context() design there lands the per-turn context-rewrite capability at the right seam on current main (agent/conversation_loop.py — the review here correctly noted this PR's hunk targeted the old run_agent.py location), and #51226's scope explicitly covers the selection use-case this PR was for.

The review's other points stand too: a per-request rewrite hook needs a defined prompt-cache contract before becoming host API, and #51226 is the right venue for that conversation. Thanks @teknium1 for the thorough salvage analysis.

chaos-xxl added a commit to chaos-xxl/hermes-agent that referenced this pull request Jul 21, 2026
Adds an optional, no-op-default select_context() hook to the ContextEngine
ABC, called every turn after the request messages are assembled and before
provider dispatch — independent of should_compress(). Lets an engine select
or replace which context enters the prompt for a single request (retrieval,
topic routing, role/branch switching) without mutating persisted history,
removing the need to abuse should_compress()=True as a per-turn callback.

The host call site (_apply_context_engine_selection) is fail-open: a missing
hook, an exception, or an invalid return value leaves the assembled request
untouched. Additive and non-breaking: the built-in compressor and every
existing engine are unaffected.

Consolidates the per-turn request-assembly surface proposed across NousResearch#41918,
NousResearch#24949, NousResearch#47109, and NousResearch#50053 into one canonical hook (RFC NousResearch#36765).

Related: NousResearch#36765 NousResearch#41918 NousResearch#24949 NousResearch#47109 NousResearch#50053 NousResearch#23837 NousResearch#25115 NousResearch#29370
teknium1 pushed a commit that referenced this pull request Jul 24, 2026
Adds an optional, no-op-default select_context() hook to the ContextEngine
ABC, called every turn after the request messages are assembled and before
provider dispatch — independent of should_compress(). Lets an engine select
or replace which context enters the prompt for a single request (retrieval,
topic routing, role/branch switching) without mutating persisted history,
removing the need to abuse should_compress()=True as a per-turn callback.

The host call site (_apply_context_engine_selection) is fail-open: a missing
hook, an exception, or an invalid return value leaves the assembled request
untouched. Additive and non-breaking: the built-in compressor and every
existing engine are unaffected.

Consolidates the per-turn request-assembly surface proposed across #41918,

Related: #36765 #41918 #24949 #47109 #50053 #23837 #25115 #29370
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
Adds an optional, no-op-default select_context() hook to the ContextEngine
ABC, called every turn after the request messages are assembled and before
provider dispatch — independent of should_compress(). Lets an engine select
or replace which context enters the prompt for a single request (retrieval,
topic routing, role/branch switching) without mutating persisted history,
removing the need to abuse should_compress()=True as a per-turn callback.

The host call site (_apply_context_engine_selection) is fail-open: a missing
hook, an exception, or an invalid return value leaves the assembled request
untouched. Additive and non-breaking: the built-in compressor and every
existing engine are unaffected.

Consolidates the per-turn request-assembly surface proposed across NousResearch#41918,

Related: NousResearch#36765 NousResearch#41918 NousResearch#24949 NousResearch#47109 NousResearch#50053 NousResearch#23837 NousResearch#25115 NousResearch#29370
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants