@matthewfl/pi-contemplator 0.1.15 → 0.1.17
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.17",
|
|
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,11 +50,11 @@
|
|
|
50
50
|
"@earendil-works/pi-tui": "*"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-agent-core": "^0.85.
|
|
54
|
-
"@earendil-works/pi-ai": "^0.85.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "^0.85.
|
|
56
|
-
"@earendil-works/pi-server": "^0.85.
|
|
57
|
-
"@earendil-works/pi-tui": "^0.85.
|
|
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",
|
|
58
58
|
"@types/node": "^22.0.0",
|
|
59
59
|
"typebox": "^1.1.38",
|
|
60
60
|
"typescript": "^5.6.0",
|
|
@@ -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";
|
|
@@ -73,6 +74,8 @@ const RecordObservationsSchema = Type.Object({
|
|
|
73
74
|
|
|
74
75
|
type RecordObservationsArgs = Static<typeof RecordObservationsSchema>;
|
|
75
76
|
|
|
77
|
+
export const OBSERVER_MAX_LENGTH_ATTEMPTS = 4;
|
|
78
|
+
|
|
76
79
|
/** A terminal provider/agent-loop failure that must not advance observation coverage. */
|
|
77
80
|
export class ObserverStreamError extends Error {
|
|
78
81
|
readonly stopReason: string;
|
|
@@ -205,7 +208,7 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
205
208
|
apiKey,
|
|
206
209
|
headers,
|
|
207
210
|
maxTokens: boundedMaxTokens(model, OBSERVER_AGENT_LOOP_MAX_TOKENS),
|
|
208
|
-
convertToLlm:
|
|
211
|
+
convertToLlm: replayTruncatedThinkingAsText,
|
|
209
212
|
toolExecution: "sequential",
|
|
210
213
|
shouldStopAfterTurn: () => {
|
|
211
214
|
turnCount++;
|
|
@@ -217,7 +220,7 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
217
220
|
const loop = args.agentLoop ?? agentLoop;
|
|
218
221
|
const history: AgentMessage[] = [];
|
|
219
222
|
let terminalFailure: { stopReason: string; errorMessage?: string } | undefined;
|
|
220
|
-
let
|
|
223
|
+
let lengthAttempts = 0;
|
|
221
224
|
const runInvocation = async (prompt: Message, afterLength = false): Promise<void> => {
|
|
222
225
|
const context: AgentContext = {
|
|
223
226
|
systemPrompt: OBSERVER_SYSTEM,
|
|
@@ -264,15 +267,18 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
264
267
|
};
|
|
265
268
|
|
|
266
269
|
await runInvocation(initialPrompt);
|
|
267
|
-
if (
|
|
268
|
-
|
|
270
|
+
if (terminalFailure?.stopReason === "length") lengthAttempts = 1;
|
|
271
|
+
while (accumulated.size === 0 && terminalFailure?.stopReason === "length" && lengthAttempts < OBSERVER_MAX_LENGTH_ATTEMPTS) {
|
|
269
272
|
// A provider can impose a lower output ceiling than the advertised model
|
|
270
273
|
// maximum. agentLoop stops on `length` when no tool call was completed; it
|
|
271
274
|
// does not automatically send a continuation request. Preserve the partial
|
|
272
275
|
// response so the model can continue from work it already performed rather
|
|
273
|
-
// than paying to reproduce it
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
+
// than paying to reproduce it. Plaintext thinking is replayed as ordinary
|
|
277
|
+
// assistant text at the LLM boundary because some provider templates strip
|
|
278
|
+
// historical reasoning; encrypted thinking retains its opaque structure.
|
|
279
|
+
// Then append a short tool-focused instruction and reduce reasoning to
|
|
280
|
+
// minimal. The bounded attempt limit prevents pathological chunks from
|
|
281
|
+
// consuming background tokens forever.
|
|
276
282
|
terminalFailure = undefined;
|
|
277
283
|
const retryPrompt: Message = {
|
|
278
284
|
role: "user",
|
|
@@ -280,6 +286,10 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
280
286
|
timestamp: Date.now(),
|
|
281
287
|
};
|
|
282
288
|
await runInvocation(retryPrompt, true);
|
|
289
|
+
// runInvocation mutates this through its async stream callbacks; TypeScript
|
|
290
|
+
// cannot infer that mutation after the explicit reset above.
|
|
291
|
+
const retryFailure = terminalFailure as { stopReason: string; errorMessage?: string } | undefined;
|
|
292
|
+
if (retryFailure?.stopReason === "length") lengthAttempts++;
|
|
283
293
|
}
|
|
284
294
|
if (accumulated.size === 0 && !doneCalled && !terminalFailure && rejectedTotal === 0) {
|
|
285
295
|
const reminder: Message = {
|
|
@@ -295,8 +305,8 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
295
305
|
// zero-observation stop is also a valid empty result after the reminder;
|
|
296
306
|
// actual stream failures, truncation, and malformed records still throw.
|
|
297
307
|
if (accumulated.size === 0 && terminalFailure) {
|
|
298
|
-
const detail = terminalFailure.stopReason === "length" &&
|
|
299
|
-
? `provider reached the output limit
|
|
308
|
+
const detail = terminalFailure.stopReason === "length" && lengthAttempts > 0
|
|
309
|
+
? `provider reached the output limit ${lengthAttempts} times without recording an observation (effective max output request: ${baseConfig.maxTokens} tokens)`
|
|
300
310
|
: terminalFailure.errorMessage;
|
|
301
311
|
throw new ObserverStreamError(terminalFailure.stopReason, detail);
|
|
302
312
|
}
|
|
@@ -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"))}`,
|