pi-observational-memory 2.4.3 → 3.0.1
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 +318 -103
- package/package.json +9 -9
- package/src/agents/dropper/agent.ts +281 -0
- package/src/agents/dropper/coverage.ts +128 -0
- package/src/agents/dropper/pool.ts +67 -0
- package/src/agents/dropper/prompts.ts +48 -0
- package/src/{observer.ts → agents/observer/agent.ts} +11 -13
- package/src/agents/observer/prompts.ts +119 -0
- package/src/agents/reflector/agent.ts +203 -0
- package/src/agents/reflector/prompts.ts +81 -0
- package/src/clipboard.ts +63 -0
- package/src/commands/status.ts +79 -78
- package/src/commands/view.ts +58 -66
- package/src/config.ts +78 -55
- package/src/debug-log.ts +24 -5
- package/src/hooks/compaction-hook.ts +24 -369
- package/src/hooks/compaction-trigger.ts +12 -21
- package/src/hooks/consolidation-trigger.ts +368 -0
- package/src/index.ts +3 -3
- package/src/model-budget.ts +1 -1
- package/src/runtime.ts +46 -19
- package/src/serialize.ts +1 -1
- package/src/session-ledger/fold.ts +100 -0
- package/src/session-ledger/index.ts +6 -0
- package/src/session-ledger/progress.ts +129 -0
- package/src/session-ledger/projection.ts +220 -0
- package/src/session-ledger/recall.ts +237 -0
- package/src/session-ledger/render-summary.ts +31 -0
- package/src/session-ledger/types.ts +200 -0
- package/src/tokens.ts +1 -1
- package/src/tools/recall-observation.ts +84 -214
- package/src/branch.ts +0 -577
- package/src/compaction.ts +0 -1030
- package/src/hooks/observer-trigger.ts +0 -129
- package/src/progress.ts +0 -155
- package/src/prompts.ts +0 -302
- package/src/relevance.ts +0 -15
- package/src/types.ts +0 -155
package/src/commands/status.ts
CHANGED
|
@@ -1,104 +1,105 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import {
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { observationPoolMetrics } from "../agents/dropper/pool.js";
|
|
3
|
+
import type { Runtime } from "../runtime.js";
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
diffProjection,
|
|
6
|
+
foldLedger,
|
|
7
|
+
fullProjection,
|
|
6
8
|
rawTokensSinceLastCompaction,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import { reflectionContent, type MemoryReflection } from "../types.js";
|
|
9
|
+
rawTokensSinceObservationCoverage,
|
|
10
|
+
rawTokensSinceReflectionCoverage,
|
|
11
|
+
visibleProjection,
|
|
12
|
+
type Entry,
|
|
13
|
+
} from "../session-ledger/index.js";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
handler: async (_args, ctx) => {
|
|
18
|
-
runtime.ensureConfig(ctx.cwd);
|
|
19
|
-
const entries = ctx.sessionManager.getBranch() as Parameters<typeof rawTokensSinceLastBound>[0];
|
|
20
|
-
const sinceBound = rawTokensSinceLastBound(entries);
|
|
21
|
-
const sinceCompaction = rawTokensSinceLastCompaction(entries);
|
|
15
|
+
function pct(current: number, total: number): number {
|
|
16
|
+
return total > 0 ? Math.min(100, Math.round((current / total) * 100)) : 0;
|
|
17
|
+
}
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const committedObsCount = committedObs.length;
|
|
27
|
-
const committedRefsTokens = committedRefItems.reduce((s, r) => s + estimateStringTokens(reflectionContent(r)), 0);
|
|
28
|
-
const committedRefsCount = committedRefItems.length;
|
|
19
|
+
function tokenSum(items: { tokenCount: number }[]): number {
|
|
20
|
+
return items.reduce((sum, item) => sum + item.tokenCount, 0);
|
|
21
|
+
}
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
function addedSuffix(count: number): string | undefined {
|
|
24
|
+
return count > 0 ? `+${count.toLocaleString()}` : undefined;
|
|
25
|
+
}
|
|
32
26
|
|
|
33
|
-
|
|
27
|
+
function removedSuffix(count: number): string | undefined {
|
|
28
|
+
return count > 0 ? `-${count.toLocaleString()}` : undefined;
|
|
29
|
+
}
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
function appendSuffixes(line: string, suffixes: (string | undefined)[]): string {
|
|
32
|
+
const rendered = suffixes.filter((suffix): suffix is string => suffix !== undefined);
|
|
33
|
+
return rendered.length > 0 ? `${line} ${rendered.join(" ")}` : line;
|
|
34
|
+
}
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const compPct = Math.min(100, Math.round((sinceCompaction / compThreshold) * 100));
|
|
48
|
-
const refPct = Math.min(100, Math.round((observationPoolTokens / refThreshold) * 100));
|
|
36
|
+
export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void {
|
|
37
|
+
pi.registerCommand("om-status", {
|
|
38
|
+
description: "Show observational memory status",
|
|
39
|
+
handler: async (_args, ctx) => {
|
|
40
|
+
runtime.ensureConfig(ctx.cwd);
|
|
41
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
42
|
+
const folded = foldLedger(entries);
|
|
43
|
+
const visible = visibleProjection(entries);
|
|
44
|
+
const full = fullProjection(entries);
|
|
45
|
+
const drift = diffProjection(visible, full);
|
|
49
46
|
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
47
|
+
const visibleObservationTokens = tokenSum(visible.observations);
|
|
48
|
+
const visibleReflectionTokens = tokenSum(visible.reflections);
|
|
49
|
+
const activeObservationPool = observationPoolMetrics(folded.activeObservations, runtime.config.observationsPoolTargetTokens);
|
|
50
|
+
const observationLine = appendSuffixes(
|
|
51
|
+
`Observations: ${folded.observations.length} recorded / ${folded.droppedObservationIds.size} dropped / ${folded.activeObservations.length} active / ${visible.observations.length} visible`,
|
|
52
|
+
[
|
|
53
|
+
addedSuffix(drift.observationsOnlyInFull.length),
|
|
54
|
+
removedSuffix(drift.droppedOnlyInFull.length),
|
|
55
|
+
],
|
|
56
|
+
);
|
|
57
|
+
const reflectionLine = appendSuffixes(
|
|
58
|
+
`Reflections: ${folded.reflections.length} recorded / ${visible.reflections.length} visible`,
|
|
59
|
+
[addedSuffix(drift.reflectionsOnlyInFull.length)],
|
|
60
|
+
);
|
|
61
|
+
const obsProgress = rawTokensSinceObservationCoverage(entries);
|
|
62
|
+
const reflectionProgress = rawTokensSinceReflectionCoverage(entries);
|
|
63
|
+
const compactionProgress = rawTokensSinceLastCompaction(entries);
|
|
53
64
|
|
|
54
65
|
const passiveLines = runtime.config.passive === true
|
|
55
66
|
? [
|
|
56
67
|
"── Mode ──",
|
|
57
|
-
"Passive:
|
|
68
|
+
"Passive: automatic memory workers and auto-compaction disabled; manual/Pi compaction, commands, and recall remain active",
|
|
58
69
|
"",
|
|
59
70
|
]
|
|
60
71
|
: [];
|
|
61
72
|
|
|
62
|
-
const activityLines = runtime.config.passive === true
|
|
63
|
-
? [
|
|
64
|
-
"── Activity ──",
|
|
65
|
-
`Observation trigger: passive (~${sinceBound.toLocaleString()} / ${obsThreshold.toLocaleString()} tokens, ${obsPct}%)`,
|
|
66
|
-
" → proactive observation is disabled; manual/Pi compaction can still run sync catch-up observation",
|
|
67
|
-
`Compaction trigger: passive (~${sinceCompaction.toLocaleString()} / ${compThreshold.toLocaleString()} tokens, ${compPct}%)`,
|
|
68
|
-
" → proactive extension-triggered compaction is disabled; manual/Pi compaction still uses the custom hook",
|
|
69
|
-
`Next reflection: ~${observationPoolTokens.toLocaleString()} / ${refThreshold.toLocaleString()} tokens (${refPct}%)`,
|
|
70
|
-
` → if observations exceed ${refThreshold.toLocaleString()} tokens when compaction runs, reflections are`,
|
|
71
|
-
` distilled from them and redundant observations are pruned away`,
|
|
72
|
-
]
|
|
73
|
-
: [
|
|
74
|
-
"── Activity ──",
|
|
75
|
-
`Next observation: ~${sinceBound.toLocaleString()} / ${obsThreshold.toLocaleString()} tokens (${obsPct}%)`,
|
|
76
|
-
` → at ${obsThreshold.toLocaleString()} tokens, recent conversation is compressed into new observations`,
|
|
77
|
-
`Next compaction: ~${sinceCompaction.toLocaleString()} / ${compThreshold.toLocaleString()} tokens (${compPct}%)`,
|
|
78
|
-
` → at ${compThreshold.toLocaleString()} tokens, raw history is replaced by the updated reflections and`,
|
|
79
|
-
` observations, keeping only the last ${keepRecentTokens.toLocaleString()} tokens of conversation verbatim`,
|
|
80
|
-
`Next reflection: ~${observationPoolTokens.toLocaleString()} / ${refThreshold.toLocaleString()} tokens (${refPct}%)`,
|
|
81
|
-
` → if observations exceed ${refThreshold.toLocaleString()} tokens when compaction runs, reflections are`,
|
|
82
|
-
` distilled from them and redundant observations are pruned away`,
|
|
83
|
-
];
|
|
84
|
-
|
|
85
73
|
const lines = [
|
|
86
74
|
...passiveLines,
|
|
87
75
|
"── Memory ──",
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
` committed ~${committedObsTokens.toLocaleString()} tokens (${committedObsCount} ${cObsLabel}) — folded into last compaction`,
|
|
91
|
-
` pending ~${pendingObsTokens.toLocaleString()} tokens (${pendingObsCount} ${pObsLabel}) — waiting for next compaction`,
|
|
92
|
-
` relevance ${formatRelevanceHistogram(relevanceHistogram)}`,
|
|
76
|
+
observationLine,
|
|
77
|
+
reflectionLine,
|
|
93
78
|
"",
|
|
94
|
-
|
|
79
|
+
"── Activity ──",
|
|
80
|
+
`Next observation: ~${obsProgress.toLocaleString()} / ${runtime.config.observeAfterTokens.toLocaleString()} tokens (${pct(obsProgress, runtime.config.observeAfterTokens)}%)`,
|
|
81
|
+
`Next reflection: ~${reflectionProgress.toLocaleString()} / ${runtime.config.reflectAfterTokens.toLocaleString()} tokens (${pct(reflectionProgress, runtime.config.reflectAfterTokens)}%)`,
|
|
82
|
+
`Next compaction: ~${compactionProgress.toLocaleString()} / ${runtime.config.compactAfterTokens.toLocaleString()} tokens (${pct(compactionProgress, runtime.config.compactAfterTokens)}%)`,
|
|
83
|
+
`Visible observation pool: ~${visibleObservationTokens.toLocaleString()} / ${runtime.config.observationsPoolMaxTokens.toLocaleString()} tokens (${pct(visibleObservationTokens, runtime.config.observationsPoolMaxTokens)}%)`,
|
|
84
|
+
`Active observation pool: ~${activeObservationPool.observationTokens.toLocaleString()} / ${runtime.config.observationsPoolTargetTokens.toLocaleString()} target tokens (${pct(activeObservationPool.observationTokens, runtime.config.observationsPoolTargetTokens)}%)`,
|
|
85
|
+
`Reflection pool: ~${visibleReflectionTokens.toLocaleString()} tokens`,
|
|
95
86
|
];
|
|
96
87
|
|
|
97
|
-
if (runtime.
|
|
98
|
-
lines.push("");
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
88
|
+
if (runtime.consolidationInFlight || runtime.compactInFlight || runtime.compactHookInFlight) {
|
|
89
|
+
lines.push("", "── In flight ──");
|
|
90
|
+
if (runtime.consolidationInFlight) {
|
|
91
|
+
const phase = runtime.consolidationPhase ? ` (${runtime.consolidationPhase})` : "";
|
|
92
|
+
lines.push(`Consolidation: running${phase}`);
|
|
93
|
+
}
|
|
94
|
+
if (runtime.compactInFlight) lines.push("Auto-compaction: running");
|
|
95
|
+
if (runtime.compactHookInFlight) lines.push("Compaction hook: running");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (runtime.lastObserverError || runtime.lastReflectorError || runtime.lastDropperError) {
|
|
99
|
+
lines.push("", "── Last error ──");
|
|
100
|
+
if (runtime.lastObserverError) lines.push(`Observer: ${runtime.lastObserverError}`);
|
|
101
|
+
if (runtime.lastReflectorError) lines.push(`Reflector: ${runtime.lastReflectorError}`);
|
|
102
|
+
if (runtime.lastDropperError) lines.push(`Dropper: ${runtime.lastDropperError}`);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
ctx.ui.notify(lines.join("\n"), "info");
|
package/src/commands/view.ts
CHANGED
|
@@ -1,82 +1,74 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { getMemoryState } from "../branch.js";
|
|
3
|
-
import { observationPoolTokens as estimateObservationPoolTokens } from "../compaction.js";
|
|
4
|
-
import { countByRelevance, formatRelevanceHistogram } from "../relevance.js";
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
2
|
import type { Runtime } from "../runtime.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { copyTextToClipboard } from "../clipboard.js";
|
|
4
|
+
import {
|
|
5
|
+
fullProjection,
|
|
6
|
+
observationToSummaryLine,
|
|
7
|
+
reflectionToSummaryLine,
|
|
8
|
+
visibleProjection,
|
|
9
|
+
type Entry,
|
|
10
|
+
type Projection,
|
|
11
|
+
} from "../session-ledger/index.js";
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const committedRefTokens = committedRefItems.reduce((s, r) => s + estimateStringTokens(reflectionContent(r)), 0);
|
|
19
|
-
const committedRefCount = committedRefItems.length;
|
|
20
|
-
|
|
21
|
-
const committedObsTokens = estimateObservationPoolTokens(committedObs);
|
|
22
|
-
const committedObsCount = committedObs.length;
|
|
13
|
+
function firstArg(args: unknown): string | undefined {
|
|
14
|
+
if (Array.isArray(args)) return typeof args[0] === "string" ? args[0] : undefined;
|
|
15
|
+
if (typeof args === "string") return args.trim().split(/\s+/)[0];
|
|
16
|
+
if (args && typeof args === "object" && "mode" in args) {
|
|
17
|
+
const mode = (args as { mode?: unknown }).mode;
|
|
18
|
+
return typeof mode === "string" ? mode : undefined;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
function renderList<T>(items: T[], render: (item: T) => string, empty: string): string {
|
|
24
|
+
return items.length > 0 ? items.map(render).join("\n") : empty;
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
function renderContentOnlyProjection(projection: Projection, emptyScope: "visible" | "recorded"): string {
|
|
28
|
+
return [
|
|
29
|
+
"── Reflections ──",
|
|
30
|
+
renderList(projection.reflections, reflectionToSummaryLine, `No ${emptyScope} reflections.`),
|
|
31
|
+
"",
|
|
32
|
+
"── Observations ──",
|
|
33
|
+
renderList(projection.observations, observationToSummaryLine, `No ${emptyScope} observations.`),
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
interface ViewCommandOptions {
|
|
38
|
+
copyToClipboard?: (text: string) => Promise<boolean>;
|
|
39
|
+
}
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
export function registerViewCommand(pi: ExtensionAPI, runtime: Runtime, options: ViewCommandOptions = {}): void {
|
|
42
|
+
const copyToClipboard = options.copyToClipboard ?? copyTextToClipboard;
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
45
|
-
sections.push("");
|
|
44
|
+
pi.registerCommand("om-view", {
|
|
45
|
+
description: "Print and copy observational memory content (visible by default, full for recorded memory)",
|
|
46
|
+
handler: async (args, ctx) => {
|
|
47
|
+
runtime.ensureConfig(ctx.cwd);
|
|
48
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
49
|
+
const mode = firstArg(args);
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const notifyWithCopy = async (output: string) => {
|
|
52
|
+
const copied = await copyToClipboard(output).catch(() => false);
|
|
53
|
+
ctx.ui.notify(
|
|
54
|
+
copied
|
|
55
|
+
? `${output}\n\nCopied /om-view output to clipboard.`
|
|
56
|
+
: `${output}\n\nWarning: failed to copy /om-view output to clipboard.`,
|
|
57
|
+
"info",
|
|
58
|
+
);
|
|
59
|
+
};
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
);
|
|
60
|
-
if (committedObs.length > 0) {
|
|
61
|
-
sections.push(committedObs.map(renderObs).join("\n"));
|
|
62
|
-
} else {
|
|
63
|
-
sections.push("(none)");
|
|
61
|
+
if (mode === "full") {
|
|
62
|
+
await notifyWithCopy(renderContentOnlyProjection(fullProjection(entries), "recorded"));
|
|
63
|
+
return;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
70
|
-
if (pendingObs.length > 0) {
|
|
71
|
-
sections.push(pendingObs.map(renderObs).join("\n"));
|
|
72
|
-
} else {
|
|
73
|
-
sections.push("(none)");
|
|
66
|
+
if (mode && mode !== "visible") {
|
|
67
|
+
ctx.ui.notify("Usage: /om-view [full]", "info");
|
|
68
|
+
return;
|
|
74
69
|
}
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
sections.push("Tip: use /tree to browse the raw messages still live in the session.");
|
|
78
|
-
|
|
79
|
-
ctx.ui.notify(sections.join("\n"), "info");
|
|
71
|
+
await notifyWithCopy(renderContentOnlyProjection(visibleProjection(entries), "visible"));
|
|
80
72
|
},
|
|
81
73
|
});
|
|
82
74
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,85 +1,96 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import type { ModelThinkingLevel } from "@
|
|
4
|
-
import { getAgentDir } from "@
|
|
3
|
+
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export interface ConfiguredModel {
|
|
7
|
+
provider: string;
|
|
8
|
+
id: string;
|
|
9
|
+
thinking?: ModelThinkingLevel;
|
|
10
|
+
}
|
|
5
11
|
|
|
6
12
|
export interface Config {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
observeAfterTokens: number;
|
|
14
|
+
reflectAfterTokens: number;
|
|
15
|
+
compactAfterTokens: number;
|
|
16
|
+
observationsPoolMaxTokens: number;
|
|
17
|
+
observationsPoolTargetTokens: number;
|
|
18
|
+
agentMaxTurns: number;
|
|
19
|
+
model?: ConfiguredModel;
|
|
10
20
|
passive: boolean;
|
|
11
21
|
debugLog: boolean;
|
|
12
|
-
compactionModel?: { provider: string; id: string };
|
|
13
|
-
observerMaxTurnsPerRun?: number;
|
|
14
|
-
reflectorMaxTurnsPerPass?: number;
|
|
15
|
-
prunerMaxTurnsPerPass?: number;
|
|
16
|
-
thinkingLevel: ModelThinkingLevel;
|
|
17
|
-
/** @deprecated Use reflectorMaxTurnsPerPass and prunerMaxTurnsPerPass. */
|
|
18
|
-
compactionMaxToolCalls?: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface EffectiveTurnLimits {
|
|
22
|
-
observerMaxTurnsPerRun: number;
|
|
23
|
-
reflectorMaxTurnsPerPass: number;
|
|
24
|
-
prunerMaxTurnsPerPass: number;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
export const DEFAULTS: Config = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
observeAfterTokens: 10_000,
|
|
26
|
+
reflectAfterTokens: 20_000,
|
|
27
|
+
compactAfterTokens: 81_000,
|
|
28
|
+
observationsPoolMaxTokens: 20_000,
|
|
29
|
+
observationsPoolTargetTokens: 10_000,
|
|
30
|
+
agentMaxTurns: 16,
|
|
31
31
|
passive: false,
|
|
32
32
|
debugLog: false,
|
|
33
|
-
thinkingLevel: "low",
|
|
34
33
|
};
|
|
35
34
|
|
|
36
35
|
export const THINKING_LEVEL_VALUES: readonly ModelThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
37
36
|
|
|
38
37
|
const SETTINGS_KEY = "observational-memory";
|
|
39
38
|
const PASSIVE_ENV = "PI_OBSERVATIONAL_MEMORY_PASSIVE";
|
|
40
|
-
const DEFAULT_MAX_TURNS = 16;
|
|
41
39
|
|
|
42
40
|
function positiveIntegerOrUndefined(value: unknown): number | undefined {
|
|
43
41
|
return Number.isInteger(value) && typeof value === "number" && value > 0 ? value : undefined;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
44
|
+
function validTargetOrUndefined(value: unknown, maxTokens: number): number | undefined {
|
|
45
|
+
const target = positiveIntegerOrUndefined(value);
|
|
46
|
+
return target !== undefined && target < maxTokens ? target : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function derivedObservationPoolTarget(maxTokens: number): number {
|
|
50
|
+
return Math.floor(maxTokens / 2);
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
function isThinkingLevel(value: unknown): value is ModelThinkingLevel {
|
|
57
54
|
return typeof value === "string" && (THINKING_LEVEL_VALUES as readonly string[]).includes(value);
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
if (!isThinkingLevel(normalized.thinkingLevel)) delete normalized.thinkingLevel;
|
|
57
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
58
|
+
return typeof value === "object" && value !== null;
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
if ("passive" in normalized && typeof normalized.passive !== "boolean") delete normalized.passive;
|
|
68
|
-
if ("debugLog" in normalized && typeof normalized.debugLog !== "boolean") delete normalized.debugLog;
|
|
69
|
-
normalizeTurnLimit(normalized, "observerMaxTurnsPerRun");
|
|
70
|
-
normalizeTurnLimit(normalized, "reflectorMaxTurnsPerPass");
|
|
71
|
-
normalizeTurnLimit(normalized, "prunerMaxTurnsPerPass");
|
|
72
|
-
normalizeTurnLimit(normalized, "compactionMaxToolCalls");
|
|
73
|
-
normalizeThinkingLevel(normalized);
|
|
74
|
-
return normalized;
|
|
61
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
62
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
75
63
|
}
|
|
76
64
|
|
|
77
|
-
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
65
|
+
function normalizeModel(value: unknown): ConfiguredModel | undefined {
|
|
66
|
+
if (!isRecord(value)) return undefined;
|
|
67
|
+
const provider = nonEmptyString(value.provider);
|
|
68
|
+
const id = nonEmptyString(value.id);
|
|
69
|
+
if (!provider || !id) return undefined;
|
|
70
|
+
const model: ConfiguredModel = { provider, id };
|
|
71
|
+
if (isThinkingLevel(value.thinking)) model.thinking = value.thinking;
|
|
72
|
+
return model;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeSettingsConfig(value: Record<string, unknown>): Partial<Config> {
|
|
76
|
+
const normalized: Partial<Config> = {};
|
|
77
|
+
const numberKeys = [
|
|
78
|
+
"observeAfterTokens",
|
|
79
|
+
"reflectAfterTokens",
|
|
80
|
+
"compactAfterTokens",
|
|
81
|
+
"observationsPoolMaxTokens",
|
|
82
|
+
"observationsPoolTargetTokens",
|
|
83
|
+
"agentMaxTurns",
|
|
84
|
+
] as const;
|
|
85
|
+
for (const key of numberKeys) {
|
|
86
|
+
const normalizedValue = positiveIntegerOrUndefined(value[key]);
|
|
87
|
+
if (normalizedValue !== undefined) normalized[key] = normalizedValue;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value.passive === "boolean") normalized.passive = value.passive;
|
|
90
|
+
if (typeof value.debugLog === "boolean") normalized.debugLog = value.debugLog;
|
|
91
|
+
const model = normalizeModel(value.model);
|
|
92
|
+
if (model) normalized.model = model;
|
|
93
|
+
return normalized;
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
export function readEnvConfig(env: NodeJS.ProcessEnv = process.env): Partial<Config> {
|
|
@@ -96,7 +107,7 @@ function readNamespacedConfig(path: string): Partial<Config> {
|
|
|
96
107
|
try {
|
|
97
108
|
const raw = JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
|
|
98
109
|
const nested = raw[SETTINGS_KEY];
|
|
99
|
-
return nested
|
|
110
|
+
return isRecord(nested) ? normalizeSettingsConfig(nested) : {};
|
|
100
111
|
} catch {
|
|
101
112
|
return {};
|
|
102
113
|
}
|
|
@@ -105,11 +116,23 @@ function readNamespacedConfig(path: string): Partial<Config> {
|
|
|
105
116
|
export function loadConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): Config {
|
|
106
117
|
const globalPath = join(getAgentDir(), "settings.json");
|
|
107
118
|
const projectPath = join(cwd, ".pi", "settings.json");
|
|
119
|
+
const globalConfig = readNamespacedConfig(globalPath);
|
|
120
|
+
const projectConfig = readNamespacedConfig(projectPath);
|
|
121
|
+
const envConfig = readEnvConfig(env);
|
|
122
|
+
const merged = {
|
|
123
|
+
...DEFAULTS,
|
|
124
|
+
observationsPoolTargetTokens: undefined,
|
|
125
|
+
...globalConfig,
|
|
126
|
+
...projectConfig,
|
|
127
|
+
...envConfig,
|
|
128
|
+
};
|
|
129
|
+
const target = validTargetOrUndefined(
|
|
130
|
+
merged.observationsPoolTargetTokens,
|
|
131
|
+
merged.observationsPoolMaxTokens,
|
|
132
|
+
) ?? derivedObservationPoolTarget(merged.observationsPoolMaxTokens);
|
|
108
133
|
|
|
109
134
|
return {
|
|
110
|
-
...
|
|
111
|
-
|
|
112
|
-
...readNamespacedConfig(projectPath),
|
|
113
|
-
...readEnvConfig(env),
|
|
135
|
+
...merged,
|
|
136
|
+
observationsPoolTargetTokens: target,
|
|
114
137
|
};
|
|
115
138
|
}
|
package/src/debug-log.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { existsSync, mkdirSync, renameSync, statSync, unlinkSync, appendFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { getAgentDir } from "@
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
export const DEBUG_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
7
7
|
export const DEBUG_LOG_RELATIVE_PATH = join("observational-memory", "debug.ndjson");
|
|
8
|
+
export const DEBUG_LOG_SESSION_DIR_RELATIVE_PATH = join("observational-memory", "debug");
|
|
8
9
|
|
|
9
|
-
interface DebugLogContext {
|
|
10
|
+
export interface DebugLogContext {
|
|
10
11
|
enabled: boolean;
|
|
11
12
|
cwd?: string;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
sessionFile?: string;
|
|
12
15
|
runId?: string;
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -19,8 +22,22 @@ export function withDebugLogContext<T>(context: DebugLogContext, fn: () => T): T
|
|
|
19
22
|
return storage.run({ ...parent, ...context }, fn);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
export function
|
|
23
|
-
|
|
25
|
+
export function safeDebugLogSessionId(sessionId: string | undefined): string | undefined {
|
|
26
|
+
const trimmed = sessionId?.trim();
|
|
27
|
+
if (!trimmed) return undefined;
|
|
28
|
+
const sanitized = trimmed
|
|
29
|
+
.replace(/[^A-Za-z0-9._-]+/g, "_")
|
|
30
|
+
.replace(/^_+|_+$/g, "")
|
|
31
|
+
.slice(0, 128);
|
|
32
|
+
if (!/[A-Za-z0-9]/.test(sanitized)) return undefined;
|
|
33
|
+
return sanitized;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function debugLogRelativePath(context: Pick<DebugLogContext, "sessionId">): string {
|
|
37
|
+
const safeSessionId = safeDebugLogSessionId(context.sessionId);
|
|
38
|
+
return safeSessionId
|
|
39
|
+
? join(DEBUG_LOG_SESSION_DIR_RELATIVE_PATH, `${safeSessionId}.ndjson`)
|
|
40
|
+
: DEBUG_LOG_RELATIVE_PATH;
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
export function debugLog(event: string, data: Record<string, unknown> = {}): void {
|
|
@@ -28,13 +45,15 @@ export function debugLog(event: string, data: Record<string, unknown> = {}): voi
|
|
|
28
45
|
if (context?.enabled !== true) return;
|
|
29
46
|
|
|
30
47
|
try {
|
|
31
|
-
const path = join(getAgentDir(),
|
|
48
|
+
const path = join(getAgentDir(), debugLogRelativePath(context));
|
|
32
49
|
mkdirSync(dirname(path), { recursive: true });
|
|
33
50
|
rotateIfNeeded(path);
|
|
34
51
|
const payload = {
|
|
35
52
|
ts: new Date().toISOString(),
|
|
36
53
|
event,
|
|
37
54
|
cwd: context.cwd,
|
|
55
|
+
sessionId: context.sessionId,
|
|
56
|
+
sessionFile: context.sessionFile,
|
|
38
57
|
runId: context.runId,
|
|
39
58
|
data,
|
|
40
59
|
};
|