@matthewfl/pi-contemplator 0.1.2 → 0.1.3

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.2",
3
+ "version": "0.1.3",
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",
@@ -41,7 +41,7 @@
41
41
  "typecheck": "tsc --noEmit",
42
42
  "test": "npm run test:unit && npm run test:e2e",
43
43
  "test:unit": "vitest run",
44
- "test:e2e": "node tests/e2e/rpc-contemplator.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-routing-isolation.mjs"
44
+ "test:e2e": "node tests/e2e/rpc-contemplator.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
47
  "@earendil-works/pi-agent-core": "*",
@@ -216,13 +216,17 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
216
216
  const loop = args.agentLoop ?? agentLoop;
217
217
  const history: AgentMessage[] = [];
218
218
  let terminalFailure: { stopReason: string; errorMessage?: string } | undefined;
219
- const runInvocation = async (prompt: Message): Promise<void> => {
219
+ let lengthRetryAttempted = false;
220
+ const runInvocation = async (prompt: Message, afterLength = false): Promise<void> => {
220
221
  const context: AgentContext = {
221
222
  systemPrompt: OBSERVER_SYSTEM,
222
223
  messages: history.slice(),
223
224
  tools: [recordObservations as AgentTool<any>, doneTool],
224
225
  };
225
- const stream = loop([prompt], context, baseConfig, signal, streamSimple);
226
+ const invocationConfig: AgentLoopConfig = afterLength && reasoning
227
+ ? { ...baseConfig, reasoning: "minimal" }
228
+ : baseConfig;
229
+ const stream = loop([prompt], context, invocationConfig, signal, streamSimple);
226
230
  for await (const event of stream) {
227
231
  args.onProgress?.();
228
232
  logAgentStreamError("observer", event);
@@ -243,6 +247,23 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
243
247
  };
244
248
 
245
249
  await runInvocation(initialPrompt);
250
+ if (accumulated.size === 0 && terminalFailure?.stopReason === "length") {
251
+ lengthRetryAttempted = true;
252
+ // A provider can impose a lower output ceiling than the advertised model
253
+ // maximum. agentLoop stops on `length` when no tool call was completed; it
254
+ // does not automatically send a continuation request. Preserve the partial
255
+ // response so the model can continue from work it already performed rather
256
+ // than paying to reproduce it, then append a short tool-focused instruction
257
+ // and reduce reasoning to minimal. A second length stop fails forward at the
258
+ // bounded-chunk level.
259
+ terminalFailure = undefined;
260
+ const retryPrompt: Message = {
261
+ role: "user",
262
+ content: [{ type: "text", text: "IMPORTANT: The previous response reached the provider output limit before recording anything. Continue from the work already above and call record_observations now instead of spending another response budget analyzing." }],
263
+ timestamp: Date.now(),
264
+ };
265
+ await runInvocation(retryPrompt, true);
266
+ }
246
267
  if (accumulated.size === 0 && !doneCalled && !terminalFailure && rejectedTotal === 0) {
247
268
  const reminder: Message = {
248
269
  role: "user",
@@ -257,7 +278,10 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
257
278
  // zero-observation stop is also a valid empty result after the reminder;
258
279
  // actual stream failures, truncation, and malformed records still throw.
259
280
  if (accumulated.size === 0 && terminalFailure) {
260
- throw new ObserverStreamError(terminalFailure.stopReason, terminalFailure.errorMessage);
281
+ const detail = terminalFailure.stopReason === "length" && lengthRetryAttempted
282
+ ? `provider reached the output limit twice without recording an observation (effective max output request: ${baseConfig.maxTokens} tokens)`
283
+ : terminalFailure.errorMessage;
284
+ throw new ObserverStreamError(terminalFailure.stopReason, detail);
261
285
  }
262
286
  if (accumulated.size === 0 && rejectedTotal > 0) {
263
287
  throw new ObserverStreamError("invalid_observations", `${rejectedTotal} proposed observation${rejectedTotal === 1 ? " was" : "s were"} rejected`);
@@ -5,6 +5,7 @@ import { debugLog, withDebugLogContext } from "../debug-log.js";
5
5
  import { resolveObserverChunkMaxTokens } from "../config.js";
6
6
  import type { ResolveResult, Runtime } from "../runtime.js";
7
7
  import { createWorkerStallWatchdog } from "../worker-watchdog.js";
8
+ import { boundedMaxTokens, OBSERVER_AGENT_LOOP_MAX_TOKENS } from "../model-budget.js";
8
9
  import { serializeSourceAddressedBranchEntries } from "../serialize.js";
9
10
  import {
10
11
  OM_SUMMARIZER_COMMIT,
@@ -466,6 +467,9 @@ async function runObserverStage(
466
467
  debugLog("observer.start", {
467
468
  tokens,
468
469
  chunkTokens,
470
+ requestedMaxOutputTokens: OBSERVER_AGENT_LOOP_MAX_TOKENS,
471
+ effectiveMaxOutputTokens: boundedMaxTokens(resolved.model as any, OBSERVER_AGENT_LOOP_MAX_TOKENS),
472
+ advertisedModelMaxTokens: (resolved.model as { maxTokens?: number }).maxTokens,
469
473
  coversUpToId,
470
474
  sourceEntryIds,
471
475
  sourceEntryCount: sourceEntryIds.length,