portable-agent-layer 0.85.5 → 0.85.6

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.85.5",
3
+ "version": "0.85.6",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { median } from "./interaction-mood";
9
+ import { approvalIsKnown } from "./interaction-reaction";
9
10
 
10
11
  const MIN_REPLIES_PER_SIDE = 20;
11
12
  const MIN_GAP = 0.1;
@@ -20,6 +21,7 @@ interface ReplyFeatures {
20
21
  interface ReactedTurn {
21
22
  reply: ReplyFeatures | null;
22
23
  reaction: string | null;
24
+ reactionRules?: number;
23
25
  }
24
26
 
25
27
  interface Outcome {
@@ -41,7 +43,9 @@ export interface Split {
41
43
 
42
44
  function outcomes(events: ReactedTurn[]): Outcome[] {
43
45
  return events.flatMap((e) =>
44
- e.reply && e.reaction ? [{ reply: e.reply, approved: e.reaction === "approved" }] : []
46
+ e.reply && e.reaction && approvalIsKnown(e)
47
+ ? [{ reply: e.reply, approved: e.reaction === "approved" }]
48
+ : []
45
49
  );
46
50
  }
47
51
 
@@ -6,7 +6,21 @@
6
6
 
7
7
  import { extractKeywords, similarity } from "./text-similarity";
8
8
 
9
- export type Reaction = "corrected" | "repeated" | "approved" | "new-topic" | "follow-up";
9
+ export type Reaction =
10
+ | "corrected"
11
+ | "repeated"
12
+ | "approved"
13
+ | "go-ahead"
14
+ | "new-topic"
15
+ | "follow-up";
16
+
17
+ /** Logged with each reaction, so a log read under older rules is not read with today's meaning. */
18
+ export const REACTION_RULES = 2;
19
+ const GO_AHEAD_SPLIT = 2;
20
+
21
+ export function approvalIsKnown(turn: { reactionRules?: number }): boolean {
22
+ return (turn.reactionRules ?? 1) >= GO_AHEAD_SPLIT;
23
+ }
10
24
 
11
25
  const CORRECTION_RE =
12
26
  /^(?:(?:no|nope|wrong)(?:[,.!]|$)|(?:that'?s|this is|it'?s) (?:wrong|not)\b|not what i\b|i said\b|i told you\b|you forgot\b|you missed\b|still (?:broken|failing|wrong)\b|i don'?t see (?:the|your) (?!(?:problem|issue|point|harm|difference|need|reason)\b))/i;
@@ -15,7 +29,10 @@ const CLAIM_DISPUTED_RE =
15
29
  /\byou(?:'re| are) (?:partially |partly |completely |totally )?wrong\b|\b(?:isn'?t|wasn'?t|aren'?t|weren'?t) (?:added|included|fixed|pushed|committed|applied|merged)\b/i;
16
30
 
17
31
  const APPROVAL_RE =
18
- /^(?:(?:understood|got it|ok(?:ay)?),?\s+)?(?:good|great|nice|perfect|cool|amazing|awesome|excellent|brilliant|works|it works|thanks|thank you|thx|y+e+s+|yep|yeah|yup|sure|exactly|correct|agreed|lgtm|well done|love it|sounds good|looks good|that'?s it|(?:it|this|that) (?:is|'s) (?:fine|good|great|right))\b/i;
32
+ /^(?:(?:understood|got it|ok(?:ay)?),?\s+)?(?:good|great|nice|perfect|cool|amazing|awesome|excellent|brilliant|works|it works|thanks|thank you|thx|y+e+s+|yep|yeah|yup|sure|exactly|correct|agreed|lgtm|well done|love it|sounds good|looks good|that'?s it|(?:it|this|that)(?: is|'s) (?:fine|good|great|right))\b/i;
33
+
34
+ const PRAISE_RE =
35
+ /\b(?:good|great|nice|perfect|cool|amazing|awesome|excellent|brilliant|works|thanks|thank you|thx|exactly|correct|agreed|lgtm|well done|good job|love it|sounds good|looks (?:good|right)|that'?s it|(?:it|this|that)(?: is|'s) (?:fine|good|great|right))\b/i;
19
36
 
20
37
  const GO_AHEAD_RE =
21
38
  /^(?:go ahead|go|do it|proceed|continue|merge|push|ship|commit|build|start|fix (?:all|them|both)|approved|let'?s (?:see|do|go|build|start|fix|ship)|(?:add|do|fix) (?:it )?(?:pls|please))\b/i;
@@ -51,7 +68,7 @@ function sentences(text: string): string[] {
51
68
  .filter(Boolean);
52
69
  }
53
70
 
54
- function isApproval(text: string): boolean {
71
+ function isAcceptance(text: string): boolean {
55
72
  const trimmed = text.trim();
56
73
  if (trimmed.includes("?") || FAILURE_RE.test(trimmed)) return false;
57
74
  if (HEDGE_RE.test(trimmed.split(/[.!?\n]/)[0])) return false;
@@ -61,6 +78,11 @@ function isApproval(text: string): boolean {
61
78
  return isShortGoAhead(all[0] ?? "") && !HEDGE_RE.test(trimmed);
62
79
  }
63
80
 
81
+ /** A yes or a go only gives permission to continue; approval judges the work itself. */
82
+ function praisesTheResult(text: string): boolean {
83
+ return PRAISE_RE.test(text);
84
+ }
85
+
64
86
  /** Word overlap was measured against real messages and does not separate topics; only saying so does. */
65
87
  function isNewTopic(text: string): boolean {
66
88
  return NEW_TOPIC_RE.test(text.trim());
@@ -69,7 +91,7 @@ function isNewTopic(text: string): boolean {
69
91
  export function reactionTo(text: string, previousPrompt: string | undefined): Reaction {
70
92
  if (isCorrection(text)) return "corrected";
71
93
  if (isRepeat(text, previousPrompt)) return "repeated";
72
- if (isApproval(text)) return "approved";
94
+ if (isAcceptance(text)) return praisesTheResult(text) ? "approved" : "go-ahead";
73
95
  if (isNewTopic(text)) return "new-topic";
74
96
  return "follow-up";
75
97
  }
@@ -21,6 +21,7 @@ export interface ReactionSample {
21
21
  const KEPT: Record<Reaction, number> = {
22
22
  "follow-up": 150,
23
23
  approved: 20,
24
+ "go-ahead": 20,
24
25
  corrected: 20,
25
26
  repeated: 20,
26
27
  "new-topic": 20,
@@ -26,6 +26,7 @@ import { shorterApprovedMore } from "./interaction-preferences";
26
26
  import {
27
27
  isCorrection,
28
28
  isRepeat,
29
+ REACTION_RULES,
29
30
  type Reaction,
30
31
  reactionTo,
31
32
  } from "./interaction-reaction";
@@ -51,6 +52,7 @@ export interface TurnEvent extends MoodTurn {
51
52
  weekday: string;
52
53
  reply: Omit<ReplyShape, "at"> | null;
53
54
  reaction: Reaction | null;
55
+ reactionRules?: number;
54
56
  mood?: string;
55
57
  hinted?: boolean;
56
58
  complied?: boolean;
@@ -129,6 +131,7 @@ function measureTurn(
129
131
  corrected: isCorrection(text),
130
132
  reply: reply ? replyFeatures(reply) : null,
131
133
  reaction: reply ? reactionTo(text, track.lastPrompt) : null,
134
+ ...(reply && { reactionRules: REACTION_RULES }),
132
135
  };
133
136
  }
134
137
 
@@ -14,13 +14,14 @@ import {
14
14
  type Split,
15
15
  splits,
16
16
  } from "../../hooks/lib/interaction-preferences";
17
+ import { approvalIsKnown } from "../../hooks/lib/interaction-reaction";
17
18
 
18
19
  const LABELS = ["short", "long", "fast", "skimming", "friction"] as const;
19
20
 
20
21
  interface ReplyUnderMood {
21
22
  labels: string[];
22
23
  words: number;
23
- approved: boolean;
24
+ approved: boolean | null;
24
25
  corrected: boolean;
25
26
  complied?: boolean;
26
27
  }
@@ -52,6 +53,7 @@ export interface InteractionSummary {
52
53
  reactions: Record<string, number>;
53
54
  hints: Record<string, number>;
54
55
  unlogged: number;
56
+ readBeforeGoAhead: number;
55
57
  usual: ReplyStats;
56
58
  byLabel: Record<string, ReplyStats>;
57
59
  approvals: Approvals;
@@ -81,7 +83,10 @@ function stats(replies: ReplyUnderMood[]): ReplyStats {
81
83
  return {
82
84
  replies: replies.length,
83
85
  medianWords: median(replies.map((r) => r.words)),
84
- approvedShare: share(replies, (r) => r.approved),
86
+ approvedShare: share(
87
+ replies.filter((r) => r.approved !== null),
88
+ (r) => r.approved === true
89
+ ),
85
90
  correctedShare: share(replies, (r) => r.corrected),
86
91
  followed: {
87
92
  checked: replies.filter((r) => r.complied !== undefined).length,
@@ -100,7 +105,7 @@ function repliesUnderMood(events: TurnEvent[]): ReplyUnderMood[] {
100
105
  replies.push({
101
106
  labels: mood ? mood.split(",") : [],
102
107
  words: e.reply.words,
103
- approved: e.reaction === "approved",
108
+ approved: approvalIsKnown(e) ? e.reaction === "approved" : null,
104
109
  corrected: e.reaction === "corrected" || e.reaction === "repeated",
105
110
  complied: e.complied,
106
111
  });
@@ -109,6 +114,11 @@ function repliesUnderMood(events: TurnEvent[]): ReplyUnderMood[] {
109
114
  return replies;
110
115
  }
111
116
 
117
+ function reactionName(e: TurnEvent): string {
118
+ if (e.reaction === "approved" && !approvalIsKnown(e)) return "approved or go-ahead";
119
+ return e.reaction ?? "";
120
+ }
121
+
112
122
  /** A turn carries the reply that answered the turn before it, so a filed reply shows up there. */
113
123
  function byAgent(events: TurnEvent[]): Record<string, AgentCounts> {
114
124
  const agents: Record<string, AgentCounts> = {};
@@ -131,7 +141,8 @@ export function summarize(events: TurnEvent[]): InteractionSummary {
131
141
  sessions: new Set(events.map((e) => e.session)).size,
132
142
  channels: tally(events.map((e) => e.channel ?? "unknown")),
133
143
  agents: byAgent(events),
134
- reactions: tally(events.flatMap((e) => (e.reaction ? [e.reaction] : []))),
144
+ reactions: tally(events.flatMap((e) => (e.reaction ? [reactionName(e)] : []))),
145
+ readBeforeGoAhead: events.filter((e) => e.reaction && !approvalIsKnown(e)).length,
135
146
  hints: tally(hinted.map((e) => e.mood || "back to usual")),
136
147
  unlogged: events.filter((e) => e.mood === undefined).length,
137
148
  usual: stats(replies.filter((r) => r.labels.length === 0)),
@@ -255,5 +266,10 @@ export function reportLines(summary: InteractionSummary, days: number): string[]
255
266
  "",
256
267
  `${summary.unlogged} older turns were logged before moods were, and are left out of the comparison.`
257
268
  );
269
+ if (summary.readBeforeGoAhead)
270
+ lines.push(
271
+ "",
272
+ `${summary.readBeforeGoAhead} older replies were read before go-ahead was split from approval, and are left out of the approval figures.`
273
+ );
258
274
  return lines;
259
275
  }