portable-agent-layer 0.108.0 → 0.109.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 +2 -1
- package/package.json +6 -2
- package/src/cli/correction.ts +115 -0
- package/src/cli/rule.ts +121 -14
- package/src/cli/tree.ts +2 -0
- package/src/hooks/handlers/rating.ts +21 -2
- package/src/hooks/handlers/update-check.ts +6 -1
- package/src/hooks/lib/adaptation-candidates.ts +41 -13
- package/src/hooks/lib/adaptation-rules.ts +138 -9
- package/src/hooks/lib/adaptation-steering.ts +7 -2
- package/src/hooks/lib/adaptation-turns.ts +43 -1
- package/src/hooks/lib/context.ts +2 -2
- package/src/hooks/lib/correction-backfill.ts +56 -0
- package/src/hooks/lib/correction-kind.ts +165 -0
- package/src/hooks/lib/correction-rates.ts +151 -0
- package/src/hooks/lib/models.ts +7 -3
- package/src/hooks/lib/opinion-gate.ts +73 -0
- package/src/hooks/lib/rule-drafter.ts +57 -24
- package/src/hooks/lib/semi-static.ts +2 -2
- package/src/tools/control-room/relationship.ts +43 -18
- package/src/tools/control-room/server.ts +12 -5
- package/src/tools/control-room/ui/dist/assets/{index-mFpXpiBj.js → index-D0pEd7Ln.js} +1 -1
- package/src/tools/control-room/ui/dist/index.html +1 -1
- package/src/tools/control-room/ui/lib/write.ts +3 -1
- package/src/tools/control-room/ui/screens/relationship.tsx +101 -29
package/README.md
CHANGED
|
@@ -97,7 +97,8 @@ pal cli status # check your setup
|
|
|
97
97
|
| `pal cli timezone [<zone>]` | Show or set your timezone. IANA names only, validated before it is stored |
|
|
98
98
|
| `pal cli knowledge` | Query & manage the knowledge store (search, graph, stats, hubs, find, show, add, ls, ingest) |
|
|
99
99
|
| `pal cli ledger` | Query the action ledger — `log`, `show <id>`, `stats`, filtered by `--project`, `--since`, `--actor`, `--machine`, `--runtime`, `--outcome`, `--tool`, `--target` |
|
|
100
|
-
| `pal cli rule` | Review the rules drafted from your corrections — `list [--all] [--json]`, `approve <id>`, `deny <id
|
|
100
|
+
| `pal cli rule` | Review the rules drafted from your corrections — `list [--all] [--json]`, `approve <id>`, `opinion <id>`, `deny <id>`, `retire <id>`, `add-opinion --kind --when --steering`. A draft steers nothing until approved; a denied one is never drafted again. An opinion has no trigger and steers every session, at most 8 of them and one per correction kind; `list --all` shows each one's correction-kind rate before and after approval |
|
|
101
|
+
| `pal cli correction` | The kinds of mistakes you corrected — `kinds [--days N] [--split DATE] [--json]` shows each kind per 100 turns, before and after a date with `--split`; `backfill` labels confirmed corrections without a kind or with one from an older prompt, and `--relabel` labels every one again |
|
|
101
102
|
| `pal cli server` | The morning screen: a local page to open before a terminal — three moves for today, an urgent/important grid over every project *and* every stated goal, where you left off, and a drawer holding the signal and the action ledger — `start [--port <n>]`, `stop`, `status`. Loopback only, default port 7250. No model runs on page load |
|
|
102
103
|
| `pal cli skill link <name>` | Link a personal `~/.pal/skills/<name>/` into every installed agent so it is discoverable |
|
|
103
104
|
| `pal cli skill doctor <name>` | Evaluate a skill against the authoring best practices (folder/file-name match, name, description, body length, point-of-view, reference depth) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.109.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -70,7 +70,11 @@
|
|
|
70
70
|
"eval:waiting-on": "bun eval/run.ts waiting-on",
|
|
71
71
|
"eval:reaction": "bun eval/run.ts reaction",
|
|
72
72
|
"eval:correction-check": "bun eval/run.ts correction-check",
|
|
73
|
-
"eval:entity-extraction": "bun eval/entity-extraction/build-prompt.ts && bun eval/run.ts entity-extraction"
|
|
73
|
+
"eval:entity-extraction": "bun eval/entity-extraction/build-prompt.ts && bun eval/run.ts entity-extraction",
|
|
74
|
+
"eval:correction-kind": "bun eval/correction-kind/build-prompt.ts && bun eval/run.ts correction-kind",
|
|
75
|
+
"eval:correction-kind:test": "bun eval/correction-kind/test-set.ts",
|
|
76
|
+
"eval:rule-drafter": "bun eval/rule-drafter/build-prompt.ts && bun eval/run.ts rule-drafter",
|
|
77
|
+
"eval:rule-drafter:test": "bun eval/rule-drafter/test-set.ts"
|
|
74
78
|
},
|
|
75
79
|
"lint-staged": {
|
|
76
80
|
"*.ts": [
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pal cli correction — the kinds of mistakes the user corrected, and how often.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* kinds [--days N] [--split DATE] [--json] Corrections per 100 turns, by kind
|
|
6
|
+
* backfill [--relabel] Label the confirmed corrections without a current kind
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readTurns } from "../hooks/lib/adaptation-turns";
|
|
10
|
+
import {
|
|
11
|
+
backfillCorrectionKinds,
|
|
12
|
+
type KindLabeller,
|
|
13
|
+
labelWithInference,
|
|
14
|
+
} from "../hooks/lib/correction-backfill";
|
|
15
|
+
import { type KindRates, kindRates, splitRates } from "../hooks/lib/correction-rates";
|
|
16
|
+
import { group, leaf, runCommand, UsageError } from "../tools/lib/command";
|
|
17
|
+
|
|
18
|
+
function correctionCommandFor(label: KindLabeller) {
|
|
19
|
+
return group({
|
|
20
|
+
summary: "The kinds of mistakes you corrected, and how often",
|
|
21
|
+
commands: {
|
|
22
|
+
kinds: leaf({
|
|
23
|
+
summary: "Corrections per 100 turns, by kind",
|
|
24
|
+
options: {
|
|
25
|
+
days: { type: "string", description: "Window in days (default 30)" },
|
|
26
|
+
split: { type: "string", description: "Compare before and after this date" },
|
|
27
|
+
json: { type: "boolean", description: "Machine-readable output" },
|
|
28
|
+
},
|
|
29
|
+
run: ({ values }) => cmdKinds(values.days, values.split, values.json === true),
|
|
30
|
+
}),
|
|
31
|
+
backfill: leaf({
|
|
32
|
+
summary: "Label the confirmed corrections without a kind of the current version",
|
|
33
|
+
options: {
|
|
34
|
+
relabel: {
|
|
35
|
+
type: "boolean",
|
|
36
|
+
description: "Label every confirmed correction again, after a prompt change",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
run: ({ values }) => cmdBackfill(label, values.relabel === true),
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const correctionCommand = correctionCommandFor(labelWithInference);
|
|
46
|
+
|
|
47
|
+
export function runCorrection(
|
|
48
|
+
args: string[],
|
|
49
|
+
label: KindLabeller = labelWithInference
|
|
50
|
+
): Promise<number> {
|
|
51
|
+
return runCommand(correctionCommandFor(label), args, ["pal", "cli", "correction"]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function windowDays(days: string | undefined): number {
|
|
55
|
+
if (days === undefined) return 30;
|
|
56
|
+
const parsed = Number(days);
|
|
57
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
58
|
+
throw new UsageError(`--days must be a whole number of days, got "${days}"`);
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function splitDate(split: string): Date {
|
|
63
|
+
const at = new Date(split);
|
|
64
|
+
if (Number.isNaN(at.getTime()))
|
|
65
|
+
throw new UsageError(`--split must be a date such as 2026-10-20, got "${split}"`);
|
|
66
|
+
return at;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function kindLine(name: string, count: string, rate: string, extra = ""): string {
|
|
70
|
+
return ` ${name.padEnd(22)}${count.padStart(4)} ${rate.padStart(5)}${extra}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatRates(rates: KindRates, title: string): string {
|
|
74
|
+
const stale = rates.stale ? `, ${rates.stale} labelled by an older prompt` : "";
|
|
75
|
+
const header = `${title}: ${rates.turns} turns, ${rates.corrections} confirmed corrections, ${rates.unlabelled} without a kind${stale}`;
|
|
76
|
+
const rows = rates.kinds.map((rate) => {
|
|
77
|
+
const names = rate.suggestions.length
|
|
78
|
+
? ` (${[...new Set(rate.suggestions)].join(", ")})`
|
|
79
|
+
: "";
|
|
80
|
+
return kindLine(rate.kind, String(rate.count), rate.per100.toFixed(1), names);
|
|
81
|
+
});
|
|
82
|
+
return [header, kindLine("kind", "n", "/100"), ...rows].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cmdKinds(days: string | undefined, split: string | undefined, json: boolean) {
|
|
86
|
+
const turns = readTurns(windowDays(days));
|
|
87
|
+
const result = split ? splitRates(turns, splitDate(split)) : kindRates(turns);
|
|
88
|
+
if (json) {
|
|
89
|
+
console.log(JSON.stringify(result, null, 2));
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
const sections =
|
|
93
|
+
"before" in result
|
|
94
|
+
? [
|
|
95
|
+
formatRates(result.before, "Before ".concat(split ?? "")),
|
|
96
|
+
formatRates(result.after, "From ".concat(split ?? "")),
|
|
97
|
+
]
|
|
98
|
+
: [formatRates(result, "Last ".concat(String(windowDays(days)), " days"))];
|
|
99
|
+
console.log(sections.join("\n\n"));
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function labelledLine(turn: { ts: string; issue: string }, kind: string): string {
|
|
104
|
+
return `${turn.ts.slice(0, 16)} ${kind} ${turn.issue}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function cmdBackfill(label: KindLabeller, relabel: boolean) {
|
|
108
|
+
const result = await backfillCorrectionKinds({
|
|
109
|
+
label,
|
|
110
|
+
relabel,
|
|
111
|
+
onLabelled: (turn, kind) => console.log(labelledLine(turn, kind?.kind ?? "failed")),
|
|
112
|
+
});
|
|
113
|
+
console.log(`Labelled ${result.labelled}, failed ${result.failed}.`);
|
|
114
|
+
return result.failed > 0 ? 1 : 0;
|
|
115
|
+
}
|
package/src/cli/rule.ts
CHANGED
|
@@ -4,17 +4,32 @@
|
|
|
4
4
|
* Subcommands:
|
|
5
5
|
* list [--all] [--json] Drafts waiting for a decision (--all: every rule)
|
|
6
6
|
* approve <id> Turn a draft into a rule that steers
|
|
7
|
+
* opinion <id> Keep a draft as an opinion, steering every session
|
|
7
8
|
* deny <id> Reject a draft; it stays on record so it is not redrafted
|
|
9
|
+
* retire <id> End an approved rule or opinion
|
|
10
|
+
* add-opinion --kind K --when W --steering S [--evidence E]
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import {
|
|
11
14
|
type AdaptationRule,
|
|
15
|
+
addOpinion,
|
|
12
16
|
decideRule,
|
|
13
|
-
|
|
17
|
+
isOpinion,
|
|
14
18
|
readRules,
|
|
19
|
+
retireRule,
|
|
15
20
|
type TriggerProof,
|
|
16
21
|
} from "../hooks/lib/adaptation-rules";
|
|
17
|
-
import {
|
|
22
|
+
import { readTurns, type Turn } from "../hooks/lib/adaptation-turns";
|
|
23
|
+
import { CORRECTION_KINDS, type CorrectionKindName } from "../hooks/lib/correction-kind";
|
|
24
|
+
import {
|
|
25
|
+
EFFECT_WINDOW_DAYS,
|
|
26
|
+
type OpinionEffect,
|
|
27
|
+
opinionEffectOf,
|
|
28
|
+
} from "../hooks/lib/correction-rates";
|
|
29
|
+
import { gateAdvice } from "../hooks/lib/opinion-gate";
|
|
30
|
+
import { group, leaf, runCommand, UsageError } from "../tools/lib/command";
|
|
31
|
+
|
|
32
|
+
type DecideResult = ReturnType<typeof decideRule>;
|
|
18
33
|
|
|
19
34
|
export const ruleCommand = group({
|
|
20
35
|
summary: "Review the rules PAL drafted from your corrections",
|
|
@@ -30,12 +45,36 @@ export const ruleCommand = group({
|
|
|
30
45
|
approve: leaf({
|
|
31
46
|
summary: "Approve a draft so it steers",
|
|
32
47
|
args: "<id>",
|
|
33
|
-
run: ({ positionals }) =>
|
|
48
|
+
run: ({ positionals }) =>
|
|
49
|
+
report(positionals[0], decideRule(positionals[0], "approved")),
|
|
50
|
+
}),
|
|
51
|
+
opinion: leaf({
|
|
52
|
+
summary: "Keep a draft as an opinion, steering every session without a trigger",
|
|
53
|
+
args: "<id>",
|
|
54
|
+
run: ({ positionals }) =>
|
|
55
|
+
report(positionals[0], decideRule(positionals[0], "opinion")),
|
|
34
56
|
}),
|
|
35
57
|
deny: leaf({
|
|
36
58
|
summary: "Deny a draft; it stays on record so it is not drafted again",
|
|
37
59
|
args: "<id>",
|
|
38
|
-
run: ({ positionals }) =>
|
|
60
|
+
run: ({ positionals }) =>
|
|
61
|
+
report(positionals[0], decideRule(positionals[0], "denied")),
|
|
62
|
+
}),
|
|
63
|
+
retire: leaf({
|
|
64
|
+
summary: "End an approved rule or opinion",
|
|
65
|
+
args: "<id>",
|
|
66
|
+
run: ({ positionals }) => report(positionals[0], retireRule(positionals[0])),
|
|
67
|
+
}),
|
|
68
|
+
"add-opinion": leaf({
|
|
69
|
+
summary: "Write an opinion by hand; it is approved at once",
|
|
70
|
+
options: {
|
|
71
|
+
kind: { type: "string", description: "The correction kind it prevents" },
|
|
72
|
+
when: { type: "string", description: "The situation, in one sentence" },
|
|
73
|
+
steering: { type: "string", description: "The instruction, imperative" },
|
|
74
|
+
evidence: { type: "string", description: "Why it is needed" },
|
|
75
|
+
},
|
|
76
|
+
run: ({ values }) =>
|
|
77
|
+
cmdAddOpinion(values.kind, values.when, values.steering, values.evidence),
|
|
39
78
|
}),
|
|
40
79
|
},
|
|
41
80
|
});
|
|
@@ -54,37 +93,105 @@ function cmdList(all: boolean, json: boolean): number {
|
|
|
54
93
|
console.log(all ? "No rules yet." : "No drafts waiting.");
|
|
55
94
|
return 0;
|
|
56
95
|
}
|
|
57
|
-
|
|
96
|
+
const measure: Measure = { turns: readTurns(EFFECT_WINDOW_DAYS), rules: readRules() };
|
|
97
|
+
console.log(rules.map((rule) => formatRule(rule, measure)).join("\n\n"));
|
|
58
98
|
return 0;
|
|
59
99
|
}
|
|
60
100
|
|
|
61
|
-
|
|
101
|
+
interface Measure {
|
|
102
|
+
turns: Turn[];
|
|
103
|
+
rules: AdaptationRule[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatRule(rule: AdaptationRule, measure: Measure): string {
|
|
107
|
+
const label = isOpinion(rule) ? "opinion" : rule.status;
|
|
62
108
|
return [
|
|
63
|
-
`${rule.id} [${
|
|
109
|
+
`${rule.id} [${label}] ${rule.when}`,
|
|
64
110
|
...(rule.widens ? [` widens: ${rule.widens}`] : []),
|
|
65
|
-
|
|
111
|
+
...(rule.trigger
|
|
112
|
+
? [` trigger (${rule.trigger.side}): ${rule.trigger.pattern}`]
|
|
113
|
+
: []),
|
|
66
114
|
` steering: ${rule.steering}`,
|
|
115
|
+
...(rule.kind ? [` kind: ${rule.kind}`] : []),
|
|
67
116
|
...(rule.check ? [` check: ${rule.check}`] : []),
|
|
68
117
|
...(rule.proof ? [` proof: ${formatProof(rule.proof)}`] : []),
|
|
118
|
+
...offerLines(rule),
|
|
119
|
+
...gateLines(rule, measure),
|
|
120
|
+
...effectLines(rule, measure),
|
|
69
121
|
...rule.evidence.map((e) => ` evidence: ${e}`),
|
|
70
122
|
].join("\n");
|
|
71
123
|
}
|
|
72
124
|
|
|
125
|
+
function offerLines(rule: AdaptationRule): string[] {
|
|
126
|
+
if (rule.offer !== "opinion-only" || rule.status !== "draft" || rule.opinionGate)
|
|
127
|
+
return [];
|
|
128
|
+
const why = rule.trigger ? "the trigger did not prove" : "no trigger";
|
|
129
|
+
return [` offer: ${why}; keep it as an opinion or deny it`];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function gateLines(rule: AdaptationRule, { turns, rules }: Measure): string[] {
|
|
133
|
+
const advice = gateAdvice(rule, rules, turns);
|
|
134
|
+
return advice ? [` no opinion: ${advice}`] : [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function effectLines(rule: AdaptationRule, { turns, rules }: Measure): string[] {
|
|
138
|
+
const effect = opinionEffectOf(rule, rules, turns);
|
|
139
|
+
if (!effect) return [];
|
|
140
|
+
return [
|
|
141
|
+
` effect: ${formatWindows(effect)} (${effect.verdict})`,
|
|
142
|
+
` other kinds: ${formatWindows(effect.control)}`,
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatWindows({
|
|
147
|
+
before,
|
|
148
|
+
after,
|
|
149
|
+
}: Pick<OpinionEffect, "before" | "after">): string {
|
|
150
|
+
return `${before.count} in ${before.turns} turns before (${before.per100.toFixed(1)}/100), ${after.count} in ${after.turns} after (${after.per100.toFixed(1)}/100)`;
|
|
151
|
+
}
|
|
152
|
+
|
|
73
153
|
function formatProof(proof: TriggerProof): string {
|
|
74
154
|
return `fired on ${proof.firedCorrections}/${proof.corrections} corrections, ${proof.firedOrdinary}/${proof.ordinary} ordinary turns`;
|
|
75
155
|
}
|
|
76
156
|
|
|
77
|
-
function
|
|
78
|
-
if (rule.id
|
|
79
|
-
|
|
157
|
+
function resultLine(id: string, rule: AdaptationRule): string {
|
|
158
|
+
if (rule.id !== id)
|
|
159
|
+
return `Rule ${rule.id} now triggers on: ${rule.trigger?.pattern} (widened by ${id})`;
|
|
160
|
+
const label = isOpinion(rule) ? "kept as an opinion" : rule.status;
|
|
161
|
+
return `Rule ${id} ${label}: ${rule.when}`;
|
|
80
162
|
}
|
|
81
163
|
|
|
82
|
-
function
|
|
83
|
-
const result = decideRule(id, decision);
|
|
164
|
+
function report(id: string, result: DecideResult): number {
|
|
84
165
|
if (!result.ok) {
|
|
85
166
|
console.error(result.reason);
|
|
86
167
|
return 1;
|
|
87
168
|
}
|
|
88
|
-
console.log(
|
|
169
|
+
console.log(resultLine(id, result.rule));
|
|
89
170
|
return 0;
|
|
90
171
|
}
|
|
172
|
+
|
|
173
|
+
function correctionKind(kind: string | undefined): CorrectionKindName {
|
|
174
|
+
if (CORRECTION_KINDS.includes(kind as CorrectionKindName))
|
|
175
|
+
return kind as CorrectionKindName;
|
|
176
|
+
throw new UsageError(`--kind must be one of ${CORRECTION_KINDS.join(", ")}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function required(name: string, value: string | undefined): string {
|
|
180
|
+
if (!value?.trim()) throw new UsageError(`--${name} is required`);
|
|
181
|
+
return value.trim();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function cmdAddOpinion(
|
|
185
|
+
kind: string | undefined,
|
|
186
|
+
when: string | undefined,
|
|
187
|
+
steering: string | undefined,
|
|
188
|
+
evidence: string | undefined
|
|
189
|
+
): number {
|
|
190
|
+
const result = addOpinion({
|
|
191
|
+
kind: correctionKind(kind),
|
|
192
|
+
when: required("when", when),
|
|
193
|
+
steering: required("steering", steering),
|
|
194
|
+
evidence: evidence?.trim() ? [evidence.trim()] : [],
|
|
195
|
+
});
|
|
196
|
+
return report(result.ok ? result.rule.id : "", result);
|
|
197
|
+
}
|
package/src/cli/tree.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../tools/lib/command";
|
|
16
16
|
import { builtinTools, builtinToolVerbs } from "./builtin-tools";
|
|
17
17
|
import { contentCommand } from "./content";
|
|
18
|
+
import { correctionCommand } from "./correction";
|
|
18
19
|
import { costCommand } from "./cost";
|
|
19
20
|
import { eventsCommand } from "./events";
|
|
20
21
|
import { identityCommand } from "./identity";
|
|
@@ -169,6 +170,7 @@ export function cliTree(admin: AdminHandlers): Group {
|
|
|
169
170
|
knowledge: knowledgeCommand,
|
|
170
171
|
ledger: ledgerCommand,
|
|
171
172
|
rule: ruleCommand,
|
|
173
|
+
correction: correctionCommand,
|
|
172
174
|
server: serverCommand,
|
|
173
175
|
skill: skillCommand,
|
|
174
176
|
subagent: subagentCommand,
|
|
@@ -17,6 +17,12 @@ import {
|
|
|
17
17
|
} from "../lib/adaptation-candidates";
|
|
18
18
|
import { readRules } from "../lib/adaptation-rules";
|
|
19
19
|
import { appendTurn, readTurns, withRequests } from "../lib/adaptation-turns";
|
|
20
|
+
import {
|
|
21
|
+
correctionKindRequest,
|
|
22
|
+
needsCorrectionKind,
|
|
23
|
+
parseCorrectionKind,
|
|
24
|
+
withCorrectionKind,
|
|
25
|
+
} from "../lib/correction-kind";
|
|
20
26
|
import { spawnDetachedInference } from "../lib/detached-inference";
|
|
21
27
|
import { canInfer, inference } from "../lib/inference";
|
|
22
28
|
import { replyEnd } from "../lib/interaction-samples";
|
|
@@ -231,6 +237,16 @@ async function checkCorrection(reply: string, message: string, sessionId?: strin
|
|
|
231
237
|
return result.success ? parseCorrectionCheck(result.output) : null;
|
|
232
238
|
}
|
|
233
239
|
|
|
240
|
+
async function labelCorrectionKind(
|
|
241
|
+
reply: string,
|
|
242
|
+
message: string,
|
|
243
|
+
issue: string,
|
|
244
|
+
sessionId?: string
|
|
245
|
+
) {
|
|
246
|
+
const result = await inference(correctionKindRequest(reply, message, issue, sessionId));
|
|
247
|
+
return result.success ? parseCorrectionKind(result.output) : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
234
250
|
function knownRules() {
|
|
235
251
|
const waiting = readCandidates().filter((candidate) => candidate.verdict === "waiting");
|
|
236
252
|
return [
|
|
@@ -251,7 +267,7 @@ async function draftRuleCandidate(sessionId?: string): Promise<void> {
|
|
|
251
267
|
else logDebug("rule-drafter", `no candidate: ${result.output ?? result.error ?? ""}`);
|
|
252
268
|
}
|
|
253
269
|
|
|
254
|
-
/** Background mode: label the reaction, check a possible correction, store the rating. */
|
|
270
|
+
/** Background mode: label the reaction, check a possible correction, name a confirmed one's kind, store the rating. */
|
|
255
271
|
async function runReactionRatingAndStore(
|
|
256
272
|
message: string,
|
|
257
273
|
reply: string,
|
|
@@ -273,7 +289,10 @@ async function runReactionRatingAndStore(
|
|
|
273
289
|
label,
|
|
274
290
|
check
|
|
275
291
|
);
|
|
276
|
-
|
|
292
|
+
const kind = needsCorrectionKind(turn)
|
|
293
|
+
? await labelCorrectionKind(reply, message, turn.issue, sessionId)
|
|
294
|
+
: null;
|
|
295
|
+
if (turn) appendTurn(withCorrectionKind(turn, kind), sentAt);
|
|
277
296
|
if (turn?.confirmed) await draftRuleCandidate(sessionId);
|
|
278
297
|
} catch (err) {
|
|
279
298
|
const { logError } = await import("../lib/log");
|
|
@@ -134,6 +134,11 @@ async function checkRepo(): Promise<UpdateCache> {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/** npm's latest tag lags a fresh release by seconds; an older latest is never an update. */
|
|
138
|
+
export function isNewerRelease(latest: string, current: string): boolean {
|
|
139
|
+
return Bun.semver.order(latest, current) === 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
137
142
|
async function checkNpm(): Promise<UpdateCache> {
|
|
138
143
|
const current = getInstalledVersion();
|
|
139
144
|
|
|
@@ -149,7 +154,7 @@ async function checkNpm(): Promise<UpdateCache> {
|
|
|
149
154
|
|
|
150
155
|
return {
|
|
151
156
|
checkedAt: new Date().toISOString(),
|
|
152
|
-
available: latest
|
|
157
|
+
available: isNewerRelease(latest, current),
|
|
153
158
|
current,
|
|
154
159
|
latest,
|
|
155
160
|
mode: "package",
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rule candidates: drafts the small model wrote from repeated corrections, each
|
|
3
|
-
* with the proof of its trigger.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* with the proof of its trigger. A proven candidate becomes a draft rule; one
|
|
4
|
+
* whose trigger failed becomes a draft that can only be kept as an opinion, since
|
|
5
|
+
* the mistake recurs even when no pattern catches it, but only once its kind has
|
|
6
|
+
* recurred enough to measure. One the log is still too short to judge waits and
|
|
7
|
+
* is proven again as the log grows.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { randomUUID } from "node:crypto";
|
|
9
11
|
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
10
12
|
import { resolve } from "node:path";
|
|
11
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
addDraft,
|
|
15
|
+
type DraftInput,
|
|
16
|
+
type OpinionGate,
|
|
17
|
+
readRules,
|
|
18
|
+
type TriggerProof,
|
|
19
|
+
} from "./adaptation-rules";
|
|
12
20
|
import type { RequestedTurn } from "./adaptation-turns";
|
|
21
|
+
import { opinionGate } from "./opinion-gate";
|
|
13
22
|
import { paths } from "./paths";
|
|
14
23
|
import {
|
|
15
24
|
type ProofVerdict,
|
|
@@ -22,12 +31,10 @@ export interface CandidateInput extends DraftInput {
|
|
|
22
31
|
check: string;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
export
|
|
34
|
+
export type Candidate = CandidateInput & {
|
|
26
35
|
id: string;
|
|
27
36
|
createdAt: string;
|
|
28
|
-
|
|
29
|
-
proof: TriggerProof;
|
|
30
|
-
}
|
|
37
|
+
} & ({ verdict: ProofVerdict; proof: TriggerProof } | { verdict: "untriggered" });
|
|
31
38
|
|
|
32
39
|
function candidatesPath(): string {
|
|
33
40
|
return resolve(paths.adaptation(), "candidates.jsonl");
|
|
@@ -59,15 +66,35 @@ function writeCandidates(candidates: Candidate[]): void {
|
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
function prove(candidate: Candidate, turns: RequestedTurn[]): Candidate {
|
|
69
|
+
if (!candidate.trigger) return { ...candidate, verdict: "untriggered" };
|
|
62
70
|
const proof = proveTrigger(candidate.trigger, turns);
|
|
63
71
|
const verdict = candidate.widens ? widenedProofVerdict(proof) : proofVerdict(proof);
|
|
64
72
|
return { ...candidate, proof, verdict };
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
function
|
|
68
|
-
|
|
75
|
+
function canOnlyBeOpinion(candidate: Candidate): boolean {
|
|
76
|
+
const unproven = candidate.verdict === "failed" || candidate.verdict === "untriggered";
|
|
77
|
+
return unproven && !candidate.widens;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function gateFor(candidate: Candidate, turns: RequestedTurn[]): OpinionGate | undefined {
|
|
81
|
+
if (!candidate.kind || candidate.widens) return undefined;
|
|
82
|
+
return opinionGate(candidate.kind, readRules(), turns);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function draftOf(candidate: Candidate, turns: RequestedTurn[]): DraftInput | null {
|
|
69
86
|
const { id: _id, createdAt: _createdAt, verdict: _verdict, ...draft } = candidate;
|
|
70
|
-
|
|
87
|
+
const gate = gateFor(candidate, turns);
|
|
88
|
+
const gated = gate ? { ...draft, opinionGate: gate } : draft;
|
|
89
|
+
if (!canOnlyBeOpinion(candidate)) return gated;
|
|
90
|
+
if (!candidate.kind || (gate && "corrections" in gate)) return null;
|
|
91
|
+
return { ...gated, offer: "opinion-only" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function promoteIfDecided(candidate: Candidate, turns: RequestedTurn[]): void {
|
|
95
|
+
if (candidate.verdict !== "passed" && !canOnlyBeOpinion(candidate)) return;
|
|
96
|
+
const draft = draftOf(candidate, turns);
|
|
97
|
+
if (draft) addDraft(draft);
|
|
71
98
|
}
|
|
72
99
|
|
|
73
100
|
export function recordCandidate(
|
|
@@ -82,7 +109,7 @@ export function recordCandidate(
|
|
|
82
109
|
} as Candidate;
|
|
83
110
|
const candidate = prove(fresh, turns);
|
|
84
111
|
writeCandidates([...readCandidates(), candidate]);
|
|
85
|
-
|
|
112
|
+
promoteIfDecided(candidate, turns);
|
|
86
113
|
return candidate;
|
|
87
114
|
}
|
|
88
115
|
|
|
@@ -91,5 +118,6 @@ export function reproveWaiting(turns: RequestedTurn[]): void {
|
|
|
91
118
|
if (!candidates.some((c) => c.verdict === "waiting")) return;
|
|
92
119
|
const updated = candidates.map((c) => (c.verdict === "waiting" ? prove(c, turns) : c));
|
|
93
120
|
writeCandidates(updated);
|
|
94
|
-
updated.filter((
|
|
121
|
+
for (const candidate of updated.filter((_c, i) => candidates[i].verdict === "waiting"))
|
|
122
|
+
promoteIfDecided(candidate, turns);
|
|
95
123
|
}
|