@matthewfl/pi-contemplator 0.1.14 → 0.1.16
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": "@matthewfl/pi-contemplator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "A Pi extension that keeps long-running agentic sessions on track with background memory, contemplation, and structural review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,10 +50,11 @@
|
|
|
50
50
|
"@earendil-works/pi-tui": "*"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
54
|
-
"@earendil-works/pi-ai": "^0.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
56
|
-
"@earendil-works/pi-
|
|
53
|
+
"@earendil-works/pi-agent-core": "^0.85.1",
|
|
54
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
56
|
+
"@earendil-works/pi-server": "^0.85.1",
|
|
57
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
57
58
|
"@types/node": "^22.0.0",
|
|
58
59
|
"typebox": "^1.1.38",
|
|
59
60
|
"typescript": "^5.6.0",
|
|
@@ -506,6 +506,8 @@ export class Contemplator {
|
|
|
506
506
|
this.history = [];
|
|
507
507
|
this.historyEntryIds = [];
|
|
508
508
|
const historyMessagesByEntryId = new Map<string, AgentMessage>();
|
|
509
|
+
const checkpointCoveredObservationIds = new Set<string>();
|
|
510
|
+
const checkpointCoveredReviewIds = new Set<string>();
|
|
509
511
|
let resetProjection: ReturnType<typeof fullProjection> | undefined;
|
|
510
512
|
if (resetTracking) {
|
|
511
513
|
this.deliveredProbeIds.clear();
|
|
@@ -539,7 +541,7 @@ export class Contemplator {
|
|
|
539
541
|
}
|
|
540
542
|
}
|
|
541
543
|
if (entry.customType === CONTEMPLATOR_MESSAGE && entry.data && typeof entry.data === "object") {
|
|
542
|
-
const data = entry.data as { message?: unknown; compacted?: unknown; retainedMessageEntryIds?: unknown };
|
|
544
|
+
const data = entry.data as { message?: unknown; compacted?: unknown; retainedMessageEntryIds?: unknown; coveredObservationIds?: unknown; coveredReviewIds?: unknown };
|
|
543
545
|
const message = data.message;
|
|
544
546
|
if (message && typeof message === "object") {
|
|
545
547
|
const typedMessage = message as AgentMessage;
|
|
@@ -550,6 +552,8 @@ export class Contemplator {
|
|
|
550
552
|
: [];
|
|
551
553
|
this.history = [typedMessage, ...retainedIds.map((id) => historyMessagesByEntryId.get(id)!)];
|
|
552
554
|
this.historyEntryIds = [entry.id, ...retainedIds];
|
|
555
|
+
if (Array.isArray(data.coveredObservationIds)) for (const id of data.coveredObservationIds) if (typeof id === "string") checkpointCoveredObservationIds.add(id);
|
|
556
|
+
if (Array.isArray(data.coveredReviewIds)) for (const id of data.coveredReviewIds) if (typeof id === "string") checkpointCoveredReviewIds.add(id);
|
|
553
557
|
} else {
|
|
554
558
|
this.history.push(typedMessage);
|
|
555
559
|
this.historyEntryIds.push(entry.id);
|
|
@@ -607,10 +611,10 @@ export class Contemplator {
|
|
|
607
611
|
}
|
|
608
612
|
}
|
|
609
613
|
if (resetTracking && resetProjection) {
|
|
610
|
-
// Successful
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
const coveredIds = new Set<string>();
|
|
614
|
+
// Successful update prompts and private-history compaction checkpoints are
|
|
615
|
+
// the durable coverage record. Checkpoints carry ids from prompts replaced
|
|
616
|
+
// by their summary; failed, unpersisted runs remain pending and retryable.
|
|
617
|
+
const coveredIds = new Set<string>([...checkpointCoveredObservationIds, ...checkpointCoveredReviewIds]);
|
|
614
618
|
for (const message of this.history) {
|
|
615
619
|
if (message.role !== "user") continue;
|
|
616
620
|
const text = customMessageText(message.content);
|
|
@@ -1359,7 +1363,28 @@ export class Contemplator {
|
|
|
1359
1363
|
debugLog("contemplator.compaction_postponed", { reason: "retained history lacked durable entry ids", retainedMessageCount: retainedMessages.length, retainedReferenceCount: retainedMessageEntryIds.length });
|
|
1360
1364
|
return;
|
|
1361
1365
|
}
|
|
1362
|
-
|
|
1366
|
+
// Record only coverage evidenced by the exact durable prompt prefix being
|
|
1367
|
+
// replaced. `seen*Ids` may also contain observations appended concurrently
|
|
1368
|
+
// while summary generation awaited; those remain pending and must not be
|
|
1369
|
+
// declared covered before their own prompt is persisted. Coverage is a delta
|
|
1370
|
+
// per checkpoint, and restore unions checkpoints, avoiding cumulative O(n²)
|
|
1371
|
+
// id duplication across a very long session.
|
|
1372
|
+
const prefixCoveredIds = new Set<string>();
|
|
1373
|
+
for (const message of history.slice(0, prefixEnd)) {
|
|
1374
|
+
if (message.role !== "user") continue;
|
|
1375
|
+
const text = customMessageText(message.content);
|
|
1376
|
+
if (!text.includes("NEW MEMORY UPDATE")) continue;
|
|
1377
|
+
for (const id of memoryReferenceIds(text)) prefixCoveredIds.add(id);
|
|
1378
|
+
}
|
|
1379
|
+
const currentProjection = fullProjection(ctx.sessionManager.getBranch() as Entry[]);
|
|
1380
|
+
const checkpoint = {
|
|
1381
|
+
version: 2,
|
|
1382
|
+
compacted: true,
|
|
1383
|
+
message: summaryMessage,
|
|
1384
|
+
retainedMessageEntryIds,
|
|
1385
|
+
coveredObservationIds: currentProjection.observations.filter((item) => prefixCoveredIds.has(item.id)).map((item) => item.id),
|
|
1386
|
+
coveredReviewIds: (currentProjection.reviews ?? []).filter((item) => prefixCoveredIds.has(item.id)).map((item) => item.id),
|
|
1387
|
+
};
|
|
1363
1388
|
const checkpointEntryId = this.appendContemplatorHistoryEntry(ctx, checkpoint);
|
|
1364
1389
|
this.history = [summaryMessage, ...retainedMessages];
|
|
1365
1390
|
this.historyEntryIds = [checkpointEntryId, ...retainedMessageEntryIds];
|
|
@@ -4,6 +4,7 @@ import { Type } from "@earendil-works/pi-ai";
|
|
|
4
4
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
5
5
|
import type { Static } from "typebox";
|
|
6
6
|
import { hashId } from "../../ids.js";
|
|
7
|
+
import { replayTruncatedThinkingAsText } from "../replay-truncated-thinking.js";
|
|
7
8
|
import { logAgentStreamError } from "../stream-errors.js";
|
|
8
9
|
import { OBSERVER_AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "../../model-budget.js";
|
|
9
10
|
import { OBSERVER_SYSTEM } from "./prompts.js";
|
|
@@ -205,7 +206,7 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
205
206
|
apiKey,
|
|
206
207
|
headers,
|
|
207
208
|
maxTokens: boundedMaxTokens(model, OBSERVER_AGENT_LOOP_MAX_TOKENS),
|
|
208
|
-
convertToLlm:
|
|
209
|
+
convertToLlm: replayTruncatedThinkingAsText,
|
|
209
210
|
toolExecution: "sequential",
|
|
210
211
|
shouldStopAfterTurn: () => {
|
|
211
212
|
turnCount++;
|
|
@@ -270,9 +271,11 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
270
271
|
// maximum. agentLoop stops on `length` when no tool call was completed; it
|
|
271
272
|
// does not automatically send a continuation request. Preserve the partial
|
|
272
273
|
// response so the model can continue from work it already performed rather
|
|
273
|
-
// than paying to reproduce it
|
|
274
|
-
//
|
|
275
|
-
//
|
|
274
|
+
// than paying to reproduce it. Plaintext thinking is replayed as ordinary
|
|
275
|
+
// assistant text at the LLM boundary because some provider templates strip
|
|
276
|
+
// historical reasoning; encrypted thinking retains its opaque structure.
|
|
277
|
+
// Then append a short tool-focused instruction and reduce reasoning to
|
|
278
|
+
// minimal. A second length stop fails forward at the bounded-chunk level.
|
|
276
279
|
terminalFailure = undefined;
|
|
277
280
|
const retryPrompt: Message = {
|
|
278
281
|
role: "user",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Many provider chat templates discard historical reasoning blocks. Preserve
|
|
6
|
+
* unfinished plaintext work after an output-length stop by replaying it as
|
|
7
|
+
* ordinary assistant text. Redacted/encrypted blocks remain structured so
|
|
8
|
+
* their opaque provider payload stays replayable. The original transcript is
|
|
9
|
+
* never mutated; this transformation is only applied at the LLM boundary.
|
|
10
|
+
*/
|
|
11
|
+
export function replayTruncatedThinkingAsText(messages: readonly AgentMessage[]): Message[] {
|
|
12
|
+
return messages.map((message) => {
|
|
13
|
+
if (message.role !== "assistant" || message.stopReason !== "length" || !message.content.some((part) => part.type === "thinking" && !part.redacted)) return message as Message;
|
|
14
|
+
return {
|
|
15
|
+
...message,
|
|
16
|
+
content: message.content.map((part) => part.type === "thinking" && !part.redacted
|
|
17
|
+
? { type: "text" as const, text: `[Incomplete analysis from the preceding truncated response]\n${part.thinking}` }
|
|
18
|
+
: part),
|
|
19
|
+
} as Message;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { estimateStringTokens } from "../../tokens.js";
|
|
27
27
|
import { createRecallAgentTool } from "../../tools/recall-observation.js";
|
|
28
28
|
import { createSearchMemoriesAgentTool } from "../../tools/search-memories.js";
|
|
29
|
+
import { replayTruncatedThinkingAsText } from "../replay-truncated-thinking.js";
|
|
29
30
|
import { logAgentStreamError } from "../stream-errors.js";
|
|
30
31
|
import { summarizerContinue, SUMMARIZER_SYSTEM } from "./prompts.js";
|
|
31
32
|
import {
|
|
@@ -106,27 +107,6 @@ function textResult(text: string, details: Record<string, unknown> = {}, termina
|
|
|
106
107
|
return { content: [{ type: "text" as const, text }], details, ...(terminate ? { terminate: true } : {}) };
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
/**
|
|
110
|
-
* Many provider chat templates discard historical reasoning blocks. Preserve
|
|
111
|
-
* unfinished plaintext work after an output-length stop by replaying it as
|
|
112
|
-
* ordinary assistant text; unlike provider-specific thinking metadata, text
|
|
113
|
-
* survives every supported conversation serializer. Redacted/encrypted blocks
|
|
114
|
-
* must remain structured so their opaque provider payload stays replayable.
|
|
115
|
-
* The durable/in-memory transcript remains unchanged—this transformation is
|
|
116
|
-
* only applied at the LLM boundary.
|
|
117
|
-
*/
|
|
118
|
-
export function replayTruncatedThinkingAsText(messages: readonly AgentMessage[]): Message[] {
|
|
119
|
-
return messages.map((message) => {
|
|
120
|
-
if (message.role !== "assistant" || message.stopReason !== "length" || !message.content.some((part) => part.type === "thinking" && !part.redacted)) return message as Message;
|
|
121
|
-
return {
|
|
122
|
-
...message,
|
|
123
|
-
content: message.content.map((part) => part.type === "thinking" && !part.redacted
|
|
124
|
-
? { type: "text" as const, text: `[Incomplete analysis from the preceding truncated response]\n${part.thinking}` }
|
|
125
|
-
: part),
|
|
126
|
-
} as Message;
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
110
|
function preview(content: string): string {
|
|
131
111
|
const compact = content.replace(/\s+/g, " ").trim();
|
|
132
112
|
return compact.length <= 100 ? compact : `${compact.slice(0, 100)}…`;
|
package/src/commands/status.ts
CHANGED
|
@@ -62,10 +62,6 @@ function truncateStatusText(value: string, limit = 1_000): string {
|
|
|
62
62
|
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function tokenSum(items: { tokenCount: number }[]): number {
|
|
66
|
-
return items.reduce((sum, item) => sum + item.tokenCount, 0);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
65
|
function addedSuffix(count: number): string | undefined {
|
|
70
66
|
return count > 0 ? `+${count.toLocaleString()}` : undefined;
|
|
71
67
|
}
|
|
@@ -90,8 +86,6 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
90
86
|
const full = fullProjection(entries);
|
|
91
87
|
const drift = diffProjection(visible, full);
|
|
92
88
|
|
|
93
|
-
const visibleObservationTokens = tokenSum(visible.observations);
|
|
94
|
-
const visibleSummaryTokens = tokenSum(visible.summaries);
|
|
95
89
|
const pools = partitionMemoryPools(folded.activeObservations, folded.activeSummaries, runtime.config.newMemoryPoolMaxTokens);
|
|
96
90
|
const observationLine = appendSuffixes(
|
|
97
91
|
`Observations: ${folded.observations.length} recorded / ${folded.activeObservations.length} active / ${visible.observations.length} visible`,
|
|
@@ -126,10 +120,9 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
126
120
|
`Observer source backlog: ~${obsProgress.toLocaleString()} / ${runtime.config.observeAfterTokens.toLocaleString()} tokens (${pct(obsProgress, runtime.config.observeAfterTokens)}%)`,
|
|
127
121
|
`Summarizer trigger: old pool ~${pools.oldTokens.toLocaleString()} / ${summarizerTrigger.toLocaleString()} tokens (${pct(pools.oldTokens, summarizerTrigger)}%)`,
|
|
128
122
|
`Automatic compaction source backlog: ~${compactionProgress.toLocaleString()} / ${compactThreshold.toLocaleString()} tokens (${pct(compactionProgress, compactThreshold)}%; injected memory excluded)`,
|
|
129
|
-
`
|
|
123
|
+
`Active memory total: ~${pools.totalTokens.toLocaleString()} tokens (observations + summaries; split below)`,
|
|
130
124
|
`New memory pool: ~${pools.newTokens.toLocaleString()} / ${runtime.config.newMemoryPoolMaxTokens.toLocaleString()} protection-budget tokens (${pct(pools.newTokens, runtime.config.newMemoryPoolMaxTokens)}%; newest memory always protected whole)`,
|
|
131
|
-
`Old memory pool: ~${pools.oldTokens.toLocaleString()} / ${runtime.config.oldMemoryPoolTargetTokens.toLocaleString()} advisory target tokens (${pct(pools.oldTokens, runtime.config.oldMemoryPoolTargetTokens)}
|
|
132
|
-
`Summary pool: ~${visibleSummaryTokens.toLocaleString()} visible tokens`,
|
|
125
|
+
`Old memory pool: ~${pools.oldTokens.toLocaleString()} / ${runtime.config.oldMemoryPoolTargetTokens.toLocaleString()} advisory target tokens (${pct(pools.oldTokens, runtime.config.oldMemoryPoolTargetTokens)}%; observations + summaries)`,
|
|
133
126
|
`Summarizer: ${runtime.config.summarizerEnabled === false ? "disabled" : "enabled"}; retrigger after +${runtime.config.summarizerRetriggerTokens.toLocaleString()} old-pool tokens / sample above ~${summarizerSamplingTokens.toLocaleString()} tokens`,
|
|
134
127
|
`Summarizer model: ${configuredModelLabel(runtime.configuredMemoryWorkerModel("summarizer"))}`,
|
|
135
128
|
`Observer model: ${configuredModelLabel(runtime.configuredMemoryWorkerModel("observer"))}`,
|