portable-agent-layer 0.79.2 → 0.79.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 +0 -1
- package/assets/templates/pal-settings.json +0 -1
- package/package.json +1 -1
- package/src/hooks/handlers/persist-last-exchange.ts +13 -1
- package/src/hooks/handlers/session-intelligence.ts +1 -0
- package/src/hooks/lib/context.ts +5 -6
- package/src/hooks/lib/stop.ts +11 -4
- package/src/hooks/lib/transcript.ts +9 -0
- package/src/hooks/lib/work-tracking.ts +20 -2
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
"relationship": { "type": "boolean", "description": "Recent relationship notes." },
|
|
60
60
|
"failurePatterns": { "type": "boolean", "description": "Recurring failure patterns." },
|
|
61
61
|
"projects": { "type": "boolean", "description": "Active projects list." },
|
|
62
|
-
"sessionIntelligence": { "type": "boolean", "description": "Algorithm reflection stats and this project's self-observations." },
|
|
63
62
|
"handoff": { "type": "boolean", "description": "Handoff note from the previous session." },
|
|
64
63
|
"selfModel": { "type": "boolean", "description": "The assistant's self-model synthesis." },
|
|
65
64
|
"contextualSteering": { "type": "boolean", "description": "Prompt-time steering self-checks (see the steering section)." },
|
package/package.json
CHANGED
|
@@ -71,8 +71,20 @@ function readHandoffs(): Record<string, StoredHandoff> {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function isWithinReadWindow(entry: StoredHandoff): boolean {
|
|
75
|
+
if (!entry.timestamp) return false;
|
|
76
|
+
return Date.now() - new Date(entry.timestamp).getTime() <= HANDOFF_STALE_MS;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
function writeHandoffs(handoffs: Record<string, StoredHandoff>): void {
|
|
75
|
-
|
|
80
|
+
const readable = Object.entries(handoffs).filter(([, entry]) =>
|
|
81
|
+
isWithinReadWindow(entry)
|
|
82
|
+
);
|
|
83
|
+
writeFileSync(
|
|
84
|
+
handoffPath(),
|
|
85
|
+
JSON.stringify(Object.fromEntries(readable), null, 2),
|
|
86
|
+
"utf-8"
|
|
87
|
+
);
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
export function structuredHandoff(parts: {
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -117,8 +117,11 @@ export function loadRelationshipContext(): string {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Off the startup context until the reflection stats are reliable enough to act on.
|
|
122
|
+
* @lintignore exercised directly by test/context-build.test.ts
|
|
123
|
+
*/
|
|
124
|
+
export function loadSessionIntelligence(): string {
|
|
122
125
|
try {
|
|
123
126
|
const p = resolve(paths.state(), "synthesis.json");
|
|
124
127
|
if (!existsSync(p)) return "";
|
|
@@ -211,9 +214,6 @@ export function buildSystemReminder(
|
|
|
211
214
|
!skipSemiStatic && settings.isEnabled("opinions") ? loadOpinionContext() : "";
|
|
212
215
|
const selfModel =
|
|
213
216
|
!skipSemiStatic && settings.isEnabled("selfModel") ? loadSelfModel() : "";
|
|
214
|
-
const intelligence = settings.isEnabled("sessionIntelligence")
|
|
215
|
-
? loadSessionIntelligence()
|
|
216
|
-
: "";
|
|
217
217
|
const handoff =
|
|
218
218
|
!opts.withoutHandoff && settings.isEnabled("handoff")
|
|
219
219
|
? loadHandoffContext(sessionDir())
|
|
@@ -231,7 +231,6 @@ export function buildSystemReminder(
|
|
|
231
231
|
if (selfModel) parts.push(selfModel);
|
|
232
232
|
if (wisdom) parts.push(wisdom);
|
|
233
233
|
if (opinions) parts.push(opinions);
|
|
234
|
-
if (intelligence) parts.push(intelligence);
|
|
235
234
|
if (relationship) parts.push(relationship);
|
|
236
235
|
if (activeProjects) parts.push(activeProjects);
|
|
237
236
|
if (failures) parts.push(failures);
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -22,7 +22,12 @@ import { captureWorkSession } from "../handlers/work-session";
|
|
|
22
22
|
import { spawnDetachedInference } from "./detached-inference";
|
|
23
23
|
import { logDebug, logError } from "./log";
|
|
24
24
|
import { assets, ensureDir, paths } from "./paths";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
extractContent,
|
|
27
|
+
extractLastAssistant,
|
|
28
|
+
parseMessages,
|
|
29
|
+
withFinalReply,
|
|
30
|
+
} from "./transcript";
|
|
26
31
|
|
|
27
32
|
interface RunStopHandlersOptions {
|
|
28
33
|
lastAssistantMessage?: string;
|
|
@@ -31,11 +36,13 @@ interface RunStopHandlersOptions {
|
|
|
31
36
|
|
|
32
37
|
/** Run all stop handlers with a transcript string */
|
|
33
38
|
export async function runStopHandlers(
|
|
34
|
-
|
|
39
|
+
fileTranscript: string,
|
|
35
40
|
options: RunStopHandlersOptions = {}
|
|
36
41
|
): Promise<void> {
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
42
|
+
const fileMessages = parseMessages(fileTranscript);
|
|
43
|
+
if (fileMessages.length < 2) return;
|
|
44
|
+
const messages = withFinalReply(fileMessages, options.lastAssistantMessage);
|
|
45
|
+
const transcript = JSON.stringify(messages);
|
|
39
46
|
|
|
40
47
|
logDebug("runStopHandlers", `Running handlers (${messages.length} messages)`);
|
|
41
48
|
|
|
@@ -89,6 +89,15 @@ export function extractLastAssistant(messages: Message[]): Message | undefined {
|
|
|
89
89
|
return messages.filter((m) => m.role === "assistant").pop();
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/** The agent hands its final reply to the Stop hook before that reply reaches the transcript file. */
|
|
93
|
+
export function withFinalReply(messages: Message[], finalReply?: string): Message[] {
|
|
94
|
+
const reply = finalReply?.trim();
|
|
95
|
+
if (!reply) return messages;
|
|
96
|
+
const last = messages.at(-1);
|
|
97
|
+
if (last?.role === "assistant" && extractContent(last).includes(reply)) return messages;
|
|
98
|
+
return [...messages, { role: "assistant", content: reply }];
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
/** Get the last user message from a messages array */
|
|
93
102
|
export function extractLastUser(messages: Message[]): Message | undefined {
|
|
94
103
|
return messages.filter((m) => m.role === "user").pop();
|
|
@@ -145,6 +145,7 @@ interface ProjectHistoryEntry {
|
|
|
145
145
|
title: string;
|
|
146
146
|
summary: string;
|
|
147
147
|
insights: string;
|
|
148
|
+
session?: string;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/**
|
|
@@ -160,11 +161,28 @@ function historyFileFor(cwd: string): string {
|
|
|
160
161
|
: resolve(paths.unboundHistory(), `${defaultSlug(cwd)}.jsonl`);
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
function withoutSession(lines: string[], session: string): string[] {
|
|
165
|
+
return lines.filter((line) => {
|
|
166
|
+
try {
|
|
167
|
+
return (JSON.parse(line) as ProjectHistoryEntry).session !== session;
|
|
168
|
+
} catch {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** A session is summarised again as it grows; its entry is replaced, not repeated. */
|
|
164
175
|
export function appendProjectHistory(cwd: string, entry: ProjectHistoryEntry): void {
|
|
165
176
|
const historyPath = historyFileFor(cwd);
|
|
166
177
|
ensureDir(dirname(historyPath));
|
|
167
|
-
|
|
178
|
+
const line = JSON.stringify(entry);
|
|
179
|
+
if (!entry.session || !existsSync(historyPath)) {
|
|
180
|
+
appendFileSync(historyPath, `${line}\n`, "utf-8");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const lines = readFileSync(historyPath, "utf-8").split("\n").filter(Boolean);
|
|
184
|
+
const kept = withoutSession(lines, entry.session);
|
|
185
|
+
writeFileSync(historyPath, `${[...kept, line].join("\n")}\n`, "utf-8");
|
|
168
186
|
}
|
|
169
187
|
|
|
170
188
|
/** Read the session history for a given cwd */
|