portable-agent-layer 0.85.1 → 0.85.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/assets/schema/pal-settings.schema.json +4 -0
- package/assets/templates/pal-settings.json +1 -0
- package/package.json +1 -1
- package/src/hooks/handlers/inject-retrieval.ts +2 -0
- package/src/hooks/lib/context.ts +3 -14
- package/src/hooks/lib/daily-nudge.ts +49 -6
- package/src/hooks/lib/interaction-reaction.ts +19 -14
- package/src/hooks/lib/settings.ts +5 -0
- package/src/hooks/lib/stop.ts +4 -1
|
@@ -70,6 +70,10 @@
|
|
|
70
70
|
"type": "boolean",
|
|
71
71
|
"description": "One line of the current time with every prompt, in identity.principal.timezone, so a session resumed hours later knows the day changed."
|
|
72
72
|
},
|
|
73
|
+
"dueReminders": {
|
|
74
|
+
"type": "boolean",
|
|
75
|
+
"description": "Opt-in, off by default until 1.0. Due maintenance reminders (learning analysis, relationship reflect) with every prompt until a reply names their command; then quiet until the next day."
|
|
76
|
+
},
|
|
73
77
|
"interactionAwareness": {
|
|
74
78
|
"type": "boolean",
|
|
75
79
|
"description": "Measure each user turn (pace, message length, skimming, corrections; never the text) into memory/signals/interaction/, and tell the agent when the session's pattern changes."
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { promptContextResponse } from "../lib/agent";
|
|
11
|
+
import { dueNudgeReminder } from "../lib/daily-nudge";
|
|
11
12
|
import { observeTurn } from "../lib/interaction";
|
|
12
13
|
import { logDebug, logError } from "../lib/log";
|
|
13
14
|
import { parkPromptContext } from "../lib/parked-context";
|
|
@@ -81,6 +82,7 @@ export async function getPromptContext(
|
|
|
81
82
|
const parts = [
|
|
82
83
|
getWallClockReminder(),
|
|
83
84
|
withinBudget(() => observeTurn(prompt, sessionId), BUDGET_MS),
|
|
85
|
+
withinBudget(() => dueNudgeReminder(), BUDGET_MS),
|
|
84
86
|
getSteeringReminder(prompt),
|
|
85
87
|
getSkillReminder(prompt),
|
|
86
88
|
await getRetrievalReminder(prompt),
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -5,16 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
|
-
import { loadReflectNudge } from "../handlers/reflect-trigger";
|
|
9
|
-
import { loadAlgorithmReviewNudge } from "./algorithm-review";
|
|
10
|
-
import { loadAnalyzeNudge } from "./analyze-nudge";
|
|
11
8
|
import { resolveAnchor } from "./anchor";
|
|
12
|
-
import {
|
|
9
|
+
import { dueNudgeSections } from "./daily-nudge";
|
|
13
10
|
import { loadHandoffContext } from "./handoff-context";
|
|
14
11
|
import { loadOpinionContext } from "./opinions";
|
|
15
12
|
import { paths, toPath } from "./paths";
|
|
16
13
|
import { loadActiveProjectsContext } from "./projects";
|
|
17
|
-
import { loadReactionAuditNudge } from "./reaction-audit";
|
|
18
14
|
import { loadRecentNotes } from "./relationship";
|
|
19
15
|
import { loadFailurePatterns } from "./semi-static";
|
|
20
16
|
import { sessionDir } from "./session-dir";
|
|
@@ -219,18 +215,11 @@ export function buildSystemReminder(
|
|
|
219
215
|
!opts.withoutHandoff && settings.isEnabled("handoff")
|
|
220
216
|
? loadHandoffContext(sessionDir())
|
|
221
217
|
: "";
|
|
222
|
-
|
|
223
|
-
const algoReview = oncePerDay("algorithm-review", loadAlgorithmReviewNudge());
|
|
224
|
-
const reactionAudit = oncePerDay("reaction-audit", loadReactionAuditNudge());
|
|
225
|
-
const reflectNudge = oncePerDay("reflect", loadReflectNudge());
|
|
226
|
-
const analyzeNudge = oncePerDay("analyze", loadAnalyzeNudge());
|
|
218
|
+
const dueNudges = opts.agent === "cursor" ? dueNudgeSections() : [];
|
|
227
219
|
const parts: string[] = [];
|
|
228
220
|
if (startup) parts.push(startup);
|
|
229
221
|
if (handoff) parts.push(handoff);
|
|
230
|
-
|
|
231
|
-
if (reactionAudit) parts.push(reactionAudit);
|
|
232
|
-
if (reflectNudge) parts.push(reflectNudge);
|
|
233
|
-
if (analyzeNudge) parts.push(analyzeNudge);
|
|
222
|
+
parts.push(...dueNudges);
|
|
234
223
|
if (selfModel) parts.push(selfModel);
|
|
235
224
|
if (wisdom) parts.push(wisdom);
|
|
236
225
|
if (opinions) parts.push(opinions);
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { loadReflectNudge } from "../handlers/reflect-trigger";
|
|
4
|
+
import { loadAlgorithmReviewNudge } from "./algorithm-review";
|
|
5
|
+
import { loadAnalyzeNudge } from "./analyze-nudge";
|
|
3
6
|
import { ensureDir, paths } from "./paths";
|
|
7
|
+
import { loadReactionAuditNudge } from "./reaction-audit";
|
|
8
|
+
import { isOptedIn } from "./settings";
|
|
4
9
|
import { localDay } from "./wall-clock";
|
|
5
10
|
|
|
6
11
|
type ShownOn = Record<string, string>;
|
|
7
12
|
|
|
13
|
+
interface DueNudge {
|
|
14
|
+
key: string;
|
|
15
|
+
command: string;
|
|
16
|
+
load: () => string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DUE_NUDGES: DueNudge[] = [
|
|
20
|
+
{
|
|
21
|
+
key: "algorithm-review",
|
|
22
|
+
command: "/algorithm-update",
|
|
23
|
+
load: loadAlgorithmReviewNudge,
|
|
24
|
+
},
|
|
25
|
+
{ key: "reaction-audit", command: "/reaction-audit", load: loadReactionAuditNudge },
|
|
26
|
+
{ key: "reflect", command: "/pal-reflect", load: loadReflectNudge },
|
|
27
|
+
{ key: "analyze", command: "/pal-analyze", load: loadAnalyzeNudge },
|
|
28
|
+
];
|
|
29
|
+
|
|
8
30
|
function shownPath(): string {
|
|
9
31
|
return resolve(ensureDir(paths.state()), "nudges-shown.json");
|
|
10
32
|
}
|
|
@@ -18,18 +40,39 @@ function readShownOn(): ShownOn {
|
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
/**
|
|
21
|
-
* A reminder asks the user for something, so it
|
|
22
|
-
*
|
|
43
|
+
* A reminder asks the user for something, so it stays until a reply has passed
|
|
44
|
+
* it on, then rests for the rest of the day.
|
|
23
45
|
*/
|
|
24
|
-
export function
|
|
46
|
+
export function pendingToday(
|
|
25
47
|
key: string,
|
|
26
48
|
reminder: string,
|
|
27
49
|
now: Date = new Date()
|
|
28
50
|
): string {
|
|
29
51
|
if (!reminder) return "";
|
|
52
|
+
return readShownOn()[key] === localDay(now) ? "" : reminder;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function acknowledgeMentioned(reply: string, now: Date = new Date()): void {
|
|
56
|
+
const mentioned = DUE_NUDGES.filter((nudge) => reply.includes(nudge.command));
|
|
57
|
+
if (!mentioned.length) return;
|
|
30
58
|
const today = localDay(now);
|
|
31
59
|
const shownOn = readShownOn();
|
|
32
|
-
|
|
33
|
-
writeFileSync(shownPath(), JSON.stringify(
|
|
34
|
-
|
|
60
|
+
for (const nudge of mentioned) shownOn[nudge.key] = today;
|
|
61
|
+
writeFileSync(shownPath(), JSON.stringify(shownOn), "utf-8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function dueNudgeSections(now: Date = new Date()): string[] {
|
|
65
|
+
if (!isOptedIn("dueReminders")) return [];
|
|
66
|
+
return DUE_NUDGES.map((nudge) => pendingToday(nudge.key, nudge.load(), now)).filter(
|
|
67
|
+
Boolean
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function dueNudgeReminder(now: Date = new Date()): string | null {
|
|
72
|
+
const sections = dueNudgeSections(now);
|
|
73
|
+
if (!sections.length) return null;
|
|
74
|
+
return [
|
|
75
|
+
"Tell the user in one line at the end of this reply that the following is due, naming its command. This comes back every turn until a reply does.",
|
|
76
|
+
...sections,
|
|
77
|
+
].join("\n\n");
|
|
35
78
|
}
|
|
@@ -9,13 +9,16 @@ import { extractKeywords, similarity } from "./text-similarity";
|
|
|
9
9
|
export type Reaction = "corrected" | "repeated" | "approved" | "new-topic" | "follow-up";
|
|
10
10
|
|
|
11
11
|
const CORRECTION_RE =
|
|
12
|
-
/^(?:(?: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;
|
|
12
|
+
/^(?:(?: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;
|
|
13
|
+
|
|
14
|
+
const CLAIM_DISPUTED_RE =
|
|
15
|
+
/\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;
|
|
13
16
|
|
|
14
17
|
const APPROVAL_RE =
|
|
15
|
-
/^(?: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;
|
|
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;
|
|
16
19
|
|
|
17
20
|
const GO_AHEAD_RE =
|
|
18
|
-
/^(?:go ahead|go|do it|proceed|continue|merge|push|ship|commit|build|start|fix (?:all|them|both)|approved|let'?s (?:see|do|go))\b/i;
|
|
21
|
+
/^(?: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;
|
|
19
22
|
|
|
20
23
|
const NEW_TOPIC_RE =
|
|
21
24
|
/^(?:(?:ok(?:ay)?|btw|also),?\s+)?(?:new (?:\w+ ){0,2}(?:task|topic|question|thing)|(?:different|unrelated|separate|another) (?:task|topic|question|thing)|unrelated\b|switching (?:gears|topics?)|change of (?:topic|subject))/i;
|
|
@@ -23,11 +26,13 @@ const NEW_TOPIC_RE =
|
|
|
23
26
|
const GO_AHEAD_MAX_WORDS = 6;
|
|
24
27
|
const BARE_OK_RE = /^(?:ok(?:ay)?|k)[,.!]?(?:\s+\S+){0,2}$/i;
|
|
25
28
|
const HEDGE_RE = /\b(?:but|however|though|except|instead)\b/i;
|
|
29
|
+
const FAILURE_RE = /\b(?:fail(?:ed|s|ing)?|broken|broke|errors?|crash(?:ed|es)?)\b/i;
|
|
26
30
|
const REPEAT_SIMILARITY = 0.6;
|
|
27
31
|
const REPEAT_MIN_KEYWORDS = 5;
|
|
28
32
|
|
|
29
33
|
export function isCorrection(text: string): boolean {
|
|
30
|
-
|
|
34
|
+
const trimmed = text.trim();
|
|
35
|
+
return CORRECTION_RE.test(trimmed) || CLAIM_DISPUTED_RE.test(trimmed);
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export function isRepeat(text: string, previous: string | undefined): boolean {
|
|
@@ -39,21 +44,21 @@ function isShortGoAhead(sentence: string): boolean {
|
|
|
39
44
|
return GO_AHEAD_RE.test(sentence) && sentence.split(/\s+/).length <= GO_AHEAD_MAX_WORDS;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
function
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.at(-1)
|
|
48
|
-
?.trim() ?? ""
|
|
49
|
-
);
|
|
47
|
+
function sentences(text: string): string[] {
|
|
48
|
+
return text
|
|
49
|
+
.split(/[.!\n]\s*/)
|
|
50
|
+
.map((s) => s.trim())
|
|
51
|
+
.filter(Boolean);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
function isApproval(text: string): boolean {
|
|
53
55
|
const trimmed = text.trim();
|
|
54
|
-
if (trimmed.includes("?") ||
|
|
56
|
+
if (trimmed.includes("?") || FAILURE_RE.test(trimmed)) return false;
|
|
57
|
+
if (HEDGE_RE.test(trimmed.split(/[.!?\n]/)[0])) return false;
|
|
55
58
|
if (APPROVAL_RE.test(trimmed) || BARE_OK_RE.test(trimmed)) return true;
|
|
56
|
-
|
|
59
|
+
const all = sentences(trimmed);
|
|
60
|
+
if (isShortGoAhead(all.at(-1) ?? "")) return true;
|
|
61
|
+
return isShortGoAhead(all[0] ?? "") && !HEDGE_RE.test(trimmed);
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/** Word overlap was measured against real messages and does not separate topics; only saying so does. */
|
|
@@ -115,6 +115,11 @@ export function isEnabled(key: string): boolean {
|
|
|
115
115
|
return load().dynamicContext?.[key] !== false;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/** Check if an opt-in dynamic context section is enabled (defaults to false) */
|
|
119
|
+
export function isOptedIn(key: string): boolean {
|
|
120
|
+
return load().dynamicContext?.[key] === true;
|
|
121
|
+
}
|
|
122
|
+
|
|
118
123
|
/** Get the loadAtStartup file list */
|
|
119
124
|
export function startupFiles(): string[] {
|
|
120
125
|
return load().loadAtStartup?.files ?? [];
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { runSynthesis } from "../handlers/synthesis";
|
|
|
19
19
|
import { resetTab } from "../handlers/tab";
|
|
20
20
|
import { updateCounts } from "../handlers/update-counts";
|
|
21
21
|
import { captureWorkSession } from "../handlers/work-session";
|
|
22
|
+
import { acknowledgeMentioned } from "./daily-nudge";
|
|
22
23
|
import { spawnDetachedInference } from "./detached-inference";
|
|
23
24
|
import {
|
|
24
25
|
fileFinalReply,
|
|
@@ -274,7 +275,9 @@ function recordFinalReply(
|
|
|
274
275
|
sessionId: string
|
|
275
276
|
): void {
|
|
276
277
|
try {
|
|
277
|
-
|
|
278
|
+
const reply = extractContent(extractLastAssistant(messages));
|
|
279
|
+
recordReply(sessionId, reply);
|
|
280
|
+
acknowledgeMentioned(reply);
|
|
278
281
|
} catch (err) {
|
|
279
282
|
logError("runStopHandlers:recordReply", err);
|
|
280
283
|
}
|