pi-continual-harness 0.17.0 → 0.18.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 +12 -1
- package/dist/auto-refine.d.ts +55 -0
- package/{src/auto-refine.ts → dist/auto-refine.js} +71 -98
- package/dist/cadence.d.ts +14 -0
- package/{src/cadence.ts → dist/cadence.js} +27 -35
- package/dist/classify.d.ts +94 -0
- package/dist/classify.js +145 -0
- package/dist/command-meta.d.ts +2 -0
- package/dist/command-meta.js +9 -0
- package/dist/config.d.ts +143 -0
- package/dist/config.js +296 -0
- package/dist/corpus.d.ts +75 -0
- package/dist/corpus.js +204 -0
- package/dist/durable.d.ts +49 -0
- package/dist/durable.js +228 -0
- package/dist/entry.d.ts +2 -0
- package/dist/entry.js +61 -0
- package/dist/harness.d.ts +32 -0
- package/dist/harness.js +726 -0
- package/dist/hints.d.ts +25 -0
- package/{src/hints.ts → dist/hints.js} +24 -37
- package/dist/index.d.ts +18 -0
- package/dist/index.js +72 -0
- package/dist/inject.d.ts +42 -0
- package/dist/inject.js +152 -0
- package/dist/lifecycle.d.ts +3 -0
- package/dist/lifecycle.js +53 -0
- package/dist/list.d.ts +28 -0
- package/dist/list.js +127 -0
- package/dist/outcome.d.ts +14 -0
- package/{src/outcome.ts → dist/outcome.js} +45 -60
- package/dist/proposer.d.ts +210 -0
- package/dist/proposer.js +334 -0
- package/dist/refine.d.ts +55 -0
- package/dist/refine.js +304 -0
- package/dist/remind.d.ts +61 -0
- package/dist/remind.js +207 -0
- package/dist/schemas.d.ts +18 -0
- package/dist/schemas.js +173 -0
- package/dist/select.d.ts +46 -0
- package/dist/select.js +135 -0
- package/dist/setup.d.ts +61 -0
- package/dist/setup.js +272 -0
- package/dist/status.d.ts +83 -0
- package/dist/status.js +248 -0
- package/dist/store.d.ts +130 -0
- package/dist/store.js +667 -0
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +147 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.js +17 -0
- package/dist/vmodel.d.ts +32 -0
- package/{src/vmodel.ts → dist/vmodel.js} +22 -44
- package/package.json +7 -4
- package/src/classify.ts +0 -220
- package/src/config.ts +0 -402
- package/src/corpus.ts +0 -330
- package/src/durable.ts +0 -253
- package/src/harness.ts +0 -879
- package/src/index.ts +0 -244
- package/src/inject.ts +0 -181
- package/src/list.ts +0 -157
- package/src/proposer.ts +0 -488
- package/src/refine.ts +0 -374
- package/src/remind.ts +0 -267
- package/src/select.ts +0 -189
- package/src/setup.ts +0 -350
- package/src/status.ts +0 -352
- package/src/store.ts +0 -781
- package/src/tools.ts +0 -267
- package/src/types.ts +0 -100
package/README.md
CHANGED
|
@@ -46,7 +46,18 @@ leaving offline and storage to the packages that already do them well.
|
|
|
46
46
|
pi install npm:pi-continual-harness
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
The npm package ships **precompiled** (`dist/`): pi loads a thin entry
|
|
50
|
+
(`dist/entry.js`) that registers hooks, commands, and tool schemas in
|
|
51
|
+
milliseconds, then dynamic-imports each feature module only when its event
|
|
52
|
+
first fires. Measured marginal startup cost: **~30ms** (was ~0.8s when pi
|
|
53
|
+
had to jiti-transpile the whole TypeScript graph every launch — issue #36).
|
|
54
|
+
|
|
55
|
+
From a source checkout instead:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
npm run build
|
|
59
|
+
pi -e ./dist/entry.js
|
|
60
|
+
```
|
|
50
61
|
|
|
51
62
|
## Usage
|
|
52
63
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, TurnEndEvent, TurnEndEventResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type HarnessConfig } from "./config.js";
|
|
3
|
+
export { turnEndedBadly } from "./cadence.js";
|
|
4
|
+
/** Test hook: reset internal counter. */
|
|
5
|
+
export declare function resetAutoRefine(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Decide whether to auto-refine this turn. Side-effectful: the first observed
|
|
8
|
+
* completed turn seeds the baseline (no refine then); every completed turn
|
|
9
|
+
* after it advances the count, and each fired refine resets it (which also
|
|
10
|
+
* prevents the refine turn itself from immediately re-triggering).
|
|
11
|
+
* Aborted/error turns are inert: they neither seed nor advance the count —
|
|
12
|
+
* the cadence counts completed turns of work, never raw turn indices, so an
|
|
13
|
+
* aborted turn in between cannot shorten the window.
|
|
14
|
+
*/
|
|
15
|
+
export declare function evaluateAutoRefine(config: HarnessConfig, outcome?: string): boolean;
|
|
16
|
+
/** The boundary draft custom type for auto-refine steering messages. */
|
|
17
|
+
export declare const AUTO_REFINE_ENTRY = "harness.auto-refine-request";
|
|
18
|
+
/** Build the boundary draft carrying a steering message. Pure — testable. */
|
|
19
|
+
export declare function buildRefineDraft(steeringMessage: string): {
|
|
20
|
+
type: "custom_message";
|
|
21
|
+
customType: string;
|
|
22
|
+
content: string;
|
|
23
|
+
display: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** Structural slice of pi's ProjectedSessionEntry (sourceEntry + messages). */
|
|
26
|
+
export interface ProjectedLike {
|
|
27
|
+
sourceEntry?: {
|
|
28
|
+
type?: string;
|
|
29
|
+
customType?: string;
|
|
30
|
+
id?: string;
|
|
31
|
+
};
|
|
32
|
+
messages?: unknown[];
|
|
33
|
+
}
|
|
34
|
+
/** A context_edit boundary draft (pi ≥ 0.87): omit one entry from future
|
|
35
|
+
* provider context — raw history, usage, and UI history stay untouched. */
|
|
36
|
+
export interface OmissionDraft {
|
|
37
|
+
type: "context_edit";
|
|
38
|
+
targetId: string;
|
|
39
|
+
replacement: null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Context-edit drafts omitting every LIVE prior auto-refine request from
|
|
43
|
+
* future provider context (pi ≥ 0.87 ContextEditEntry). Invariant: at most
|
|
44
|
+
* ONE auto-refine request is ever model-visible — the newest. Already-omitted
|
|
45
|
+
* drafts surface in the projection with empty `messages` and are skipped, so
|
|
46
|
+
* the operation is idempotent and never stacks redundant edits. Raw history
|
|
47
|
+
* (and the HTML export) keeps every draft: /tree rollback still applies.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildOmissionDrafts(projected: Iterable<ProjectedLike>): OmissionDraft[];
|
|
50
|
+
/** turn_end handler — run /refine on the configured cadence. Returns the
|
|
51
|
+
* boundary continuation when a steering draft lands (pi ≥ 0.87). Exported
|
|
52
|
+
* so the thin entry (entry.ts) can delegate via dynamic import (issue #36). */
|
|
53
|
+
export declare function handleAutoRefineTurnEnd(pi: ExtensionAPI, event: TurnEndEvent, ctx: ExtensionContext): Promise<TurnEndEventResult | void>;
|
|
54
|
+
/** Subscribe to turn_end and run /refine on the configured cadence. */
|
|
55
|
+
export declare function registerAutoRefine(pi: ExtensionAPI): void;
|
|
@@ -34,25 +34,19 @@
|
|
|
34
34
|
// It stays visible: it notifies before firing, and the boundary draft (and the
|
|
35
35
|
// gate decision) appear in the transcript. The decision logic is factored into
|
|
36
36
|
// evaluateAutoRefine() so it is unit-testable without a live pi runtime.
|
|
37
|
-
|
|
38
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
39
|
-
import { DEFAULT_AUTO_EVERY_TURNS, type HarnessConfig, loadConfig } from "./config.js";
|
|
37
|
+
import { DEFAULT_AUTO_EVERY_TURNS, loadConfig } from "./config.js";
|
|
40
38
|
import { runRefine, gatherEvidence, DEFAULT_LOOKBACK_TURNS } from "./refine.js";
|
|
41
39
|
import { buildClassify, buildGateRequest, gateDecision } from "./classify.js";
|
|
42
40
|
import { makeTurnCadence } from "./cadence.js";
|
|
43
|
-
|
|
44
41
|
// turnEndedBadly's canonical home is cadence.ts (shared completed-turn
|
|
45
42
|
// semantics); re-exported here so existing imports keep working.
|
|
46
43
|
export { turnEndedBadly } from "./cadence.js";
|
|
47
|
-
|
|
48
44
|
// The auto-refine cadence counter (shared implementation — see cadence.ts).
|
|
49
45
|
const cadence = makeTurnCadence();
|
|
50
|
-
|
|
51
46
|
/** Test hook: reset internal counter. */
|
|
52
|
-
export function resetAutoRefine()
|
|
53
|
-
|
|
47
|
+
export function resetAutoRefine() {
|
|
48
|
+
cadence.reset();
|
|
54
49
|
}
|
|
55
|
-
|
|
56
50
|
/**
|
|
57
51
|
* Decide whether to auto-refine this turn. Side-effectful: the first observed
|
|
58
52
|
* completed turn seeds the baseline (no refine then); every completed turn
|
|
@@ -62,39 +56,18 @@ export function resetAutoRefine(): void {
|
|
|
62
56
|
* the cadence counts completed turns of work, never raw turn indices, so an
|
|
63
57
|
* aborted turn in between cannot shorten the window.
|
|
64
58
|
*/
|
|
65
|
-
export function evaluateAutoRefine(config
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
export function evaluateAutoRefine(config, outcome) {
|
|
60
|
+
if (!config.autoRefine?.enabled)
|
|
61
|
+
return false;
|
|
62
|
+
const every = config.autoRefine.everyTurns ?? DEFAULT_AUTO_EVERY_TURNS;
|
|
63
|
+
return cadence.tick(every, outcome);
|
|
69
64
|
}
|
|
70
|
-
|
|
71
65
|
/** The boundary draft custom type for auto-refine steering messages. */
|
|
72
66
|
export const AUTO_REFINE_ENTRY = "harness.auto-refine-request";
|
|
73
|
-
|
|
74
67
|
/** Build the boundary draft carrying a steering message. Pure — testable. */
|
|
75
|
-
export function buildRefineDraft(steeringMessage
|
|
76
|
-
|
|
77
|
-
customType: string;
|
|
78
|
-
content: string;
|
|
79
|
-
display: boolean;
|
|
80
|
-
} {
|
|
81
|
-
return { type: "custom_message", customType: AUTO_REFINE_ENTRY, content: steeringMessage, display: true };
|
|
68
|
+
export function buildRefineDraft(steeringMessage) {
|
|
69
|
+
return { type: "custom_message", customType: AUTO_REFINE_ENTRY, content: steeringMessage, display: true };
|
|
82
70
|
}
|
|
83
|
-
|
|
84
|
-
/** Structural slice of pi's ProjectedSessionEntry (sourceEntry + messages). */
|
|
85
|
-
export interface ProjectedLike {
|
|
86
|
-
sourceEntry?: { type?: string; customType?: string; id?: string };
|
|
87
|
-
messages?: unknown[];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** A context_edit boundary draft (pi ≥ 0.87): omit one entry from future
|
|
91
|
-
* provider context — raw history, usage, and UI history stay untouched. */
|
|
92
|
-
export interface OmissionDraft {
|
|
93
|
-
type: "context_edit";
|
|
94
|
-
targetId: string;
|
|
95
|
-
replacement: null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
71
|
/**
|
|
99
72
|
* Context-edit drafts omitting every LIVE prior auto-refine request from
|
|
100
73
|
* future provider context (pi ≥ 0.87 ContextEditEntry). Invariant: at most
|
|
@@ -103,76 +76,76 @@ export interface OmissionDraft {
|
|
|
103
76
|
* the operation is idempotent and never stacks redundant edits. Raw history
|
|
104
77
|
* (and the HTML export) keeps every draft: /tree rollback still applies.
|
|
105
78
|
*/
|
|
106
|
-
export function buildOmissionDrafts(projected
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
79
|
+
export function buildOmissionDrafts(projected) {
|
|
80
|
+
const drafts = [];
|
|
81
|
+
for (const p of projected) {
|
|
82
|
+
const src = p.sourceEntry;
|
|
83
|
+
if (src?.type !== "custom_message" || src.customType !== AUTO_REFINE_ENTRY)
|
|
84
|
+
continue;
|
|
85
|
+
if ((p.messages?.length ?? 0) === 0)
|
|
86
|
+
continue; // already omitted → skip
|
|
87
|
+
if (typeof src.id === "string")
|
|
88
|
+
drafts.push({ type: "context_edit", targetId: src.id, replacement: null });
|
|
89
|
+
}
|
|
90
|
+
return drafts;
|
|
115
91
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
92
|
+
/** turn_end handler — run /refine on the configured cadence. Returns the
|
|
93
|
+
* boundary continuation when a steering draft lands (pi ≥ 0.87). Exported
|
|
94
|
+
* so the thin entry (entry.ts) can delegate via dynamic import (issue #36). */
|
|
95
|
+
export async function handleAutoRefineTurnEnd(pi, event, ctx) {
|
|
120
96
|
const config = await loadConfig();
|
|
121
|
-
if (!evaluateAutoRefine(config, event.outcome))
|
|
97
|
+
if (!evaluateAutoRefine(config, event.outcome))
|
|
98
|
+
return;
|
|
122
99
|
const every = config.autoRefine?.everyTurns ?? DEFAULT_AUTO_EVERY_TURNS;
|
|
123
|
-
|
|
124
100
|
// Opt-in classifier gate: cheap yes/no before spending the refine.
|
|
125
101
|
if (config.autoRefine?.gate) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
102
|
+
const classify = buildClassify(ctx, config.classifier?.model);
|
|
103
|
+
if (classify) {
|
|
104
|
+
const evidence = gatherEvidence(ctx, DEFAULT_LOOKBACK_TURNS);
|
|
105
|
+
const res = await classify(buildGateRequest(evidence));
|
|
106
|
+
const decision = gateDecision(res);
|
|
107
|
+
if (!decision.proceed) {
|
|
108
|
+
ctx.ui.notify(`Auto-refine: skipped — ${decision.because}.`, "info");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
ctx.ui.notify(`Auto-refine: gate passed (${decision.because}).`, "info");
|
|
134
112
|
}
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
// No resolvable classifier → plain cadence behavior (documented soft-fail).
|
|
113
|
+
// No resolvable classifier → plain cadence behavior (documented soft-fail).
|
|
138
114
|
}
|
|
139
|
-
|
|
140
115
|
ctx.ui.notify(`Auto-refine: running /refine (every ${every} turns, opt-in).`, "info");
|
|
141
116
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
117
|
+
const result = await runRefine(pi, ctx, {
|
|
118
|
+
commit: config.autoRefine?.commit ?? false,
|
|
119
|
+
...(config.proposer ? { proposer: config.proposer } : {}),
|
|
120
|
+
delivery: "boundary",
|
|
121
|
+
}, "auto");
|
|
122
|
+
// Boundary delivery: persist the steering message as a structural entry
|
|
123
|
+
// and guarantee one next provider request. Only when the boundary CAN
|
|
124
|
+
// continue — otherwise fall back to the legacy steering send.
|
|
125
|
+
if (result.steeringMessage) {
|
|
126
|
+
if (event.context?.canContinue === false) {
|
|
127
|
+
pi.sendUserMessage(result.steeringMessage);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Hygiene (pi ≥ 0.87 context edits): omit every LIVE prior auto-refine
|
|
131
|
+
// request from future provider context, so at most one draft (the new
|
|
132
|
+
// one) is ever model-visible. Already-omitted drafts are skipped by
|
|
133
|
+
// buildOmissionDrafts; raw history and /tree rollback are untouched.
|
|
134
|
+
// Kill-switch: autoRefine.omitStaleDrafts: false.
|
|
135
|
+
const omissions = config.autoRefine?.omitStaleDrafts === false
|
|
136
|
+
? []
|
|
137
|
+
: buildOmissionDrafts(event.context?.contextEntries ?? []);
|
|
138
|
+
return {
|
|
139
|
+
entries: [...event.entries, ...omissions, buildRefineDraft(result.steeringMessage)],
|
|
140
|
+
continue: true,
|
|
141
|
+
};
|
|
159
142
|
}
|
|
160
|
-
// Hygiene (pi ≥ 0.87 context edits): omit every LIVE prior auto-refine
|
|
161
|
-
// request from future provider context, so at most one draft (the new
|
|
162
|
-
// one) is ever model-visible. Already-omitted drafts are skipped by
|
|
163
|
-
// buildOmissionDrafts; raw history and /tree rollback are untouched.
|
|
164
|
-
// Kill-switch: autoRefine.omitStaleDrafts: false.
|
|
165
|
-
const omissions =
|
|
166
|
-
config.autoRefine?.omitStaleDrafts === false
|
|
167
|
-
? []
|
|
168
|
-
: buildOmissionDrafts(event.context?.contextEntries ?? []);
|
|
169
|
-
return {
|
|
170
|
-
entries: [...event.entries, ...omissions, buildRefineDraft(result.steeringMessage)],
|
|
171
|
-
continue: true,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
} catch (err) {
|
|
175
|
-
ctx.ui.notify(`Auto-refine failed: ${(err as Error).message}`, "error");
|
|
176
143
|
}
|
|
177
|
-
|
|
144
|
+
catch (err) {
|
|
145
|
+
ctx.ui.notify(`Auto-refine failed: ${err.message}`, "error");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Subscribe to turn_end and run /refine on the configured cadence. */
|
|
149
|
+
export function registerAutoRefine(pi) {
|
|
150
|
+
pi.on("turn_end", (event, ctx) => handleAutoRefineTurnEnd(pi, event, ctx));
|
|
178
151
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** True when a turn_end's outcome marks a turn that ended badly (pi's
|
|
2
|
+
* BoundaryState.outcome: "aborted" | "error"). Undefined is treated as
|
|
3
|
+
* completed — current pi always reports the field, but synthetic events and
|
|
4
|
+
* the earliest 0.99.x may omit it. */
|
|
5
|
+
export declare function turnEndedBadly(outcome?: string): boolean;
|
|
6
|
+
export interface TurnCadence {
|
|
7
|
+
/** Observe a turn boundary; true when the cadence fires this turn.
|
|
8
|
+
* Aborted/error outcomes and every <= 0 are inert (state untouched). */
|
|
9
|
+
tick(every: number, outcome?: string): boolean;
|
|
10
|
+
/** Back to the unseen baseline (session_start: a fork/resume opens a fresh window). */
|
|
11
|
+
reset(): void;
|
|
12
|
+
}
|
|
13
|
+
/** One independent cadence counter. Feature gates stay with the caller. */
|
|
14
|
+
export declare function makeTurnCadence(): TurnCadence;
|
|
@@ -12,45 +12,37 @@
|
|
|
12
12
|
// - The seed turn itself does not count.
|
|
13
13
|
// - every <= 0 disables the cadence (tick never fires).
|
|
14
14
|
// - Firing resets the count, so the firing turn itself never re-triggers.
|
|
15
|
-
|
|
16
15
|
/** True when a turn_end's outcome marks a turn that ended badly (pi's
|
|
17
16
|
* BoundaryState.outcome: "aborted" | "error"). Undefined is treated as
|
|
18
17
|
* completed — current pi always reports the field, but synthetic events and
|
|
19
18
|
* the earliest 0.99.x may omit it. */
|
|
20
|
-
export function turnEndedBadly(outcome
|
|
21
|
-
|
|
19
|
+
export function turnEndedBadly(outcome) {
|
|
20
|
+
return outcome === "aborted" || outcome === "error";
|
|
22
21
|
}
|
|
23
|
-
|
|
24
|
-
export interface TurnCadence {
|
|
25
|
-
/** Observe a turn boundary; true when the cadence fires this turn.
|
|
26
|
-
* Aborted/error outcomes and every <= 0 are inert (state untouched). */
|
|
27
|
-
tick(every: number, outcome?: string): boolean;
|
|
28
|
-
/** Back to the unseen baseline (session_start: a fork/resume opens a fresh window). */
|
|
29
|
-
reset(): void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
22
|
/** One independent cadence counter. Feature gates stay with the caller. */
|
|
33
|
-
export function makeTurnCadence()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
23
|
+
export function makeTurnCadence() {
|
|
24
|
+
// Completed turns observed since the last fire (or the seeded baseline).
|
|
25
|
+
// -1 = unseen.
|
|
26
|
+
let completedTurns = -1;
|
|
27
|
+
return {
|
|
28
|
+
tick(every, outcome) {
|
|
29
|
+
if (turnEndedBadly(outcome))
|
|
30
|
+
return false;
|
|
31
|
+
if (every <= 0)
|
|
32
|
+
return false;
|
|
33
|
+
if (completedTurns < 0) {
|
|
34
|
+
completedTurns = 0; // seed: the seed turn itself doesn't count
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
completedTurns += 1;
|
|
38
|
+
if (completedTurns >= every) {
|
|
39
|
+
completedTurns = 0;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
},
|
|
44
|
+
reset() {
|
|
45
|
+
completedTurns = -1;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
56
48
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/** One yes/no question in a ClassifyFn request. */
|
|
3
|
+
export interface ClassifyBoolQuestion {
|
|
4
|
+
instructions: string;
|
|
5
|
+
/** Criteria text for the yes answer. */
|
|
6
|
+
trueCriteria: string;
|
|
7
|
+
/** Criteria text for the no answer. */
|
|
8
|
+
falseCriteria: string;
|
|
9
|
+
}
|
|
10
|
+
/** A parsed answer per question key. */
|
|
11
|
+
export interface ClassifyBoolAnswer {
|
|
12
|
+
/** The classifier's decision. */
|
|
13
|
+
value: boolean;
|
|
14
|
+
/** [0,1] confidence when the engine reports one. */
|
|
15
|
+
confidence?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Result of a ClassifyFn call: one answer per question key, or an error. */
|
|
18
|
+
export interface ClassifyBoolResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
answers: Record<string, ClassifyBoolAnswer>;
|
|
21
|
+
/** Resolved classifier label ("provider/id") for telemetry. */
|
|
22
|
+
model?: string;
|
|
23
|
+
/** Token usage when the engine reports it (for the audit trail). */
|
|
24
|
+
usage?: {
|
|
25
|
+
input: number;
|
|
26
|
+
output: number;
|
|
27
|
+
};
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Batched yes/no classification seam injected into ProposeInput when a
|
|
32
|
+
* classifier model is configured AND resolvable. Structural on purpose —
|
|
33
|
+
* the adapter below is the only place that knows pi-ai's shapes.
|
|
34
|
+
*/
|
|
35
|
+
export type ClassifyFn = (input: {
|
|
36
|
+
state: Record<string, unknown>;
|
|
37
|
+
questions: Record<string, ClassifyBoolQuestion>;
|
|
38
|
+
}) => Promise<ClassifyBoolResult>;
|
|
39
|
+
/** Structural slice of ctx.modelRegistry used here. */
|
|
40
|
+
interface RegistryLike {
|
|
41
|
+
classify?: (model: unknown, context: unknown, options?: unknown) => Promise<ClassifyRawResult>;
|
|
42
|
+
findOfType?: (type: "classifier", provider: string, id: string) => unknown | undefined;
|
|
43
|
+
getModelsOfType?: (type: "classifier") => readonly unknown[];
|
|
44
|
+
}
|
|
45
|
+
/** Raw pi-ai ClassifierResult slice (never rejects, but stopReason may be error). */
|
|
46
|
+
interface ClassifyRawResult {
|
|
47
|
+
answers?: Record<string, {
|
|
48
|
+
type?: string;
|
|
49
|
+
choice?: unknown;
|
|
50
|
+
score?: number;
|
|
51
|
+
probability?: number;
|
|
52
|
+
confidence?: number;
|
|
53
|
+
}>;
|
|
54
|
+
stopReason?: string;
|
|
55
|
+
errorMessage?: string;
|
|
56
|
+
}
|
|
57
|
+
/** Raw classifier-model slice (Model<ClassifierApi>). */
|
|
58
|
+
interface ClassifierModelLike {
|
|
59
|
+
provider?: string;
|
|
60
|
+
id?: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a classifier model from a configured id ("provider/id" or bare id).
|
|
64
|
+
* Returns undefined when unconfigured or unresolvable — every feature keyed on
|
|
65
|
+
* it then degrades to its no-classifier behavior.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveClassifierModel(registry: RegistryLike | undefined, configured?: string): ClassifierModelLike | undefined;
|
|
68
|
+
/** Build the ClassifyFn adapter over a resolved classifier model. */
|
|
69
|
+
export declare function makeClassifyFn(registry: RegistryLike, model: ClassifierModelLike, signal?: AbortSignal): ClassifyFn;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the ClassifyFn for a session from config + ctx, or undefined.
|
|
72
|
+
* The single construction point shared by refine (ProposeInput.classify) and
|
|
73
|
+
* the auto-refine gate.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildClassify(ctx: ExtensionContext, configuredModel?: string): ClassifyFn | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* The auto-refine gate question: did the recent trajectory contain a durable,
|
|
78
|
+
* reusable correction worth a /refine pass? Pure builder — unit-testable.
|
|
79
|
+
* Keeps the state payload bounded (the caller already bounds evidence bytes).
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildGateRequest(evidence: string): {
|
|
82
|
+
state: Record<string, unknown>;
|
|
83
|
+
questions: Record<string, ClassifyBoolQuestion>;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Gate decision from a classify result. Errors and missing answers fall back to
|
|
87
|
+
* `true` (proceed) so a flaky classifier never disables refinement — the gate
|
|
88
|
+
* can only SKIP work, never block it.
|
|
89
|
+
*/
|
|
90
|
+
export declare function gateDecision(result: ClassifyBoolResult): {
|
|
91
|
+
proceed: boolean;
|
|
92
|
+
because: string;
|
|
93
|
+
};
|
|
94
|
+
export {};
|
package/dist/classify.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Classifier adapter (pi ≥ 0.99). Wraps ctx.modelRegistry.classify() — cheap
|
|
2
|
+
// typed yes/no or score questions answered from next-token label probabilities
|
|
3
|
+
// (local llama.cpp) or a hosted Jev model — behind two package seams:
|
|
4
|
+
//
|
|
5
|
+
// 1. autoRefine gate (auto-refine.ts): when the cadence elapses, ask ONE bool
|
|
6
|
+
// question over the recent trajectory ("did it contain a durable, reusable
|
|
7
|
+
// correction?") and skip the refine when the answer is no. Soft-fails to the
|
|
8
|
+
// old cadence behavior on any classifier error, so a flaky classifier never
|
|
9
|
+
// blocks refinement.
|
|
10
|
+
// 2. dedupe confirmation (proposer.ts): a precision filter on candidate merge
|
|
11
|
+
// pairs — see classifyConfirmPairs().
|
|
12
|
+
//
|
|
13
|
+
// The pi-ai classifier shapes (ClassifierContext/ClassifierQuestion/…) are kept
|
|
14
|
+
// OUT of the public proposer interface: ClassifyFn below is a minimal structural
|
|
15
|
+
// contract so proposers stay testable without pi runtime types.
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a classifier model from a configured id ("provider/id" or bare id).
|
|
18
|
+
* Returns undefined when unconfigured or unresolvable — every feature keyed on
|
|
19
|
+
* it then degrades to its no-classifier behavior.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveClassifierModel(registry, configured) {
|
|
22
|
+
if (!configured || !registry)
|
|
23
|
+
return undefined;
|
|
24
|
+
const slash = configured.indexOf("/");
|
|
25
|
+
if (slash > 0 && typeof registry.findOfType === "function") {
|
|
26
|
+
const byPair = registry.findOfType("classifier", configured.slice(0, slash), configured.slice(slash + 1));
|
|
27
|
+
if (byPair)
|
|
28
|
+
return byPair;
|
|
29
|
+
}
|
|
30
|
+
const all = typeof registry.getModelsOfType === "function"
|
|
31
|
+
? [...registry.getModelsOfType("classifier")]
|
|
32
|
+
: [];
|
|
33
|
+
return (all.find((m) => {
|
|
34
|
+
const cm = m;
|
|
35
|
+
return cm.id === configured || `${cm.provider}/${cm.id}` === configured;
|
|
36
|
+
}) ?? undefined);
|
|
37
|
+
}
|
|
38
|
+
/** Build the ClassifyFn adapter over a resolved classifier model. */
|
|
39
|
+
export function makeClassifyFn(registry, model, signal) {
|
|
40
|
+
const label = `${model.provider}/${model.id}`;
|
|
41
|
+
return async ({ state, questions }) => {
|
|
42
|
+
if (typeof registry.classify !== "function") {
|
|
43
|
+
return { ok: false, answers: {}, model: label, error: "modelRegistry.classify unavailable" };
|
|
44
|
+
}
|
|
45
|
+
// Map our minimal bool contract onto pi-ai's ClassifierBoolQuestion.
|
|
46
|
+
const piQuestions = {};
|
|
47
|
+
for (const [key, q] of Object.entries(questions)) {
|
|
48
|
+
piQuestions[key] = {
|
|
49
|
+
type: "bool",
|
|
50
|
+
instructions: q.instructions,
|
|
51
|
+
criteria: { true: q.trueCriteria, false: q.falseCriteria },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
let raw;
|
|
55
|
+
try {
|
|
56
|
+
raw = (await registry.classify(model, { state, questions: piQuestions }, signal ? { signal } : undefined));
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
// classify "never rejects", but stay defensive around transport edges.
|
|
60
|
+
return { ok: false, answers: {}, model: label, error: err.message };
|
|
61
|
+
}
|
|
62
|
+
const usage = typeof raw.usage?.input === "number" &&
|
|
63
|
+
typeof raw.usage?.output === "number"
|
|
64
|
+
? {
|
|
65
|
+
input: raw.usage.input,
|
|
66
|
+
output: raw.usage.output,
|
|
67
|
+
}
|
|
68
|
+
: undefined;
|
|
69
|
+
if (raw.stopReason && raw.stopReason !== "stop") {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
answers: {},
|
|
73
|
+
model: label,
|
|
74
|
+
...(usage ? { usage } : {}),
|
|
75
|
+
error: raw.errorMessage ?? `stopReason ${raw.stopReason}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const answers = {};
|
|
79
|
+
for (const [key, a] of Object.entries(raw.answers ?? {})) {
|
|
80
|
+
let value;
|
|
81
|
+
let confidence;
|
|
82
|
+
if (a?.type === "bool") {
|
|
83
|
+
// Bool answers carry the probability of TRUE.
|
|
84
|
+
value = (a.probability ?? 0) >= 0.5;
|
|
85
|
+
confidence = a.probability;
|
|
86
|
+
}
|
|
87
|
+
else if (a?.type === "choice") {
|
|
88
|
+
value = a.choice === true || a.choice === "true";
|
|
89
|
+
confidence = a.confidence;
|
|
90
|
+
}
|
|
91
|
+
else if (a?.type === "score") {
|
|
92
|
+
value = (a.score ?? 0) >= 0.5;
|
|
93
|
+
confidence = a.confidence;
|
|
94
|
+
}
|
|
95
|
+
if (value !== undefined) {
|
|
96
|
+
answers[key] = { value, ...(typeof confidence === "number" ? { confidence } : {}) };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, answers, model: label, ...(usage ? { usage } : {}) };
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the ClassifyFn for a session from config + ctx, or undefined.
|
|
104
|
+
* The single construction point shared by refine (ProposeInput.classify) and
|
|
105
|
+
* the auto-refine gate.
|
|
106
|
+
*/
|
|
107
|
+
export function buildClassify(ctx, configuredModel) {
|
|
108
|
+
const registry = ctx.modelRegistry;
|
|
109
|
+
const model = resolveClassifierModel(registry, configuredModel);
|
|
110
|
+
if (!registry || !model)
|
|
111
|
+
return undefined;
|
|
112
|
+
return makeClassifyFn(registry, model, ctx.signal);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The auto-refine gate question: did the recent trajectory contain a durable,
|
|
116
|
+
* reusable correction worth a /refine pass? Pure builder — unit-testable.
|
|
117
|
+
* Keeps the state payload bounded (the caller already bounds evidence bytes).
|
|
118
|
+
*/
|
|
119
|
+
export function buildGateRequest(evidence) {
|
|
120
|
+
return {
|
|
121
|
+
state: { trajectory: evidence },
|
|
122
|
+
questions: {
|
|
123
|
+
gate: {
|
|
124
|
+
instructions: "You are gating an online self-improvement pass for a coding agent. Does the recent trajectory below contain a durable, reusable correction — a mistake fixed, a preference learned, a convention discovered, or a fact worth remembering across sessions — that is NOT already routine work?",
|
|
125
|
+
trueCriteria: "The trajectory contains at least one durable, reusable lesson or correction worth persisting into the agent's harness state.",
|
|
126
|
+
falseCriteria: "The trajectory is routine work (ordinary edits, reads, test runs) with no reusable correction worth persisting.",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Gate decision from a classify result. Errors and missing answers fall back to
|
|
133
|
+
* `true` (proceed) so a flaky classifier never disables refinement — the gate
|
|
134
|
+
* can only SKIP work, never block it.
|
|
135
|
+
*/
|
|
136
|
+
export function gateDecision(result) {
|
|
137
|
+
if (!result.ok)
|
|
138
|
+
return { proceed: true, because: `classifier error (${result.error ?? "unknown"}); falling back to cadence` };
|
|
139
|
+
const a = result.answers.gate;
|
|
140
|
+
if (!a)
|
|
141
|
+
return { proceed: true, because: "no gate answer; falling back to cadence" };
|
|
142
|
+
return a.value
|
|
143
|
+
? { proceed: true, because: `classifier: yes (confidence ${a.confidence ?? "?"})` }
|
|
144
|
+
: { proceed: false, because: `classifier: no durable correction (confidence ${a.confidence ?? "?"})` };
|
|
145
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const REFINE_COMMAND_DESCRIPTION = "Online self-improvement: review recent trajectory and propose evidence-backed CRUD deltas to the harness state. Usage: /refine [lookback-turns] [--commit] [--proposer steering|dedupe] [--threshold 0.75]";
|
|
2
|
+
export declare const HARNESS_COMMAND_DESCRIPTION: string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Command registration metadata shared by the thin entry (entry.ts) and the
|
|
2
|
+
// feature modules' own register functions (refine.ts, harness.ts). Registration
|
|
3
|
+
// must be synchronous at factory time, so the description strings cannot live
|
|
4
|
+
// behind a dynamic import — but they CAN live in this tiny data module both
|
|
5
|
+
// paths share, which keeps them from drifting apart (issue #36).
|
|
6
|
+
export const REFINE_COMMAND_DESCRIPTION = "Online self-improvement: review recent trajectory and propose evidence-backed CRUD deltas to the harness state. Usage: /refine [lookback-turns] [--commit] [--proposer steering|dedupe] [--threshold 0.75]";
|
|
7
|
+
export const HARNESS_COMMAND_DESCRIPTION = "Durable harness-state I/O + importance hygiene " +
|
|
8
|
+
"(import · export · status · adopt · prune · keep · drop · push-mem — " +
|
|
9
|
+
"subcommands autocomplete as you type)";
|