portable-agent-layer 0.86.1 → 0.86.3
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/assets/skills/rule-review/SKILL.md +106 -0
- package/package.json +1 -1
- package/src/cli/index.ts +8 -0
- package/src/cli/rule.ts +77 -0
- package/src/hooks/handlers/rating.ts +8 -0
- package/src/hooks/lib/adaptation-rules.ts +83 -0
- package/src/hooks/lib/adaptation-turns.ts +90 -0
- package/src/hooks/lib/paths.ts +1 -0
- package/src/hooks/lib/reaction-rating.ts +20 -0
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ pal cli status # check your setup
|
|
|
91
91
|
| `pal cli timezone [<zone>]` | Show or set your timezone. IANA names only, validated before it is stored |
|
|
92
92
|
| `pal cli knowledge` | Query & manage the knowledge store (search, graph, stats, hubs, find, show, add, ls, ingest) |
|
|
93
93
|
| `pal cli ledger` | Query the action ledger — `log`, `show <id>`, `stats`, filtered by `--project`, `--since`, `--actor`, `--machine`, `--runtime`, `--outcome`, `--tool`, `--target` |
|
|
94
|
+
| `pal cli rule` | Review the rules drafted from your corrections — `list [--all] [--json]`, `approve <id>`, `deny <id>`. A draft steers nothing until approved; a denied one is never drafted again |
|
|
94
95
|
| `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 |
|
|
95
96
|
| `pal cli skill link <name>` | Link a personal `~/.pal/skills/<name>/` into every installed agent so it is discoverable |
|
|
96
97
|
| `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) |
|
|
@@ -203,7 +204,7 @@ PAL ships with built-in skills that extend your agent's capabilities:
|
|
|
203
204
|
| `presentation` | Build branded slide decks from outlines |
|
|
204
205
|
| `projects` | Look up, resume, register, or manage tracked projects |
|
|
205
206
|
| `reflect` | Diagnose why a PAL behavior didn't trigger |
|
|
206
|
-
| `telos` | Inspect or update goals, beliefs, strategies, narratives |
|
|
207
|
+
| `rule-review` | Approve or deny the rules PAL drafted from your corrections || `telos` | Inspect or update goals, beliefs, strategies, narratives |
|
|
207
208
|
| `think` | Structured first-pass reasoning on a problem |
|
|
208
209
|
|
|
209
210
|
---
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rule-review
|
|
3
|
+
license: MIT
|
|
4
|
+
description: "Review the adaptation rule drafts PAL wrote from the user's corrections and record the user's approve-or-deny decision for each via the rule CLI. Use when the user asks what rule drafts are waiting, wants to review pending rules, or says to approve or deny a rule draft. Not for editing the shipped steering hints or for the opinion tracker."
|
|
5
|
+
argument-hint: "[list | approve <id> | deny <id>]"
|
|
6
|
+
metadata:
|
|
7
|
+
source: portable-agent-layer
|
|
8
|
+
triggers:
|
|
9
|
+
- "rule-review"
|
|
10
|
+
- "rule review"
|
|
11
|
+
- "rule drafts"
|
|
12
|
+
- "pending rules"
|
|
13
|
+
- "review rules"
|
|
14
|
+
- "approve the rule"
|
|
15
|
+
- "deny the rule"
|
|
16
|
+
- "adaptation rules"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Rule review
|
|
20
|
+
|
|
21
|
+
An adaptation rule is "when this situation happens (trigger), steer the assistant like this (steering)", backed by evidence from the user's past corrections. PAL drafts them; every rule starts as a draft and steers nothing until the user approves it. A denied draft stays on record so it is never drafted again. A draft can only be approved or denied, never edited.
|
|
22
|
+
|
|
23
|
+
Your job: show the user the drafts and record their decision. The decision is the user's alone. Never approve or deny a draft the user has not explicitly decided on.
|
|
24
|
+
|
|
25
|
+
## Workflow
|
|
26
|
+
|
|
27
|
+
### 1. List the drafts
|
|
28
|
+
|
|
29
|
+
When the user asks what rule drafts are waiting, wants to review rules, or invokes the skill without an id, run:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pal cli rule list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Each draft prints as:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
<id> [draft] <situation>
|
|
39
|
+
trigger (<prompt|reply>): <pattern>
|
|
40
|
+
steering: <steering>
|
|
41
|
+
evidence: <line>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- If the output is `No drafts waiting.`, tell the user that and stop.
|
|
45
|
+
- Otherwise present every draft in the format under "Output format", then ask the user to approve or deny each one. Keep the ids from this listing; they are what a follow-up decision refers to.
|
|
46
|
+
|
|
47
|
+
When the user asks about rules already decided (approved, denied, history, all rules), run `pal cli rule list --all` instead and present them the same way, showing each rule's status.
|
|
48
|
+
|
|
49
|
+
### 2. Resolve which draft the user means
|
|
50
|
+
|
|
51
|
+
Map the user's decision to exactly one draft id before running anything:
|
|
52
|
+
|
|
53
|
+
- The user names an id → use that id.
|
|
54
|
+
- The user says "approve it", "deny it", "approve", "deny", "yes approve", or "reject" right after you presented exactly one draft → use that draft's id.
|
|
55
|
+
- The user says "approve them all" or "deny all" after a listing → one command per listed id, in the order listed.
|
|
56
|
+
- The user refers to a draft by its content ("the one about commit messages") and exactly one presented draft matches → use its id.
|
|
57
|
+
- Anything else, including several drafts presented with no id named, or a decision given before any listing in this conversation → do not guess. Run step 1 if there is no listing yet, then ask which id the user means.
|
|
58
|
+
|
|
59
|
+
A bare "yes", "ok", "fine", or "looks good" is not a decision. Ask "approve or deny <id>?" and wait.
|
|
60
|
+
|
|
61
|
+
### 3. Record the decision
|
|
62
|
+
|
|
63
|
+
Run the matching command:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pal cli rule approve <id>
|
|
67
|
+
pal cli rule deny <id>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Report the CLI's result line verbatim, e.g. `Rule 3f9a1c2e approved: <situation>`.
|
|
71
|
+
|
|
72
|
+
If the command exits 1, report its reason verbatim. The reasons are `No rule with id <id>` and `Rule <id> was already <approved|denied>`. On the first, re-run `pal cli rule list` and show the current ids. On the second, say the draft was already decided and leave it.
|
|
73
|
+
|
|
74
|
+
### 4. Handle requests to change a draft
|
|
75
|
+
|
|
76
|
+
Editing a draft is not supported, so never offer it and never rewrite a draft's text yourself. When the user wants a different wording, scope, or steering, say so and give the two options: deny the draft, or approve it as is. Then wait for that decision.
|
|
77
|
+
|
|
78
|
+
## Output format
|
|
79
|
+
|
|
80
|
+
Present each draft as one compact block, nothing else in between:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
Draft <id>
|
|
84
|
+
When: <situation>
|
|
85
|
+
Steer: <steering>
|
|
86
|
+
Evidence: <evidence line>
|
|
87
|
+
<evidence line>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Follow the list with one question, e.g. "Approve or deny each? Reply with the id and your decision." After a single draft, "Approve or deny <id>?" is enough.
|
|
91
|
+
|
|
92
|
+
After each decision, output the CLI's result line (or error reason) verbatim and nothing more unless the user asked a question.
|
|
93
|
+
|
|
94
|
+
## When to use
|
|
95
|
+
|
|
96
|
+
- The user asks what rule drafts are pending, waiting, or need review.
|
|
97
|
+
- The user asks to review, approve, or deny an adaptation rule.
|
|
98
|
+
- The user says "approve it" or "deny it" right after you presented a draft.
|
|
99
|
+
- The user asks which rules have been approved or denied so far.
|
|
100
|
+
|
|
101
|
+
## Do NOT use
|
|
102
|
+
|
|
103
|
+
- To change the shipped contextual-steering hints. Those live under the `steering` key in `pal-settings.json`, not in rules.
|
|
104
|
+
- For the opinion tracker (confirming or contradicting a tracked preference). That is the `opinion` skill.
|
|
105
|
+
- To edit, reword, or merge a draft. Editing is not supported; the only outcomes are approve or deny.
|
|
106
|
+
- To approve or deny on the user's behalf because a draft "looks reasonable". Wait for the explicit decision.
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* doctor Check prerequisites and system health
|
|
18
18
|
* usage Summarize token usage and cost
|
|
19
19
|
* ledger <sub> [filters] Query the action ledger (log · show · stats)
|
|
20
|
+
* rule list|approve|deny [id] Review rules drafted from your corrections
|
|
20
21
|
* server start|stop|restart|status The control room, a local page over ~/.pal
|
|
21
22
|
* <tool> [args] Run a built-in agent tool (project, thread, analyze, …)
|
|
22
23
|
* skill run <skill> <tool> [-- args] Run a skill's own tool by name, not by path
|
|
@@ -202,6 +203,12 @@ async function runCli(command: string | undefined, args: string[]) {
|
|
|
202
203
|
if (code !== 0) process.exit(code);
|
|
203
204
|
break;
|
|
204
205
|
}
|
|
206
|
+
case "rule": {
|
|
207
|
+
const { runRule } = await import("./rule");
|
|
208
|
+
const code = await runRule(args);
|
|
209
|
+
if (code !== 0) process.exit(code);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
205
212
|
case "server": {
|
|
206
213
|
const { runServer } = await import("./server");
|
|
207
214
|
const code = await runServer(args);
|
|
@@ -298,6 +305,7 @@ function showHelp() {
|
|
|
298
305
|
(search · graph · stats · hubs · find · show · add · ls)
|
|
299
306
|
pal cli ledger <sub> [filters] Query the action ledger (log · show · stats)
|
|
300
307
|
e.g. ledger log --project X --since 7d
|
|
308
|
+
pal cli rule list|approve|deny [id] Review rules drafted from your corrections
|
|
301
309
|
pal cli server start|stop|restart|status The control room: a local page to open before a terminal
|
|
302
310
|
pal cli skill run <skill> <tool> [-- args] Run ~/.pal/skills/<skill>/tools/<tool> by name
|
|
303
311
|
pal cli skill link <name> Link a personal ~/.pal/skills/<name>/ into installed agents
|
package/src/cli/rule.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pal cli rule — review the adaptation rules drafted from the user's corrections.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* list [--all] [--json] Drafts waiting for a decision (--all: every rule)
|
|
6
|
+
* approve <id> Turn a draft into a rule that steers
|
|
7
|
+
* deny <id> Reject a draft; it stays on record so it is not redrafted
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type AdaptationRule,
|
|
12
|
+
decideRule,
|
|
13
|
+
type RuleStatus,
|
|
14
|
+
readRules,
|
|
15
|
+
} from "../hooks/lib/adaptation-rules";
|
|
16
|
+
|
|
17
|
+
const DECISIONS: Record<string, Exclude<RuleStatus, "draft">> = {
|
|
18
|
+
approve: "approved",
|
|
19
|
+
deny: "denied",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function runRule(args: string[]): Promise<number> {
|
|
23
|
+
const [sub, ...rest] = args;
|
|
24
|
+
if (sub === "list") return cmdList(rest);
|
|
25
|
+
if (sub && sub in DECISIONS) return cmdDecide(rest[0], DECISIONS[sub]);
|
|
26
|
+
showHelp();
|
|
27
|
+
return sub === undefined || sub === "help" ? 0 : 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cmdList(args: string[]): number {
|
|
31
|
+
const all = args.includes("--all");
|
|
32
|
+
const rules = readRules().filter((r) => all || r.status === "draft");
|
|
33
|
+
if (args.includes("--json")) {
|
|
34
|
+
console.log(JSON.stringify(rules, null, 2));
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
if (rules.length === 0) {
|
|
38
|
+
console.log(all ? "No rules yet." : "No drafts waiting.");
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
console.log(rules.map(formatRule).join("\n\n"));
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatRule(rule: AdaptationRule): string {
|
|
46
|
+
return [
|
|
47
|
+
`${rule.id} [${rule.status}] ${rule.when}`,
|
|
48
|
+
` trigger (${rule.trigger.side}): ${rule.trigger.pattern}`,
|
|
49
|
+
` steering: ${rule.steering}`,
|
|
50
|
+
...rule.evidence.map((e) => ` evidence: ${e}`),
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cmdDecide(
|
|
55
|
+
id: string | undefined,
|
|
56
|
+
decision: Exclude<RuleStatus, "draft">
|
|
57
|
+
): number {
|
|
58
|
+
if (!id) {
|
|
59
|
+
console.error("Give the rule id: pal cli rule approve|deny <id>");
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const result = decideRule(id, decision);
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
console.error(result.reason);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
console.log(`Rule ${id} ${decision}: ${result.rule.when}`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function showHelp(): void {
|
|
72
|
+
console.log(`pal cli rule — review adaptation rules drafted from your corrections
|
|
73
|
+
|
|
74
|
+
pal cli rule list [--all] [--json] Drafts waiting for a decision
|
|
75
|
+
pal cli rule approve <id> Approve a draft so it steers
|
|
76
|
+
pal cli rule deny <id> Deny a draft; it is not drafted again`);
|
|
77
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { resolve } from "node:path";
|
|
13
|
+
import { appendTurn } from "../lib/adaptation-turns";
|
|
13
14
|
import { spawnDetachedInference } from "../lib/detached-inference";
|
|
14
15
|
import { canInfer, inference } from "../lib/inference";
|
|
15
16
|
import { replyEnd } from "../lib/interaction-samples";
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
ratingContext,
|
|
22
23
|
ratingFromLabels,
|
|
23
24
|
reactionRequest,
|
|
25
|
+
turnFromLabels,
|
|
24
26
|
} from "../lib/reaction-rating";
|
|
25
27
|
import { emitRating } from "../lib/signals";
|
|
26
28
|
import { now } from "../lib/time";
|
|
@@ -223,6 +225,12 @@ async function runReactionRatingAndStore(
|
|
|
223
225
|
if (first && rating !== null) {
|
|
224
226
|
handleRating(rating, ratingContext(first, message), "implicit", reply, message);
|
|
225
227
|
}
|
|
228
|
+
const turn = turnFromLabels(
|
|
229
|
+
{ session: sessionId ?? "", message, replyEnd: reply },
|
|
230
|
+
first,
|
|
231
|
+
confirmation
|
|
232
|
+
);
|
|
233
|
+
if (turn) appendTurn(turn);
|
|
226
234
|
} catch (err) {
|
|
227
235
|
const { logError } = await import("../lib/log");
|
|
228
236
|
logError("rating:reaction-child", err);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptation rules: "when this happens, steer like this", learned from the
|
|
3
|
+
* user's corrections. A rule starts as a draft and only steers once the user
|
|
4
|
+
* approves it. A denied rule stays on record so the same rule is not drafted again.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { paths } from "./paths";
|
|
11
|
+
|
|
12
|
+
export type RuleStatus = "draft" | "approved" | "denied";
|
|
13
|
+
type Decision = Exclude<RuleStatus, "draft">;
|
|
14
|
+
|
|
15
|
+
interface RuleTrigger {
|
|
16
|
+
/** Whether the pattern is matched against the user's prompt or the assistant's reply. */
|
|
17
|
+
side: "prompt" | "reply";
|
|
18
|
+
pattern: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DraftInput {
|
|
22
|
+
when: string;
|
|
23
|
+
trigger: RuleTrigger;
|
|
24
|
+
steering: string;
|
|
25
|
+
evidence: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AdaptationRule extends DraftInput {
|
|
29
|
+
id: string;
|
|
30
|
+
status: RuleStatus;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
decidedAt?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type DecideResult = { ok: true; rule: AdaptationRule } | { ok: false; reason: string };
|
|
36
|
+
|
|
37
|
+
export function rulesPath(): string {
|
|
38
|
+
return resolve(paths.adaptation(), "rules.json");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readRules(): AdaptationRule[] {
|
|
42
|
+
const path = rulesPath();
|
|
43
|
+
if (!existsSync(path)) return [];
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
46
|
+
return Array.isArray(parsed) ? (parsed as AdaptationRule[]) : [];
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeRules(rules: AdaptationRule[]): void {
|
|
53
|
+
const path = rulesPath();
|
|
54
|
+
const pending = `${path}.${process.pid}.tmp`;
|
|
55
|
+
writeFileSync(pending, `${JSON.stringify(rules, null, 2)}\n`, "utf-8");
|
|
56
|
+
renameSync(pending, path);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function addDraft(input: DraftInput): AdaptationRule {
|
|
60
|
+
const rule: AdaptationRule = {
|
|
61
|
+
...input,
|
|
62
|
+
id: randomUUID().slice(0, 8),
|
|
63
|
+
status: "draft",
|
|
64
|
+
createdAt: new Date().toISOString(),
|
|
65
|
+
};
|
|
66
|
+
writeRules([...readRules(), rule]);
|
|
67
|
+
return rule;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function decideRule(id: string, decision: Decision): DecideResult {
|
|
71
|
+
const rules = readRules();
|
|
72
|
+
const rule = rules.find((r) => r.id === id);
|
|
73
|
+
if (!rule) return { ok: false, reason: `No rule with id ${id}` };
|
|
74
|
+
if (rule.status !== "draft")
|
|
75
|
+
return { ok: false, reason: `Rule ${id} was already ${rule.status}` };
|
|
76
|
+
const decided: AdaptationRule = {
|
|
77
|
+
...rule,
|
|
78
|
+
status: decision,
|
|
79
|
+
decidedAt: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
writeRules(rules.map((r) => (r.id === id ? decided : r)));
|
|
82
|
+
return { ok: true, rule: decided };
|
|
83
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn log: every message the reaction rater labelled, with the end of the
|
|
3
|
+
* reply it answered. Rule drafts are proven by replaying their trigger over it.
|
|
4
|
+
*
|
|
5
|
+
* One append-only file per month, so parallel sessions never rewrite each
|
|
6
|
+
* other's lines; a month is deleted whole once it ends before the window.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { appendFileSync, existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { paths } from "./paths";
|
|
12
|
+
|
|
13
|
+
const WINDOW_DAYS = 30;
|
|
14
|
+
const MESSAGE_MAX = 800;
|
|
15
|
+
const REPLY_END_MAX = 600;
|
|
16
|
+
const DAY_MS = 86_400_000;
|
|
17
|
+
const MONTH_FILE = /^turns-(\d{4})-(\d{2})\.jsonl$/;
|
|
18
|
+
|
|
19
|
+
export interface TurnInput {
|
|
20
|
+
session: string;
|
|
21
|
+
message: string;
|
|
22
|
+
replyEnd: string;
|
|
23
|
+
reaction: string;
|
|
24
|
+
issue: string;
|
|
25
|
+
/** Set on a correction: whether the second label agreed. */
|
|
26
|
+
confirmed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Turn extends TurnInput {
|
|
30
|
+
ts: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function monthFile(date: Date): string {
|
|
34
|
+
return `turns-${date.toISOString().slice(0, 7)}.jsonl`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function monthEnd(file: string): number | null {
|
|
38
|
+
const match = MONTH_FILE.exec(file);
|
|
39
|
+
return match ? Date.UTC(Number(match[1]), Number(match[2]), 1) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function monthFiles(): string[] {
|
|
43
|
+
const dir = paths.adaptation();
|
|
44
|
+
return existsSync(dir)
|
|
45
|
+
? readdirSync(dir)
|
|
46
|
+
.filter((f) => MONTH_FILE.test(f))
|
|
47
|
+
.sort()
|
|
48
|
+
: [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pruneMonthsBefore(cutoff: number): void {
|
|
52
|
+
for (const file of monthFiles()) {
|
|
53
|
+
const end = monthEnd(file);
|
|
54
|
+
if (end !== null && end <= cutoff)
|
|
55
|
+
rmSync(resolve(paths.adaptation(), file), { force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function appendTurn(turn: TurnInput, now: Date = new Date()): void {
|
|
60
|
+
const record: Turn = {
|
|
61
|
+
ts: now.toISOString(),
|
|
62
|
+
...turn,
|
|
63
|
+
message: turn.message.slice(0, MESSAGE_MAX),
|
|
64
|
+
replyEnd: turn.replyEnd.slice(-REPLY_END_MAX),
|
|
65
|
+
};
|
|
66
|
+
appendFileSync(
|
|
67
|
+
resolve(paths.adaptation(), monthFile(now)),
|
|
68
|
+
`${JSON.stringify(record)}\n`
|
|
69
|
+
);
|
|
70
|
+
pruneMonthsBefore(now.getTime() - WINDOW_DAYS * DAY_MS);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseLine(line: string): Turn | null {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(line) as Turn;
|
|
76
|
+
return typeof parsed.ts === "string" ? parsed : null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function readTurns(days: number = WINDOW_DAYS, now: Date = new Date()): Turn[] {
|
|
83
|
+
const cutoff = now.getTime() - days * DAY_MS;
|
|
84
|
+
return monthFiles()
|
|
85
|
+
.flatMap((file) =>
|
|
86
|
+
readFileSync(resolve(paths.adaptation(), file), "utf-8").split("\n")
|
|
87
|
+
)
|
|
88
|
+
.map(parseLine)
|
|
89
|
+
.filter((turn): turn is Turn => turn !== null && Date.parse(turn.ts) >= cutoff);
|
|
90
|
+
}
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -98,6 +98,7 @@ export const paths = {
|
|
|
98
98
|
wisdom: () => ensureDir(home("memory", "wisdom", "frames")),
|
|
99
99
|
wisdomState: () => ensureDir(home("memory", "wisdom", "state")),
|
|
100
100
|
relationship: () => ensureDir(home("memory", "relationship")),
|
|
101
|
+
adaptation: () => ensureDir(home("memory", "adaptation")),
|
|
101
102
|
knowledge: () => ensureDir(home("memory", "knowledge")),
|
|
102
103
|
knowledgeDomain: (d: string) => ensureDir(home("memory", "knowledge", d)),
|
|
103
104
|
failures: () => ensureDir(home("memory", "learning", "failures")),
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* go-ahead, a question or news carries no rating at all.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { TurnInput } from "./adaptation-turns";
|
|
10
|
+
|
|
9
11
|
const MODEL_REACTIONS = [
|
|
10
12
|
"corrected",
|
|
11
13
|
"repeated",
|
|
@@ -109,6 +111,24 @@ export function ratingFromLabels(
|
|
|
109
111
|
return RATING_BY_REACTION[first.reaction] ?? null;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
interface SeenTurn {
|
|
115
|
+
session: string;
|
|
116
|
+
message: string;
|
|
117
|
+
replyEnd: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function turnFromLabels(
|
|
121
|
+
seen: SeenTurn,
|
|
122
|
+
first: ReactionLabel | null,
|
|
123
|
+
confirmation: ReactionLabel | null
|
|
124
|
+
): TurnInput | null {
|
|
125
|
+
if (!first) return null;
|
|
126
|
+
const turn = { ...seen, reaction: first.reaction, issue: first.issue };
|
|
127
|
+
return isCorrectionLabel(first)
|
|
128
|
+
? { ...turn, confirmed: isCorrectionLabel(confirmation) }
|
|
129
|
+
: turn;
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
export function ratingContext(label: ReactionLabel, message: string): string {
|
|
113
133
|
const what = label.reaction === "praised" ? "Praised the reply" : label.issue;
|
|
114
134
|
return `${what || label.reaction}: ${message.slice(0, 200)}`;
|