pi-continual-harness 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/package.json +1 -1
- package/src/index.ts +13 -0
- package/src/proposer.ts +47 -3
- package/src/refine.ts +73 -1
package/README.md
CHANGED
|
@@ -178,9 +178,16 @@ for auto-refine via `proposer` in the [config](#configuration). Both paths are
|
|
|
178
178
|
audited: the `harness-refinement` entry records which proposer ran and how many
|
|
179
179
|
deltas it applied directly.
|
|
180
180
|
|
|
181
|
-
A dedicated-model proposer
|
|
182
|
-
|
|
183
|
-
spend is a tradeoff kept as a separate
|
|
181
|
+
A dedicated-model proposer — one that makes its own (hidden) LLM call to
|
|
182
|
+
produce deltas directly — is the obvious next alternate. This package still
|
|
183
|
+
does **not** ship one (hidden model spend is a tradeoff kept as a separate
|
|
184
|
+
decision; see `docs/ROADMAP.md`), but it now **enables** one: when a model is
|
|
185
|
+
available, `/refine` injects a one-shot `complete(prompt, opts?)` into
|
|
186
|
+
`ProposeInput` and records any `modelCall` telemetry a proposer returns
|
|
187
|
+
(model, tokens, latency, ok/error) in the `harness-refinement` audit entry —
|
|
188
|
+
so a companion package can ship a dedicated-model proposer whose spend stays
|
|
189
|
+
**audited, not hidden**. Both `complete` and `modelCall` are optional;
|
|
190
|
+
`steering` and `dedupe` ignore them, so the default behavior is unchanged.
|
|
184
191
|
|
|
185
192
|
Register your own proposer from another extension (the registry is re-exported
|
|
186
193
|
from the package entry):
|
|
@@ -190,7 +197,9 @@ import { registerProposer } from "pi-continual-harness";
|
|
|
190
197
|
|
|
191
198
|
registerProposer({
|
|
192
199
|
name: "my-proposer",
|
|
193
|
-
async propose({ evidence, state }) {
|
|
200
|
+
async propose({ evidence, state, complete }) {
|
|
201
|
+
// `complete` is injected when a model is available (undefined otherwise);
|
|
202
|
+
// a dedicated-model proposer calls it and returns deltas + modelCall telemetry.
|
|
194
203
|
/* inspect evidence + state, return deltas (and/or a steering message) */
|
|
195
204
|
return { deltas: [/* { delta, rationale } */] };
|
|
196
205
|
},
|
|
@@ -228,8 +237,10 @@ Then `"my-proposer"` is selectable via `/refine --proposer my-proposer` or
|
|
|
228
237
|
|
|
229
238
|
Open extension points (see `docs/ROADMAP.md`):
|
|
230
239
|
|
|
231
|
-
- A dedicated-model proposer (
|
|
232
|
-
is a
|
|
240
|
+
- A dedicated-model proposer (the enabler landed: `complete` injection +
|
|
241
|
+
`modelCall` telemetry; the proposer logic itself is left to a companion
|
|
242
|
+
package — the hidden-model-spend tradeoff is resolved by making the spend
|
|
243
|
+
audited rather than shipping it invisible).
|
|
233
244
|
- Correction-side outcome signals (promotion is shipped; autonomous demotion
|
|
234
245
|
is high-false-positive, so it is served by `/harness drop`, `prune --decay`,
|
|
235
246
|
and the `dedupe` proposer — a fuzzy `corrections` proposer is the natural
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-continual-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "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) and pi-mem (storage); owns only the online optimizer layer. Inspired by Continual Harness (arXiv 2605.09998) and ACE (arXiv 2510.04618).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/index.ts
CHANGED
|
@@ -47,13 +47,26 @@ export default function continualHarness(pi: ExtensionAPI): void {
|
|
|
47
47
|
|
|
48
48
|
export { STATE_ENTRY };
|
|
49
49
|
|
|
50
|
+
// Core domain types (re-exported so companion packages — e.g. a dedicated-model
|
|
51
|
+
// proposer — can build and validate deltas without reaching into internal paths).
|
|
52
|
+
export {
|
|
53
|
+
type AppliedDelta,
|
|
54
|
+
type ComponentKind,
|
|
55
|
+
type Delta,
|
|
56
|
+
type HarnessItem,
|
|
57
|
+
type HarnessState,
|
|
58
|
+
} from "./types.js";
|
|
59
|
+
|
|
50
60
|
// Public extension API: other extensions can register their own delta proposer
|
|
51
61
|
// (see src/proposer.ts) and it becomes selectable via /refine --proposer <name>
|
|
52
62
|
// or the `proposer` config key.
|
|
53
63
|
export {
|
|
54
64
|
listProposers,
|
|
55
65
|
registerProposer,
|
|
66
|
+
type CompleteOptions,
|
|
67
|
+
type CompleteResult,
|
|
56
68
|
type DeltaProposer,
|
|
69
|
+
type ModelCallTelemetry,
|
|
57
70
|
type ProposeInput,
|
|
58
71
|
type ProposedDelta,
|
|
59
72
|
type ProposeResult,
|
package/src/proposer.ts
CHANGED
|
@@ -14,15 +14,50 @@
|
|
|
14
14
|
// overlap, keeping the higher-importance one. Pure function of state —
|
|
15
15
|
// deterministic, no model call, fully testable.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// The interface supports a dedicated-model proposer — one that makes its own
|
|
18
|
+
// hidden LLM call to produce deltas directly, instead of delegating to the
|
|
19
|
+
// agent via a steering message. This package does NOT ship one (hidden model
|
|
20
|
+
// spend is a tradeoff the roadmap keeps as a separate decision), but it DOES
|
|
21
|
+
// inject a one-shot `complete` into ProposeInput when a model is available, so a
|
|
22
|
+
// companion package can register one. The closure is built by runRefine from
|
|
23
|
+
// ctx.modelRegistry; telemetry a proposer returns is recorded in the refine
|
|
24
|
+
// audit entry, so the spend stays visible (audited, not hidden).
|
|
20
25
|
//
|
|
21
26
|
// Extension is open: call registerProposer() from your own extension to add a
|
|
22
27
|
// named proposer, then select it via /refine --proposer <name> or config.
|
|
23
28
|
|
|
24
29
|
import type { Delta, HarnessItem, HarnessState } from "./types.js";
|
|
25
30
|
|
|
31
|
+
/** Options for the one-shot model completion injected into ProposeInput. */
|
|
32
|
+
export interface CompleteOptions {
|
|
33
|
+
/** Resolve and use this model id ("provider/id" or a bare id) instead of the
|
|
34
|
+
* active session model. Falls back to the active model if unresolvable. */
|
|
35
|
+
modelId?: string;
|
|
36
|
+
/** System prompt wrapping the user prompt. */
|
|
37
|
+
systemPrompt?: string;
|
|
38
|
+
/** Max output tokens for the completion (maps to the provider maxTokens). */
|
|
39
|
+
maxOutputTokens?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Result of a one-shot model completion. */
|
|
43
|
+
export interface CompleteResult {
|
|
44
|
+
/** The assistant's text response. */
|
|
45
|
+
text: string;
|
|
46
|
+
/** Token usage, when the provider reports it (for the audit trail). */
|
|
47
|
+
usage?: { input: number; output: number };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Telemetry from a dedicated model call, surfaced in the refine audit entry so
|
|
51
|
+
* hidden model spend is visible (which model, tokens, latency, ok/error). */
|
|
52
|
+
export interface ModelCallTelemetry {
|
|
53
|
+
model?: string;
|
|
54
|
+
inputTokens?: number;
|
|
55
|
+
outputTokens?: number;
|
|
56
|
+
latencyMs?: number;
|
|
57
|
+
ok: boolean;
|
|
58
|
+
error?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
26
61
|
/** Inputs to a proposer. */
|
|
27
62
|
export interface ProposeInput {
|
|
28
63
|
/** Recent trajectory evidence (the same string /refine gathers). */
|
|
@@ -32,6 +67,11 @@ export interface ProposeInput {
|
|
|
32
67
|
state: HarnessState;
|
|
33
68
|
/** Lookback window in turns. */
|
|
34
69
|
lookback: number;
|
|
70
|
+
/** One-shot model completion, injected by runRefine when a model is available.
|
|
71
|
+
* Dedicated-model proposers call this to make a hidden completion; rule-based
|
|
72
|
+
* and steering proposers ignore it. Undefined when no model is resolvable, so
|
|
73
|
+
* a model proposer can no-op (and record an audited failure) rather than throw. */
|
|
74
|
+
complete?: (prompt: string, opts?: CompleteOptions) => Promise<CompleteResult>;
|
|
35
75
|
}
|
|
36
76
|
|
|
37
77
|
/** A single delta plus a human-readable reason for the audit trail. */
|
|
@@ -49,6 +89,10 @@ export interface ProposeResult {
|
|
|
49
89
|
* A proposer MAY return both — deltas are applied first, then steering sent.
|
|
50
90
|
*/
|
|
51
91
|
steeringMessage?: string;
|
|
92
|
+
/** Telemetry from a dedicated model call, recorded in the refine audit entry so
|
|
93
|
+
* hidden model spend is visible. Set by dedicated-model proposers; ignored by
|
|
94
|
+
* rule-based/steering ones. */
|
|
95
|
+
modelCall?: ModelCallTelemetry;
|
|
52
96
|
}
|
|
53
97
|
|
|
54
98
|
/** A strategy for turning evidence + state into (or toward) harness deltas. */
|
package/src/refine.ts
CHANGED
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
// model spend is a tradeoff the roadmap keeps as a separate decision).
|
|
19
19
|
|
|
20
20
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import type { Context, Model, TextContent } from "@earendil-works/pi-ai";
|
|
21
22
|
import { applyDeltas, exportDurable, REFINE_ENTRY, snapshotState } from "./store.js";
|
|
22
23
|
import { getProposer } from "./proposer.js";
|
|
24
|
+
import type { CompleteOptions, CompleteResult } from "./proposer.js";
|
|
23
25
|
|
|
24
26
|
const DEFAULT_EVIDENCE_BYTES = 16000;
|
|
25
27
|
export const DEFAULT_LOOKBACK_TURNS = 25;
|
|
@@ -82,9 +84,17 @@ export async function runRefine(
|
|
|
82
84
|
ctx.ui.setStatus("harness", `Refining (last ${lookback} turns)… [${proposer.name}]`);
|
|
83
85
|
|
|
84
86
|
const evidence = gatherEvidence(ctx, lookback);
|
|
87
|
+
// Inject a one-shot model completion (built from ctx) so dedicated-model
|
|
88
|
+
// proposers can make a hidden LLM call. Undefined when no model is resolvable.
|
|
89
|
+
const complete = buildComplete(ctx);
|
|
85
90
|
// Pass a defensive copy: proposers are a public extension point and must not
|
|
86
91
|
// be able to mutate the live store outside applyDeltas (no audit / no rollback).
|
|
87
|
-
const result = await proposer.propose({
|
|
92
|
+
const result = await proposer.propose({
|
|
93
|
+
evidence,
|
|
94
|
+
state: snapshotState(),
|
|
95
|
+
lookback,
|
|
96
|
+
...(complete ? { complete } : {}),
|
|
97
|
+
});
|
|
88
98
|
const proposedDeltas = result.deltas ?? [];
|
|
89
99
|
|
|
90
100
|
let applied = 0;
|
|
@@ -110,6 +120,9 @@ export async function runRefine(
|
|
|
110
120
|
proposer: proposer.name,
|
|
111
121
|
applied,
|
|
112
122
|
rationales: proposedDeltas.map((d) => d.rationale),
|
|
123
|
+
// Hidden model spend made visible: dedicated-model proposers report what the
|
|
124
|
+
// call cost (model, tokens, latency, ok/error). Absent for rule-based/steering.
|
|
125
|
+
...(result.modelCall ? { modelCall: result.modelCall } : {}),
|
|
113
126
|
});
|
|
114
127
|
|
|
115
128
|
if (commit) {
|
|
@@ -180,3 +193,62 @@ function gatherEvidence(ctx: ExtensionContext, lookback: number): string {
|
|
|
180
193
|
}
|
|
181
194
|
return lines.join("\n") || "(no recent trajectory evidence found)";
|
|
182
195
|
}
|
|
196
|
+
|
|
197
|
+
// ---- one-shot model completion injection (for dedicated-model proposers) -------
|
|
198
|
+
//
|
|
199
|
+
// runRefine builds this closure from ctx.modelRegistry + ctx.model and passes it
|
|
200
|
+
// into ProposeInput.complete. A dedicated-model proposer (shipped as a companion
|
|
201
|
+
// package) calls it to produce deltas directly via a hidden completion; the
|
|
202
|
+
// telemetry it returns is recorded in the refine audit entry so the spend stays
|
|
203
|
+
// visible. Rule-based/steering proposers ignore it.
|
|
204
|
+
|
|
205
|
+
/** Resolve a model id ("provider/id" or a bare id) against the registry, falling
|
|
206
|
+
* back to the active session model. Returns undefined if nothing resolves. */
|
|
207
|
+
function resolveModel(ctx: ExtensionContext, modelId?: string): Model<any> | undefined {
|
|
208
|
+
const registry = ctx.modelRegistry;
|
|
209
|
+
if (modelId && registry) {
|
|
210
|
+
const slash = modelId.indexOf("/");
|
|
211
|
+
if (slash >= 0) {
|
|
212
|
+
const found = registry.find(modelId.slice(0, slash), modelId.slice(slash + 1));
|
|
213
|
+
if (found) return found;
|
|
214
|
+
}
|
|
215
|
+
const byId =
|
|
216
|
+
registry.getAvailable().find((m) => m.id === modelId) ??
|
|
217
|
+
registry.getAll().find((m) => m.id === modelId);
|
|
218
|
+
if (byId) return byId;
|
|
219
|
+
}
|
|
220
|
+
return ctx.model;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Build the one-shot completion closure injected into ProposeInput, or undefined
|
|
224
|
+
* when no model can be resolved (so a model proposer can no-op gracefully).
|
|
225
|
+
* Honors the agent abort signal and a per-call token budget. */
|
|
226
|
+
function buildComplete(
|
|
227
|
+
ctx: ExtensionContext,
|
|
228
|
+
): ((prompt: string, opts?: CompleteOptions) => Promise<CompleteResult>) | undefined {
|
|
229
|
+
const registry = ctx.modelRegistry;
|
|
230
|
+
const active = ctx.model;
|
|
231
|
+
if (!registry || (!active && registry.getAvailable().length === 0)) return undefined;
|
|
232
|
+
|
|
233
|
+
return async (prompt, opts) => {
|
|
234
|
+
const model = resolveModel(ctx, opts?.modelId);
|
|
235
|
+
if (!model) throw new Error("no model available for proposer completion");
|
|
236
|
+
const context: Context = {
|
|
237
|
+
...(opts?.systemPrompt !== undefined ? { systemPrompt: opts.systemPrompt } : {}),
|
|
238
|
+
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
239
|
+
};
|
|
240
|
+
const assistant = await registry.complete(model, context, {
|
|
241
|
+
...(opts?.maxOutputTokens !== undefined ? { maxTokens: opts.maxOutputTokens } : {}),
|
|
242
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
243
|
+
});
|
|
244
|
+
const text = assistant.content
|
|
245
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
246
|
+
.map((c) => c.text)
|
|
247
|
+
.join("");
|
|
248
|
+
const usage = assistant.usage;
|
|
249
|
+
return {
|
|
250
|
+
text,
|
|
251
|
+
...(usage ? { usage: { input: usage.input, output: usage.output } } : {}),
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
}
|