@matthewfl/pi-contemplator 0.2.0 → 0.3.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 CHANGED
@@ -157,7 +157,7 @@ The observer and summarizer provide the durable memory substrate. The contemplat
157
157
 
158
158
  ## Installation
159
159
 
160
- `pi-contemplator` 0.2.0 requires **Pi 0.86.x**.
160
+ `pi-contemplator` 0.3.0 requires **Pi 0.87.x**.
161
161
 
162
162
  Install the published package through Pi:
163
163
 
@@ -203,4 +203,4 @@ npm run typecheck
203
203
 
204
204
  ## License
205
205
 
206
- MIT. See [LICENSE](LICENSE).
206
+ MIT. See [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthewfl/pi-contemplator",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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",
@@ -44,17 +44,17 @@
44
44
  "test:e2e": "node tests/e2e/rpc-contemplator.mjs && node tests/e2e/rpc-contemplator-history.mjs && node tests/e2e/rpc-summarizer.mjs && node tests/e2e/rpc-delivery.mjs && node tests/e2e/rpc-restore-review.mjs && node tests/e2e/rpc-compaction.mjs && node tests/e2e/rpc-compaction-resilience.mjs && node tests/e2e/rpc-memory-edges.mjs && node tests/e2e/rpc-observer-length.mjs && node tests/e2e/rpc-routing-isolation.mjs"
45
45
  },
