portable-agent-layer 0.86.2 → 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/package.json
CHANGED
|
@@ -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,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
|
+
}
|
|
@@ -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)}`;
|