pi-continual-harness
Online self-improvement layer for pi. A unified, in-trajectory harness-state store (prompt notes, memory, skill descriptions, sub-agent specs) with a manual /refine that proposes evidence-backed structured CRUD deltas. Composes with pi-reflect (offline) a
Package details
Install pi-continual-harness from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-continual-harness- Package
pi-continual-harness- Version
0.10.0- Published
- Sep 20, 2026
- Downloads
- 383/mo · 284/wk
- Author
- ngsoftware
- License
- MIT
- Types
- extension
- Size
- 148.8 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-continual-harness
Online self-improvement layer for the pi coding agent.
This package owns only the online optimizer layer: a unified, in-trajectory
harness-state store (prompt notes, memory, skill descriptions, sub-agent specs)
plus a manual /refine that proposes evidence-backed structured CRUD deltas.
It deliberately does not reinvent storage or offline refinement. It composes with:
- pi-reflect — offline transcript → behavioral-file refinement (the "deep" path).
- pi-mem / pi-memory — durable memory storage.
The durable markdown layers are the composition seam — the shared
~/.pi/agent/harness-state.md plus per-project files under
~/.pi/agent/harness-state/<slug>.md — and they are two-way:
/refine --commit and /harness export write them; /harness import parses
them back and merges into the live store (offline edits win on conflict), so
refinements pi-reflect makes flow back online. /harness push-mem
pushes active items into pi-mem's semantic store (see
Composing with pi-mem).
Why
Grounded in two lines of research:
- Continual Harness (arXiv 2605.09998) — reset-free online CRUD over prompt / sub-agents / skills / memory drawn from the trajectory. Distinct from prompt-optimization methods that need episode resets.
- ACE — Agentic Context Engineering (arXiv 2510.04618) — context as an evolving playbook. Key design lesson applied here: the optimizer emits structured, itemized deltas, never prose prompt rewrites, to prevent context collapse and brevity bias.
Prime Intellect's Prime Agent ships a Continual Harness built on pi; this package is a minimal, package-shaped take on the same idea — online only, leaving offline and storage to the packages that already do them well.
Install
pi install npm:pi-continual-harness
Or drop src/index.ts into ~/.pi/agent/extensions/.
Usage
/refine # review last 25 turns, propose deltas
/refine 50 # review last 50 turns
/refine 25 --commit # also export durable state (global + project layers)
/refine --proposer dedupe # run the rule-based dedupe proposer instead of steering
/refine --proposer dedupe --threshold 0.75 # dedupe with a one-shot threshold
Durable I/O (round-trip with pi-reflect):
/harness status # counts + durable layer presence/mtime
/harness export [path] # layered export (or full snapshot to a path)
/harness import [--prune] [path] # layered import (or single file from a path)
/harness prune [--decay <days>] # drop items below the importance floor
/harness keep <id> # nudge importance up (+0.1)
/harness drop <id> # nudge importance down (−0.1)
/harness move <id> global|project # move an item between durable layers
/harness split # steer the agent to classify every item's scope
/harness push-mem [--all|--kind <kind>|--model <provider/id|active>] # persist active items to pi-mem (save_memory)
Subcommands, flags, --kind/--model values, move scopes, and keep/
drop/move item ids all autocomplete in the TUI as you type after
/harness .
Durable layers. Every item carries its own durable scope: global
(the shared ~/.pi/agent/harness-state.md) or project
(~/.pi/agent/harness-state/<slug>.md, slug derived from the session cwd).
/harness export without a path partitions items by their scope into the
layer files; /harness import without a path merges the global file always,
plus the current project's file (the project layer wins id collisions;
--prune drops only items absent from every layer). An explicit path keeps
the classic single-file semantics. Move items between layers with
/harness move, or classify the whole store at once with /harness split
(the agent proposes scope-only harness_mutate updates — visible in the
transcript, /tree-rollback-able).
import reconciles the file(s) into the live store: items whose id matches an
existing entry are updated (offline edits win on content/evidence/importance),
new entries are created. By default nothing is deleted — --prune also drops
active items whose id is no longer in the file(s) (inactive items are always
preserved). Point pi-reflect at either layer to refine it offline:
/reflect ~/.pi/agent/harness-state.md
/reflect ~/.pi/agent/harness-state/<project-slug>.md
The model-facing tools:
harness_list({ kind?, model? })— read current state.modeldefaults to the active model's items (what gets injected this turn);"*"returns every model.harness_mutate { deltas: [...] }— apply a batch ofcreate/update/deletedeltas. Everycreaterequiresevidence. New items are stamped automatically with the active model (see Model binding).create/updatedeltas also acceptscope: "global" | "project"(the project slug is stamped server-side from the session cwd — used by/harness split; normally managed via/harness move).
Active items are injected into the system prompt each turn as a structured block, appended to (never replacing) the base prompt.
Composing with pi-mem
/harness push-mem copies active harness items into
pi-mem's semantic memory store, so they
become searchable across sessions. It works by steering the agent to call
pi-mem's save_memory tool — there is no dependency on pi-mem: it is a
soft-fail composition. If pi-mem (or any memory tool) is not installed, the
agent tells you so rather than fabricating one.
pi install npm:pi-mem # optional companion
By default only memory-kind items are pushed (the clean 1:1 mapping); use
--all for every active item, --kind prompt|skill|subagent for a specific
kind, or --model <provider/id|active> to scope the push to one model's items
(--model active = the model driving the command). The harness store itself is
unchanged by a push — pi-mem gets a separate copy.
Configuration
Optional config at ~/.pi/agent/harness.json (missing or malformed → defaults):
{
"autoImport": false,
"proposer": "steering",
"dedupe": { "threshold": 0.6, "merge": true },
"injection": { "enabled": true, "maxTokens": 1500, "maxPerKind": 10, "charsPerToken": 4 },
"remindRefine": { "enabled": false, "everyTurns": 50 },
"autoRefine": { "enabled": false, "everyTurns": 100, "commit": false },
"outcomeImportance": { "enabled": false, "bump": 0.03 }
}
autoImport— opt-in durable sync (off by default).truebundles both directions: onsession_startthe durable layers are imported automatically (global file always + the current project's file, with the same loss-free merge as/harness import— an import that changes nothing stays silent), and onturn_endthe layers are re-exported whenever the live store changed since the last export. This is what makes/refineoutput survive into new sessions without manual export/import ceremony. Both halves are visible (one notify line) and use the same persistedharness-stateentries as every other mutation (/treerollback covers them).injection— the selection policy for WHAT gets surfaced in the system prompt each turn (see Injection selection). ON by default: items are importance-ordered, capped atmaxPerKind(default- per kind and
maxTokens(default 1500) total. Setenabled: falseto restore the legacy "inject all items, in store order" behaviour.
- per kind and
proposer— which delta proposer/refineand auto-refine use. Defaults tosteering(the agent reasons via a steering message).dedupeapplies a rule-based merge directly. See Proposers.dedupe— the rule-based dedupe proposer's policy.threshold(default0.6, valid(0,1]) is the token-overlap level at which two items sharing the key fields (kind, owner model, durable layer) count as duplicates;merge(defaulttrue) merges each duplicate into its keeper — the keeper keeps its content verbatim, itsevidencebecomes the line-wise union of both, applied as one auditedupdate+deletepair — whilefalserestores the pre-0.10 delete-only behavior./refine --proposer dedupe --threshold 0.75overrides the threshold for one run. A fourth key,similarity, is API-only (functions cannot come fromharness.json): companion packages inject a richer comparator whose return may abstain per pair ({ score, abstain }— seeSimilarityResult); an abstaining pair is always kept, never merged or deleted.remindRefine— opt-inturn_endnudge.{ "enabled": true, "everyTurns": 50 }notifies you to run/refineon a cadence. It is informational only — it never mutates state.autoRefine— opt-in autonomous self-improvement (off by default). Whenenabled, the agent runs/refineitself everyeveryTurnsturns (default 100). It is one of the package's opt-in autonomous paths: it reuses the exact/refineroutine (auditedREFINE_ENTRYtaggedsource: "auto", branch-local,/treerollback) and notifies before firing.commit: truealso flushes durable state on each run.outcomeImportance— opt-in autonomous promotion loop (off by default). Whenenabled, aturn_endhook bumps (+bump, default 0.03) the importance of any active item the agent cited by its[h_xxxx]tag in the turn's output. Referenced items gain importance and get theirupdatedAttouched (so they survive time-based decay); ignored items keep decaying. This is the package's second opt-in autonomous path — promotion only, never deletes, persisted/branchable likeharness_mutate. Autonomous demotion from outcomes is intentionally NOT done (high false-positive); use/harness drop,prune --decay, or thededupeproposer for that.
Deprecated:
durableScope(0.9.0). Items now carry their own scope (/harness move), and durable I/O is always layered — the key no longer switches anything but is still parsed so existing configs keep loading. Migration from adurableScope: "project"setup: run/harness importonce in the project (its file's items adopt project scope), then move any strays with/harness move(or classify the whole store with/harness split).
How it works
/refinegathers recent trajectory evidence from the current session branch.- A proposer decides what to do. The default (
steering) sends a steering user message asking the agent to propose evidence-backed CRUD deltas viaharness_mutate; rule-based proposers (e.g.dedupe) return deltas the harness applies directly. See Proposers. - The agent calls the tools (steering path); each accepted delta updates the
in-memory state, which is snapshotted to the session via
appendEntry("harness-state", ...). Direct-apply proposers snapshot the same way. New items are stamped with the active model (see Model binding). - Because pi's session tree branches at any entry,
/treenavigation gives rollback to any pre-refinement point for free — no bespoke snapshot system.
This reuses the existing agent loop (no nested/hidden model calls), is model-agnostic, and keeps every delta visible and reviewable in the transcript.
Model binding (per-model isolation)
Every item is bound to exactly one model as ownerModel ("provider/id"). An
item is only injected for the model it belongs to — so switching to a
brand-new model id starts from a blank harness, and one model's notes never
leak into another's context. Binding is at the exact model id (not family or
vendor): a new version id is a clean slate, by design.
How the binding is set and respected:
- Created items are stamped automatically with the model driving the turn.
The model-facing tools cannot read the active model, so
before_agent_start(which always fires first in a turn, with the model) caches it;harness_mutatestamps creates from that cache, and direct-apply proposers stamp from theirctx.model. You never name the model yourself. - Injection filters by owner. Only items whose
ownerModelmatches the active model are appended to the system prompt. An unknown model injects nothing. harness_listdefaults to the active model (passmodel: "*"for every model, or an explicit"provider/id").- Orphan adoption. Items with no owner — from a legacy session snapshot, an
old durable file, or created while the model was unknown — are adopted by the
active model on first contact (the next
before_agent_start). This is the migration path: existing harnesses transition cleanly with no manual steps, and it's persisted as a normalharness-stateentry (so/treerollback covers it). - Durable round-trip preserves owner.
/harness exporttags each item withmodel: provider/id;/harness importrestores it. An item whose tag pi-reflect stripped becomes an orphan and is adopted by the active model.
Manual commands (export, import, move, split, keep, drop, prune,
push-mem, status) operate on the whole store by design — they are
explicit human actions with full control. Isolation is enforced only where pollution would
leak automatically: injection, listing, create-stamping, and the outcome loop.
In particular, /harness push-mem pushes every model's active items into
pi-mem by default (which can yield near-duplicate memories across models); pass
--model <provider/id|active> to scope it to one model.
Injection selection (on by default)
The harness ACCUMULATES notes (create / refine / auto-refine / outcome-promotion), but the system prompt is finite. So since 0.8 the block appended each turn is the result of a selection policy, not the whole store — and it is on by default. The store itself is never changed by selection (nothing is lost); only what is surfaced changes.
The policy (pure, deterministic; src/select.ts):
- Filter — only active items bound to the active model (strict per-model isolation, unchanged).
- Order — importance desc; ties keep store/insertion order (stable). The highest-fitness notes lead each section.
- Cap —
maxPerKind(default 10: balanced sections, no single kind drowns the block), thenmaxTokens(default 1500: total budget). The budget is filled round-robin across kinds by importance rank, so one kind cannot starve the others; within that order an item that doesn't fit is skipped (not a hard stop), so a large item never blocks smaller higher-priority ones.
The defaults are deliberately generous — a no-op for small stores (nothing trimmed) and protective as the harness grows. When the policy drops items, the block ends with a one-line transparency note:
_(3 item(s) not shown — below the injection budget. Raise `injection.maxTokens`/`maxPerKind` in harness.json or run `/harness prune`.)_
Tune or disable it in harness.json:
{ "injection": { "maxTokens": 3000, "maxPerKind": 20 } } // raise the ceiling
{ "injection": { "maxPerKind": 5 } } // trim harder, per kind
{ "injection": { "enabled": false } } // opt out: legacy "all, in order"
Selection is factored into a pure function (selectForInjection) and
re-exported from the package entry, so a companion package can layer richer
policies (e.g. relevance to the current turn, via the shared tokenize /
tokenOverlap helpers) without touching inject.ts.
Proposers
/refine is split into two stages: propose (given evidence + state, decide
what deltas to pursue) and apply (send a steering message, or apply returned
deltas directly). The propose stage is pluggable via a registry
(src/proposer.ts).
| Name | What it does |
|---|---|
steering (default) |
Delegates reasoning to the agent via a steering message — reuses the agent loop, model-agnostic, fully visible. |
dedupe |
Rule-based: merges near-duplicate active items (token-overlap ≥ the configured threshold, same kind / owner model / durable layer) into the higher-importance keeper — one evidence-union update per keeper, then deletes the duplicates. "dedupe": { "merge": false } restores delete-only. No model call. |
The dedupe planner's similarity seam (DedupeOptions.similarity, re-exported
from the package entry) is how a companion package upgrades the comparison —
e.g. embedding cosine similarity, or a local decision engine such as pi-jev
(a local-only project for now). A comparator may return
{ score, abstain } instead of a plain number: an abstaining pair is
treated as not duplicates — both items are kept — so an engine with
conformal uncertainty guarantees can safely decline to merge. Plain numeric
returns keep working (the default remains token Jaccard).
Select a proposer per run with /refine --proposer <name>, or set the default
for auto-refine via proposer in the config. Both paths are
audited: the harness-refinement entry records which proposer ran and how many
deltas it applied directly.
A dedicated-model proposer — one that makes its own (hidden) LLM call to
produce deltas directly — is the obvious next alternate. This package still
does not ship one (hidden model spend is a tradeoff kept as a separate
decision; see docs/ROADMAP.md), but it now enables one: when a model is
available, /refine injects a one-shot complete(prompt, opts?) into
ProposeInput and records any modelCall telemetry a proposer returns
(model, tokens, latency, ok/error) in the harness-refinement audit entry —
so a companion package can ship a dedicated-model proposer whose spend stays
audited, not hidden. One ships as a companion: pi-harness-model-proposer
(pi install npm:pi-harness-model-proposer). Both complete and modelCall are optional;
steering and dedupe ignore them, so the default behavior is unchanged.
Register your own proposer from another extension (the registry is re-exported from the package entry):
import { registerProposer } from "pi-continual-harness";
registerProposer({
name: "my-proposer",
async propose({ evidence, state, complete }) {
// `complete` is injected when a model is available (undefined otherwise);
// a dedicated-model proposer calls it and returns deltas + modelCall telemetry.
/* inspect evidence + state, return deltas (and/or a steering message) */
return { deltas: [/* { delta, rationale } */] };
},
});
Then "my-proposer" is selectable via /refine --proposer my-proposer or
"proposer": "my-proposer" in the config.
Scope and non-goals
- In scope: unified state store, online
/refine, structured deltas, prompt injection, branching rollback, durable markdown export. - Out of scope (compose instead): durable storage engines (pi-mem), offline deep refinement of behavioral files (pi-reflect), live sub-agent orchestration (pi-boss / pi-room). Sub-agent specs are stored as data only.
Status
0.9.x. Implemented:
- Unified harness-state store with branch-local snapshots (
/treerollback). - Online
/refine+harness_mutate/harness_listtools. - Two-way durable round-trip with pi-reflect (
/harness import|export|status), layered on per-item scope: every item isglobalorproject(/harness move,/harness split), and/harness export|importoperate on both layers. - Importance hygiene:
/harness prune [--decay <days>]and/harness keep|drop <id>. - pi-mem composition:
/harness push-mem [--all|--kind|--model]steers the agent to persist active items into pi-mem (soft-fail; no dependency). - Opt-in durable sync (
"autoImport": true): session_start layered auto-import + turn_end layered auto-export when the store changed —/refineoutput survives into new sessions with zero ceremony. Off by default; every action visible and/tree-rollback-able. - Optional config (
~/.pi/agent/harness.json): the durable-sync opt-in, an opt-inturn_endreminder, opt-inturn_endauto-refine, and an opt-inturn_endoutcome-importance loop — the package's opt-in autonomous paths (all off by default). - Pluggable delta proposers with a registry:
steering(default) anddedupe(rule-based) shipped;registerProposer()for custom ones. - Per-model isolation: every item is bound to a
provider/idand injected only for that model; new items are stamped automatically and orphans adopted on first contact. A new model id starts from a blank harness, and the durable round-trip preserves the owner tag. - Bounded injection (on by default): the supplemental block is
importance-ordered and capped per kind + by a total token budget, so a growing
harness never balloons the system prompt. Tunable / opt-out via the
injectionconfig key; the store is never changed by selection.
Open extension points (see docs/ROADMAP.md):
- A dedicated-model proposer (the enabler landed:
completeinjection +modelCalltelemetry; the proposer logic itself is left to a companion package — the hidden-model-spend tradeoff is resolved by making the spend audited rather than shipping it invisible). - Correction-side outcome signals (promotion is shipped; autonomous demotion
is high-false-positive, so it is served by
/harness drop,prune --decay, and thededupeproposer — a fuzzycorrectionsproposer is the natural future extension).
License
MIT