@matthewfl/pi-contemplator 0.1.16 → 0.1.18

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.16",
3
+ "version": "0.1.18",
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",
@@ -74,6 +74,8 @@ const RecordObservationsSchema = Type.Object({
74
74
 
75
75
  type RecordObservationsArgs = Static<typeof RecordObservationsSchema>;
76
76
 
77
+ export const OBSERVER_MAX_LENGTH_ATTEMPTS = 4;
78
+
77
79
  /** A terminal provider/agent-loop failure that must not advance observation coverage. */
78
80
  export class ObserverStreamError extends Error {
79
81
  readonly stopReason: string;
@@ -218,7 +220,7 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
218
220
  const loop = args.agentLoop ?? agentLoop;
219
221
  const history: AgentMessage[] = [];
220
222
  let terminalFailure: { stopReason: string; errorMessage?: string } | undefined;
221
- let lengthRetryAttempted = false;
223
+ let lengthAttempts = 0;
222
224
  const runInvocation = async (prompt: Message, afterLength = false): Promise<void> => {
223
225
  const context: AgentContext = {
224
226
  systemPrompt: OBSERVER_SYSTEM,
@@ -265,8 +267,8 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
265
267
  };
266
268
 
267
269
  await runInvocation(initialPrompt);
268
- if (accumulated.size === 0 && terminalFailure?.stopReason === "length") {
269
- lengthRetryAttempted = true;
270
+ if (terminalFailure?.stopReason === "length") lengthAttempts = 1;
271
+ while (accumulated.size === 0 && terminalFailure?.stopReason === "length" && lengthAttempts < OBSERVER_MAX_LENGTH_ATTEMPTS) {
270
272
  // A provider can impose a lower output ceiling than the advertised model
271
273
  // maximum. agentLoop stops on `length` when no tool call was completed; it
272
274
  // does not automatically send a continuation request. Preserve the partial
@@ -275,7 +277,8 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
275
277
  // assistant text at the LLM boundary because some provider templates strip
276
278
  // historical reasoning; encrypted thinking retains its opaque structure.
277
279
  // Then append a short tool-focused instruction and reduce reasoning to
278
- // minimal. A second length stop fails forward at the bounded-chunk level.
280
+ // minimal. The bounded attempt limit prevents pathological chunks from
281
+ // consuming background tokens forever.
279
282
  terminalFailure = undefined;
280
283
  const retryPrompt: Message = {
281
284
  role: "user",
@@ -283,6 +286,10 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
283
286
  timestamp: Date.now(),
284
287
  };
285
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++;
286
293
  }
287
294
  if (accumulated.size === 0 && !doneCalled && !terminalFailure && rejectedTotal === 0) {
288
295
  const reminder: Message = {
@@ -298,8 +305,8 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
298
305
  // zero-observation stop is also a valid empty result after the reminder;
299
306
  // actual stream failures, truncation, and malformed records still throw.
300
307
  if (accumulated.size === 0 && terminalFailure) {
301
- const detail = terminalFailure.stopReason === "length" && lengthRetryAttempted
302
- ? `provider reached the output limit twice without recording an observation (effective max output request: ${baseConfig.maxTokens} tokens)`
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)`
303
310
  : terminalFailure.errorMessage;
304
311
  throw new ObserverStreamError(terminalFailure.stopReason, detail);
305
312
  }
@@ -20,12 +20,11 @@ export function createCompactContextTool(runtime: Runtime) {
20
20
  promptGuidelines: [
21
21
  "Use compact_context sparingly when either substantial additional work remains and there is not enough context left to complete it, or accumulated past context has become noisy, stale, or distracting enough that you are struggling to focus on the current task or reason about it reliably.",
22
22
  "Do not use compact_context routinely, for short tasks, or merely because the conversation is long; use it for genuine context-capacity pressure or context degradation that is interfering with the work.",
23
- "Call compact_context by itself, provide short_continuation_prompt with concrete instructions for the next agent step, and stop the current turn; pi-contemplator will compact the context and automatically resume with those instructions.",
23
+ "Call compact_context by itself and provide short_continuation_prompt with concrete instructions for the next agent step. The tool ends the current turn automatically; pi-contemplator will compact the context and resume with those instructions, so do not add a separate response after the tool call.",
24
24
  ],
25
25
  parameters: Type.Object({
26
26
  short_continuation_prompt: Type.String({
27
27
  minLength: 1,
28
- maxLength: 1_000,
29
28
  pattern: "\\S",
30
29
  description: "Short, concrete instructions to your post-compaction self describing the next action and any critical immediate constraint. Do not summarize the whole conversation.",
31
30
  }),
@@ -33,14 +32,14 @@ export function createCompactContextTool(runtime: Runtime) {
33
32
  async execute(_toolCallId, params) {
34
33
  if (runtime.compactInFlight) {
35
34
  return {
36
- content: [{ type: "text" as const, text: "Context compaction is already in progress. Stop this turn and wait for automatic resume." }],
35
+ content: [{ type: "text" as const, text: "Context compaction is already in progress. This turn is ending automatically; wait for automatic resume." }],
37
36
  details: { status: "in_progress" } as CompactContextDetails,
38
37
  terminate: true,
39
38
  };
40
39
  }
41
40
  if (runtime.compactRequested) {
42
41
  return {
43
- content: [{ type: "text" as const, text: "Context compaction is already scheduled. Stop this turn and wait for automatic resume." }],
42
+ content: [{ type: "text" as const, text: "Context compaction is already scheduled. This turn is ending automatically; wait for automatic resume." }],
44
43
  details: { status: "already_pending" } as CompactContextDetails,
45
44
  terminate: true,
46
45
  };
@@ -49,7 +48,7 @@ export function createCompactContextTool(runtime: Runtime) {
49
48
  runtime.compactRequested = true;
50
49
  runtime.compactContinuationPrompt = params.short_continuation_prompt.trim();
51
50
  return {
52
- content: [{ type: "text" as const, text: "Context compaction scheduled. Stop this turn; the task will resume automatically after compaction." }],
51
+ content: [{ type: "text" as const, text: "Context compaction scheduled. This turn is ending automatically, and the task will resume after compaction." }],
53
52
  details: { status: "scheduled" } as CompactContextDetails,
54
53
  terminate: true,
55
54
  };