46
46
  "peerDependencies": {
47
- "@earendil-works/pi-agent-core": "^0.86.0",
48
- "@earendil-works/pi-ai": "^0.86.0",
49
- "@earendil-works/pi-coding-agent": "^0.86.0",
50
- "@earendil-works/pi-tui": "^0.86.0"
47
+ "@earendil-works/pi-agent-core": "^0.87.0",
48
+ "@earendil-works/pi-ai": "^0.87.0",
49
+ "@earendil-works/pi-coding-agent": "^0.87.0",
50
+ "@earendil-works/pi-tui": "^0.87.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@earendil-works/pi-agent-core": "^0.86.0",
54
- "@earendil-works/pi-ai": "^0.86.0",
55
- "@earendil-works/pi-coding-agent": "^0.86.0",
56
- "@earendil-works/pi-server": "^0.86.0",
57
- "@earendil-works/pi-tui": "^0.86.0",
53
+ "@earendil-works/pi-agent-core": "^0.87.0",
54
+ "@earendil-works/pi-ai": "^0.87.0",
55
+ "@earendil-works/pi-coding-agent": "^0.87.0",
56
+ "@earendil-works/pi-server": "^0.87.0",
57
+ "@earendil-works/pi-tui": "^0.87.0",
58
58
  "@types/node": "^22.0.0",
59
59
  "typebox": "^1.1.38",
60
60
  "typescript": "^5.6.0",
@@ -885,7 +885,10 @@ export class Contemplator {
885
885
  // A clean final-action call is the end of the contemplator turn. Do not
886
886
  // spend another model request asking it to narrate after its decision.
887
887
  // Citation warnings leave the loop open so it can correct the action.
888
- shouldStopAfterTurn: () => intervention !== undefined && !finalActionWarned,
888
+ finishTurn: ({ message }) => {
889
+ if (message.stopReason === "error" || message.stopReason === "aborted") return undefined;
890
+ return intervention !== undefined && !finalActionWarned ? { action: "end" } : undefined;
891
+ },
889
892
  ...(supportsReasoning && selectedThinkingLevel !== "off" ? { reasoning: selectedThinkingLevel } : {}),
890
893
  };
891
894
  const runMessages: AgentMessage[] = [];
@@ -206,9 +206,12 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
206
206
  maxTokens: boundedMaxTokens(model, OBSERVER_AGENT_LOOP_MAX_TOKENS),
207
207
  convertToLlm: replayTruncatedThinkingAsText,
208
208
  toolExecution: "sequential",
209
- shouldStopAfterTurn: () => {
209
+ finishTurn: ({ message }) => {
210
+ if (message.stopReason === "error" || message.stopReason === "aborted") return undefined;
210
211
  turnCount++;
211
- return doneCalled || (effectiveMaxTurns !== undefined && turnCount >= effectiveMaxTurns);
212
+ return doneCalled || (effectiveMaxTurns !== undefined && turnCount >= effectiveMaxTurns)
213
+ ? { action: "end" }
214
+ : undefined;
212
215
  },
213
216
  ...(reasoning && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
214
217
  };
@@ -165,7 +165,10 @@ export async function runStructuralReview(args: RunStructuralReviewArgs): Promis
165
165
  maxTokens: boundedMaxTokens(args.model, remainingBudget()),
166
166
  convertToLlm: (messages) => messages as Message[],
167
167
  toolExecution: "sequential",
168
- shouldStopAfterTurn: () => terminal !== undefined || remainingBudget() === 0,
168
+ finishTurn: ({ message }) => {
169
+ if (message.stopReason === "error" || message.stopReason === "aborted") return undefined;
170
+ return terminal !== undefined || remainingBudget() === 0 ? { action: "end" } : undefined;
171
+ },
169
172
  };
170
173
  const promptMessage = prompt as AgentMessage;
171
174
  // agentLoop receives the new prompt separately. Its context must therefore
@@ -197,8 +200,8 @@ export async function runStructuralReview(args: RunStructuralReviewArgs): Promis
197
200
  // assistant usage that the real stream wrapper did not already account for.
198
201
  const reportedOutputTokens = assistantOutputTokens(assistants);
199
202
  totalOutputTokens += Math.max(0, reportedOutputTokens - streamedOutputTokens);
200
- // A warned terminal call deliberately leaves shouldStopAfterTurn false so
201
- // the reviewer can search/recall and replace it. If the reviewer instead
203
+ // A warned terminal call deliberately leaves finishTurn undecided so the
204
+ // reviewer can search/recall and replace it. If the reviewer instead
202
205
  // ends its turn, honor the staged outcome exactly as the warning promised.
203
206
  if (!terminal && warnedTerminal) {
204
207
  terminal = warnedTerminal;
@@ -546,7 +546,12 @@ export async function runSummarizer(args: RunSummarizerArgs): Promise<Summarizer
546
546
  if (calls.length > 1) return { block: true, reason: "Call done alone in a later response after sibling tool results are visible." };
547
547
  return undefined;
548
548
  },
549
- shouldStopAfterTurn: () => completedWithDone || (effectiveMaxTurns !== undefined && ++turnCount >= effectiveMaxTurns),
549
+ finishTurn: ({ message }) => {
550
+ if (message.stopReason === "error" || message.stopReason === "aborted") return undefined;
551
+ return completedWithDone || (effectiveMaxTurns !== undefined && ++turnCount >= effectiveMaxTurns)
552
+ ? { action: "end" }
553
+ : undefined;
554
+ },
550
555
  ...(requireToolCall ? { onPayload: (payload: unknown) => forceRequiredToolPayload(payload, args.model.api) } : {}),
551
556
  ...(reasoning && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
552
557
  };
@@ -7,9 +7,16 @@ import {
7
7
  type MemoryCoverageCustomType,
8
8
  } from "./types.js";
9
9
 
10
- const SOURCE_ENTRY_TYPES = new Set(["message", "custom_message", "branch_summary"]);
10
+ const SOURCE_ENTRY_TYPES = new Set(["custom_message", "branch_summary"]);
11
11
 
12
12
  export function isSourceEntry(entry: Entry): boolean {
13
+ if (entry.type === "message") {
14
+ // Pi 0.87 persists its canonical system prompt as a message entry. It is
15
+ // extension/runtime configuration, not new conversation evidence; counting
16
+ // its large sections makes a fresh session immediately cross the observer
17
+ // threshold even though only a few hundred user-visible tokens exist.
18
+ return !isObject(entry.message) || entry.message.role !== "system";
19
+ }
13
20
  return SOURCE_ENTRY_TYPES.has(entry.type);
14
21
  }
15
22