Skip to content

feat(integrations): give the automatic integration lifecycle one implementation per language #2250

Open

Description

@Fengzdadi

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_unavailable and authentication_failed diagnostics.
  • 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_POLICY and the BEGIN_POWERCONTEXT_PREPARED_CONTEXT_V1 envelope.

#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 acknowledgement

Delivery

  • 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 the python3 on PATH and 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.py and the vendored powercontext_client_config.py fold into it.
  • 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 .mjs hooks, so they get a checked-in single-file build. That build is checked the same way DSH's checked-in lib/ is.
  • One command writes every copy, and its --check mode runs in make check.
    • The repository already does this in two places: make js-api-generate writes operations.generated.ts into the DSH, OpenCode, and Pi plugins, and make integration-guidance refreshes packaged guidance.
    • This replaces the hand-kept list in tests/test_integration_shared_files.py.

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 httpx client.
  • 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.

  1. 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, and src/transport.ts.
    • Add characterization fixtures for Claude Code and Codex: native payload → requests sent to the Server → output returned to the host.
  2. 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.
  3. 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.json as its fixture.
    • TypeScript: move ZCode, which covers the single-file .mjs build.
  4. Remaining hosts, one PR each

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 query maximum come from openapi/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

  1. Each host gets characterization fixtures, recorded before the host moves.
  2. 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 read tests/fixtures/automatic_prompt_privacy.json.
  3. Each host gets shell tests built from recorded native payloads.
  4. 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.toml excludes.

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. ScopeBindingKey includes integration, and resolve_binding matches 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 | host on the Server. Host plugins have no matching signal, so a host can inject context that the gateway has already injected.

Questions for maintainers

Alternatives considered

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.

Activity

  1. Fengzdadi commented on Oct 26, 2026

    @Fengzdadi
    ContributorAuthor

    @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:

    src/powercontext_integration_lifecycle/      # new top-level package, standard-library only
    ├── __init__.py
    ├── client_config.py       # source of powercontext_client_config.py (Claude Code, Codex, Hermes, WorkBuddy)
    ├── prepared_context.py    # source of hooks/prepared_context.py (Claude Code, Codex, WorkBuddy)
    ├── prompt_privacy.py      # source of hooks/prompt_privacy.py (Claude Code, Codex, WorkBuddy)
    └── bootstrap_context.py   # source of hooks/bootstrap_context.py (Claude Code, Codex)
    scripts/generate_integration_lifecycle.py    # `make integration-lifecycle` writes the 12 copies; `--check` runs in `make check`
    
    • The package is added to the wheel's packages, next to src/powercontext_service_bootstrap. It stays out of powercontext.client, so it is not SDK API, is not in the Python API reference, and needs no client extra.
    • The copies stay where each plugin imports them today and gain only a generated-file header. No plugin's layout, installation, or behavior changes, and the Claude Code and Codex characterization fixtures from test(integrations): characterize the Claude Code and Codex hooks #2255 pass unchanged.
    • Settings, transport policy, Scope resolution, capture, flush, and diagnostics move into the same package in later PRs, one host per PR.

    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.

  2. Fengzdadi commented on Oct 27, 2026

    @Fengzdadi
    ContributorAuthor

    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 .mjs hooks. 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:

    • Every plugin would depend on a separately installed component, at a version that matches both the plugin and the Server. That is the "shared package that every plugin installs" alternative in the issue, and its "runtime dependency between installed packages" non-goal.
    • The protocol between plugins and the client would be a universal cross-host hook API, which the issue also lists as a non-goal.
    • The nine in-process hosts call the Server directly today and would gain a hop. Each command hook would also spend the client's startup time inside its request budget, which is 1 s for Kimi Code and ZCode.

    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 runs powercontext/client/__init__.py, which imports httpx. It would also be published in the Python API reference, and its normalize_server_url behaves differently from the SDK function of the same name.
    • A top-level package, src/powercontext_integration_lifecycle/, meets all of them, following the precedent of src/powercontext_service_bootstrap.
    • An unpackaged source directory under integrations/ would also meet them, but adapters could not import it from the installed package.

    One caveat: powercontext-langchain, powercontext-langgraph, powercontext-pydantic-ai, and powercontext-bub are released separately and depend on powercontext[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, as powercontext.client.transport_policy is 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions