Repository navigation
feat(integrations): give the automatic integration lifecycle one implementation per language #2250
Description
Feature description
Give the automatic integration lifecycle one implementation per language. A Python module and a TypeScript module would each own what every agent host does on the automatic path: resolve connection settings, apply transport policy, resolve and bind the Scope, bound the recall query, capture Sources, and flush before compaction.
Plugins that are copied into the host ship a generated copy of the module, so installed packages stay independent of each other. Framework adapters import it from the installed powercontext package. Host plugins keep only what belongs to the host: hook registration, translating native events and outputs, and packaging. The Server API does not change.
Problem and proposed solution
Problem
Fourteen integrations run the automatic lifecycle: every integration in integrations/capabilities.toml except Dify, which is tool-only. Each one implements the lifecycle on its own, and the repository already works around the duplication by hand.
Copies. #2084 tracks the divergence between host integrations as R14. For copied files, #2148 added tests/test_integration_shared_files.py, which keeps two files identical across hosts "until they move to one package". The table lists those two and the other copies:
| File | Copies | Compared by a test |
|---|---|---|
powercontext_client_config.py |
Claude Code, Codex, Hermes, WorkBuddy | yes |
hooks/prepared_context.py |
Claude Code, Codex, WorkBuddy | yes |
hooks/prompt_privacy.py |
Claude Code, Codex, WorkBuddy; identical | no |
hooks/bootstrap_context.py |
Claude Code, Codex; one license-header line differs | no |
hooks/diagnostics.py |
Claude Code, Codex; only the output file name differs | no |
scripts/workspace_scope.py |
Claude Code, WorkBuddy, and Codex as scripts/scope_binding.py; 14 of 15 functions in common, 69 and 104 lines differ from the Claude Code copy |
no |
shared/prompt-privacy.mjs |
Kimi Code, ZCode; identical | no |
src/transport.ts |
DSH, OpenClaw, OpenCode, Pi; identical | no |
src/memory-operations.ts |
DSH, OpenCode, Pi; OpenCode and Pi identical, DSH differs in three lines of wording | no |
src/secrets.ts |
OpenCode, Pi; two comments and one variable name differ | no |
src/config.ts |
DSH, OpenClaw, OpenCode, Pi; 94 to 148 lines, all different | — |
The privacy copies also run the shared cases in tests/fixtures/automatic_prompt_privacy.json, which catch a difference only on those cases.
Claude Code and ZCode derive the capture source_id with the same scheme: a sha256 of the Scope, session, turn, and prompt. That scheme is written twice, once in Python and once in .mjs.
New contracts are written once per host. #2180 adds flushProgress, scopeCorrelation, and nonNegativeInteger to both OpenClaw's lifecycle.ts and Pi's flush.ts. The two test suites stay aligned by reading the same tests/integration_conformance/cases.json. Every further host that adopts pre-compaction checkpoints writes this logic again. #2174 shows the same pattern for an existing policy: it applied one credential policy for automatic prompts to seven adapters by writing it three times, in Python, .mjs, and TypeScript, copying the versions into three, two, and two plugins, and changing each host's call site.
Shared behavior has drifted. Bounding the recall query to the 8,192-character limit on PrepareContextRequest.query is one example:
| Behavior | Hosts |
|---|---|
| keep the end | Claude Code, Codex, WorkBuddy, ZCode, Kimi Code |
| keep the start | Hermes, LangChain, LangGraph, OpenClaw, Pydantic AI |
| no bound | Pi, OpenCode, DSH (sends the joined text of all messages), Bub |
With no bound, recall fails for any longer prompt.
Two more examples:
- When the Scope cannot be resolved, for example because the Server is not running or rejects the credential, DSH, Kimi Code, and WorkBuddy report the failure. The Claude Code and Codex prompt hooks end without output, although their guides promise
server_unavailableandauthentication_faileddiagnostics. - Eleven adapters prepend their own notice that the content is untrusted: Bub, DSH, Hermes, Kimi Code, LangChain, LangGraph, OpenClaw, OpenCode, Pi, Pydantic AI, and ZCode. The wording varies, and the Server has already wrapped that text in
TRUST_POLICYand theBEGIN_POWERCONTEXT_PREPARED_CONTEXT_V1envelope.
#2055 lists the configuration differences: variable names, units, plaintext-HTTP opt-in, and flush points.
Proposal
Split the work into three layers, each with a single owner:
| Layer | Owns |
|---|---|
| Host shell, one per host | Hook or plugin registration; translating native payloads into lifecycle events; the current-turn projection (#2119); emitting prepared context in the host's native form; deciding when to flush from the hook points the host offers; packaging and docs |
| Lifecycle module, one per language | Connection settings, reading each host's current names as aliases; transport policy; Scope resolution and binding requests; bounding the query to the contract limit; capture requests and source_id; privacy pre-filtering; flush acknowledgement with the semantics defined in #2068; diagnostics and the outcome vocabulary |
| Server, unchanged | Retrieval, rendering, trust wrapping, and persistence |
The module covers the automatic-path capabilities in capabilities.toml: context_injection, source_capture, flush_or_checkpoint, and persistent_scope_binding. It also covers session bootstrap and diagnostics. Tool capabilities such as search, handoff, and review are out of scope. Hosts expose those either through the Server's MCP endpoint or through their own tool definitions, and that is a separate question.
An illustrative Python shape; the TypeScript module uses the same names:
core = Lifecycle(
HostProfile(integration="claude-code", query_bound="tail", workspace_key="git_root"),
settings,
transport=UrllibTransport(), # replaceable
state=FileState(...), # hooks persist state between processes; in-process hosts use MemoryState
)
core.session_start(event) # bootstrap context, or None
core.prompt(event) # prepared context to inject, or None
core.capture(event) # the raw turn plus the shell's current-turn projection
core.flush(event, deadline) # pre-compaction acknowledgementDelivery
- Python
- The source is one package in the wheel, either
src/powercontext/client/lifecycle/or a top-level package; the questions below ask which. A test keeps it standard-library only, because Claude Code runs its hooks with thepython3onPATHand Hermes installs no dependency for its plugin. It also has to run on the oldest Python that a host runs its copy with; for Claude Code on macOS, that is often the system Python 3.9. - Framework adapters that depend on
powercontext[client]import it. Copy-installed plugins (Claude Code, Codex, WorkBuddy, Hermes) ship a generated copy. client/transport_policy.pyand the vendoredpowercontext_client_config.pyfold into it.
- The source is one package in the wheel, either
- TypeScript
- There is one source, copied into each plugin's
src/so that every package still builds on its own. - Kimi Code and ZCode run plain
.mjshooks, so they get a checked-in single-file build. That build is checked the same way DSH's checked-inlib/is.
- There is one source, copied into each plugin's
- One command writes every copy, and its
--checkmode runs inmake check.- The repository already does this in two places:
make js-api-generatewritesoperations.generated.tsinto the DSH, OpenCode, and Pi plugins, andmake integration-guidancerefreshes packaged guidance. - This replaces the hand-kept list in
tests/test_integration_shared_files.py.
- The repository already does this in two places:
Differences become declarations. Each intended difference is declared in the host's HostProfile or in capabilities.toml. Examples are the query-bound direction and how the workspace key is derived. A difference declared in neither place is a bug.
Python starts with a synchronous driver.
- The Python hosts that flush and bind (Claude Code, Codex, WorkBuddy, Hermes) are all synchronous.
- Pydantic AI and LangGraph only inject. Framework adapters use the module's pure parts and keep their own
httpxclient. - Bub's asynchronous flush stays as it is until a second asynchronous host needs the same driver.
What stays per host
- Manifests, hook registration, installation, and setup commands.
- Parsing native payloads, extracting the current turn, and building the capture projection. WorkBuddy's projection is defined in feat(sources): design versioned host-owned current-turn evidence projections #2119.
- How prepared context reaches the model:
additionalContext,prependContext, a system message, and so on. - Features only one host has, such as Codex token-savings reporting.
Relationship to #1362 and #2068
#1362 closed with the item "Share contract fixtures or helpers where they reduce drift without coupling installation packages" still unchecked. It also asks not to add a shared integration runtime "before a second maintained integration proves that the abstraction is useful".
This proposal shares helpers by generating a copy into each package, so installed packages stay uncoupled. More than one maintained integration already needs the same code. OpenClaw and Pi need the same flush acknowledgement in #2180. Claude Code and Codex already ship copies of the same five files.
#2068 calls for reusable conformance cases "without a shared integration runtime". It also keeps "HTTP clients, hook registration, configuration, and logging in the owning adapter". Under this proposal, hook registration stays in the adapter, but request construction and settings resolution move into the generated module. If that second rule is meant to hold beyond the pre-compaction work, the two conflict. The first question below asks which is intended.
Rendering, citations, and persistence stay with the Server, as #1362 requires. Removing the adapter-side trust notices moves adapters closer to that rule.
Plan
A structural move never changes behavior, and a behavior fix never moves structure. Before a host moves, it gets characterization fixtures; they show that the move changed nothing.
- Guard what is already shared (tests only)
- Extend the shared-files test to
hooks/prompt_privacy.py,hooks/bootstrap_context.py(after aligning its license header),shared/prompt-privacy.mjs, andsrc/transport.ts. - Add characterization fixtures for Claude Code and Codex: native payload → requests sent to the Server → output returned to the host.
- Extend the shared-files test to
- Fix drift first, one PR each
- Bound the recall query in Pi, OpenCode, DSH, and Bub, keeping the end of the prompt as the hook-based hosts do.
- Claude Code and Codex: report Scope resolution failures from the prompt hooks, as their guides promise, and stop reporting an unknown Scope as a version mismatch.
- Remove adapter-side trust notices that repeat the Server's wrapping. Keep any marker an adapter uses to recognize its own earlier injections.
- Pilot, once the questions below are settled
- Python: add the module and the generate/check command, then move Claude Code and Codex onto them.
- Python: move Hermes, which covers in-process state.
- TypeScript: after feat(integrations): enforce pre-compaction lifecycle contracts #2180 merges, move the Pi and OpenClaw flush acknowledgement into the module, with
tests/integration_conformance/cases.jsonas its fixture. - TypeScript: move ZCode, which covers the single-file
.mjsbuild.
- Remaining hosts, one PR each
- WorkBuddy, whose capture follows feat(sources): design versioned host-owned current-turn evidence projections #2119
- DSH
- OpenCode
- Kimi Code
- LangChain, LangGraph, and Pydantic AI, using the pure parts only
- Bub
Every PR in steps 3 and 4 must:
- leave the host's characterization fixtures unchanged
- add shell tests built from recorded native payloads
- run the real host once
Compatibility
- Settings: the module reads every name each host reads today. Nothing users set is renamed. Whether names should converge is a separate decision, tracked in feat(integrations): reconcile agent host configuration and behavior after the core gateway lands #2055.
- Local state: diagnostics files, binding caches, and flush cursors keep their current paths and formats. The host profile carries host-specific names such as
claude-code-diagnostics.json. - Packages: every plugin stays self-contained. Plugins that run without third-party packages today still do.
- Server: no new endpoints or fields. Limits such as the
querymaximum come fromopenapi/powercontext.yaml. - Workspace keys: each host keeps deriving them as it does today. For example, Claude Code and Codex use the git root; Pi, OpenCode, and DSH use the resolved working directory. Deriving them differently would orphan existing bindings. Any change waits for the Server-side work below, which has to define how legacy keys are handled.
Verification
- Each host gets characterization fixtures, recorded before the host moves.
- Shared fixtures at the module boundary have the shape
{event, Server responses} → {requests, result}. Both the Python and the TypeScript module run them, the same way several hosts already readtests/fixtures/automatic_prompt_privacy.json. - Each host gets shell tests built from recorded native payloads.
- Every migration PR runs the real host. Kimi Code and ZCode have acceptance workflows, and DSH has a pinned-runtime test (
make dsh-runtime-test). Other hosts are run by hand from their setup docs. Each PR says which hosts were actually run, and whether its payloads were recorded or synthetic.
Non-goals
- A universal cross-host hook API.
- A runtime dependency between installed packages.
- Changes to the HTTP API, MCP tools, or Server behavior.
- Dify, and the directories that
capabilities.tomlexcludes.
Tracked separately
Some cross-host problems cannot be fixed inside the module. Each item below belongs in its own issue or RFC; the module picks up the result once it lands.
- Workspace bindings are scoped per host.
ScopeBindingKeyincludesintegration, andresolve_bindingmatches the full key. As a result, a repository bound in Claude Code is not bound in Codex, even though both derive the same git root. A host-neutral workspace key, with handling for legacy keys, needs an RFC. - Captured text can include context the Server injected earlier. The Server does not recognize its own envelope at ingestion. This belongs with the current-turn projection in feat(sources): design versioned host-owned current-turn evidence projections #2119.
- Injection owner. docs(rfc): define Rust PowerContext Gateway #2199 configures
context_owner: gateway | hoston the Server. Host plugins have no matching signal, so a host can inject context that the gateway has already injected.
Questions for maintainers
-
@AlexStocks: in feat(integrations): qualify pre-compaction checkpoint durability and failure semantics #2068, is "Keep HTTP clients, hook registration, configuration, and logging in the owning adapter" scoped to the pre-compaction work, or is it a standing rule for integrations?
-
@PsiACE: does generating one source into each package meet the conditions in Tracking Issue: lifecycle-aware Agent integrations #1362? And where should the Python module live? Under
powercontext.client, it ships in the wheel as SDK API:- The website builds its Python API reference from
powercontext.client, so these modules would be published there. powercontext/client/__init__.pyimportshttpx, so the module cannot be imported without theclientextra, although each plugin copy is standard-library only.- Its
normalize_server_urldrops an/mcpsuffix, andpowercontext.client.settings.normalize_server_urlkeeps it.
A top-level standard-library-only package, like
src/powercontext_service_bootstrap, would ship in the same wheel, so framework adapters could still import it, but it would stay out of the SDK and its API reference. I lean to the top-level package. Either way, an edit to the source undersrc/selects all 16 CI suites, while an edit to a plugin copy selects onlytests. - The website builds its Python API reference from
-
@beatenevo: once feat(integrations): enforce pre-compaction lifecycle contracts #2180 merges, can the Pi and OpenClaw flush code move into the TypeScript module, with
cases.jsonkept as its fixture? -
@hidb4ai: is docs(rfc): define Rust PowerContext Gateway #2199 the gateway that feat(integrations): reconcile agent host configuration and behavior after the core gateway lands #2055 was deferred for, or is a separate entry point for agent hosts planned?
-
Is this issue enough for the module boundary, or should it go through an RFC first?
Alternatives considered
- Share fixtures only. This is the current approach, used by fix(integrations): guard automatic prompt capture and preparation #2174 and feat(integrations): enforce pre-compaction lifecycle contracts #2180. Shared cases keep the hosts aligned on those cases, but each new contract is still written again for every host, and copies that no test compares still drift.
- Wait for a gateway. feat(integrations): reconcile agent host configuration and behavior after the core gateway lands #2055 deferred these differences until a core gateway gave hosts a shared entry point. The gateway in docs(rfc): define Rust PowerContext Gateway #2199 forwards
/v1and/mcpunchanged and adds/gateway/v1for model traffic. Host plugins would still call the Server themselves, so unless a separate entry point for agent hosts is planned, these differences remain after it lands. - A shared package that every plugin installs. Claude Code runs its hooks with the
python3onPATH, and Hermes installs no dependency for its plugin, so neither can rely on an installed package. A shared install would also couple installation packages, which Tracking Issue: lifecycle-aware Agent integrations #1362 asks to avoid. - Move everything to the Server. Settings, transport, and native I/O have to run inside the host. The Server-side items above are tracked separately.
Additional context
Code facts were observed on upstream/master at bad4c0c, except the #2180 details, which come from its open diff:
- Copies were compared by git blob and
diff. - Query bounds were read from each host's prepare call.
- The capability scope comes from
integrations/capabilities.toml.
Are you willing to contribute to this feature?
- Yes, I am willing to contribute code, docs, or design feedback.
For now I'll implement this myself, including every host migration. Please comment here before starting on any item, so the work isn't duplicated.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
Fengzdadi commentedon Oct 26, 2026
@PsiACE, here is the location question from the issue in concrete form, so it can be answered with a yes or no.
I'd like to start step 3 with this layout:
packages, next tosrc/powercontext_service_bootstrap. It stays out ofpowercontext.client, so it is not SDK API, is not in the Python API reference, and needs noclientextra.OK to go ahead with this layout? If you'd rather have it under
src/powercontext/client/lifecycle/, I'll use that path; the prototype already works there.Fengzdadi commentedon Oct 27, 2026
Two other directions are worth settling before step 3, together with where the Python module lives.
One language for every plugin. Nine of the 14 hosts on the automatic path run the adapter inside the host process, so the host fixes the language: Hermes, Bub, LangChain, LangGraph, and Pydantic AI are Python; Pi, OpenCode, OpenClaw, and DSH are TypeScript. Only the five command-hook hosts can choose. Claude Code, Codex, and WorkBuddy run Python hooks, and Kimi Code and ZCode run
.mjshooks. Both implementations therefore stay, and moving a command-hook host to the other language would not remove either of them. Which language each command-hook host uses can be decided per host later, by which runtime that host can rely on. The fixtures in #2255 record only what crosses the process boundary, so they could check such a port as they are.A fat client. If every plugin called one local client, there would be one implementation, and each plugin would only forward events. The costs:
The fat client and this plan don't exclude each other. Once each language has one module, a command hook could call an installed hook command that wraps that module, if setup can guarantee the command exists. I'd treat that as a possible later step, not as an alternative to this plan.
Where the Python module lives. The module is the single source of the host-side lifecycle code. In step 3 it reaches users only as copies inside plugins, and framework adapters may import it later. So it has to import with the standard library alone, run on Python 3.9 (the Claude Code copy often runs on the macOS system Python), and stay out of the SDK's public API.
src/powercontext/client/lifecycle/misses these. Importing it runspowercontext/client/__init__.py, which importshttpx. It would also be published in the Python API reference, and itsnormalize_server_urlbehaves differently from the SDK function of the same name.src/powercontext_integration_lifecycle/, meets all of them, following the precedent ofsrc/powercontext_service_bootstrap.integrations/would also meet them, but adapters could not import it from the installed package.One caveat:
powercontext-langchain,powercontext-langgraph,powercontext-pydantic-ai, andpowercontext-bubare released separately and depend onpowercontext[client]without pinning a version (Pydantic AI requires only>=0.0.3). Once an adapter imports the module, the module becomes an interface between separately released packages, aspowercontext.client.transport_policyis today. The first step-3 PR has no such consumer, so importing versus copying can be decided when an adapter moves.@PsiACE, unless you'd rather have it under
powercontext.client, or someone wants an RFC first, I'll open the first step-3 PR with the top-level package after #2255 merges: the module, the generate/check command, and the 12 generated copies. Moving it later is a path change.