@averyyy/pi-coding-agent 0.80.3-piclient.3 → 0.80.3-piclient.6

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.
@@ -62,12 +62,36 @@ function estimateMessagesTokens(messages) {
62
62
  // ============================================================================
63
63
  /** Standard thinking levels */
64
64
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
65
+ const INTRA_TURN_COMPACTION_MARKER_TYPE = "pi:intra-turn-compaction";
66
+ const INTRA_TURN_COMPACTION_MARKER_TEXT = "Continue from the compaction summary above. The previous tool loop was compacted before the next provider request; use the summary's Operational State, file lists, failures, last command, and next steps.";
67
+ const VALIDATION_HINT_MESSAGE_TYPE = "pi:validation-hint";
68
+ const VALIDATION_HINT_FILE_LIMIT = 12;
69
+ const VALIDATION_HINT_FAILURE_CHAR_LIMIT = 1200;
65
70
  function isPiServerMode() {
66
71
  return process.env.PI_SERVER_MODE === "true";
67
72
  }
68
73
  function toProviderReasoning(thinkingLevel) {
69
74
  return thinkingLevel === "off" ? undefined : thinkingLevel;
70
75
  }
76
+ function getStringField(input, key) {
77
+ if (!input || typeof input !== "object" || Array.isArray(input))
78
+ return undefined;
79
+ const value = input[key];
80
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
81
+ }
82
+ function toolTextContent(content) {
83
+ return content
84
+ .filter((part) => part.type === "text")
85
+ .map((part) => part.text)
86
+ .join("\n")
87
+ .trim();
88
+ }
89
+ function compactValidationSummary(text) {
90
+ const trimmed = text.trim();
91
+ if (trimmed.length <= VALIDATION_HINT_FAILURE_CHAR_LIMIT)
92
+ return trimmed;
93
+ return `...${trimmed.slice(trimmed.length - VALIDATION_HINT_FAILURE_CHAR_LIMIT + 3)}`;
94
+ }
71
95
  // ============================================================================
72
96
  // AgentSession Class
73
97
  // ============================================================================
@@ -98,6 +122,8 @@ export class AgentSession {
98
122
  // Bash execution state
99
123
  _bashAbortController = undefined;
100
124
  _pendingBashMessages = [];
125
+ _validationModifiedFiles = new Set();
126
+ _validationFailure = undefined;
101
127
  // Extension system
102
128
  _extensionRunner;
103
129
  _turnIndex = 0;
@@ -197,6 +223,71 @@ export class AgentSession {
197
223
  const result = await this._modelRegistry.getApiKeyAndHeaders(model);
198
224
  return result.ok ? { apiKey: result.apiKey, headers: result.headers, env: result.env } : {};
199
225
  }
226
+ _resetValidationHintState() {
227
+ this._validationModifiedFiles.clear();
228
+ this._validationFailure = undefined;
229
+ }
230
+ _recordValidationToolResult(toolName, input, content, isError) {
231
+ if ((toolName === "edit" || toolName === "write") && !isError) {
232
+ const path = getStringField(input, "path") ?? getStringField(input, "file_path");
233
+ if (path) {
234
+ this._validationModifiedFiles.add(path);
235
+ }
236
+ return;
237
+ }
238
+ if (toolName !== "bash") {
239
+ return;
240
+ }
241
+ const command = getStringField(input, "command");
242
+ if (!command) {
243
+ return;
244
+ }
245
+ if (isError) {
246
+ this._validationFailure = {
247
+ command,
248
+ summary: compactValidationSummary(toolTextContent(content)),
249
+ };
250
+ }
251
+ else if (this._validationFailure?.command === command) {
252
+ this._resetValidationHintState();
253
+ }
254
+ else if (!this._validationFailure) {
255
+ this._validationModifiedFiles.clear();
256
+ }
257
+ }
258
+ _createValidationHintMessage() {
259
+ const modifiedFiles = Array.from(this._validationModifiedFiles).slice(-VALIDATION_HINT_FILE_LIMIT);
260
+ if (modifiedFiles.length === 0 && !this._validationFailure) {
261
+ return undefined;
262
+ }
263
+ const lines = ["<validation-hint>"];
264
+ if (modifiedFiles.length > 0) {
265
+ lines.push("Recent modified files:", ...modifiedFiles.map((file) => `- ${file}`));
266
+ }
267
+ if (this._validationFailure) {
268
+ lines.push("Recent failed command:", this._validationFailure.command, "Failure summary:", this._validationFailure.summary || "(no output)", "Suggested next validation command:", this._validationFailure.command);
269
+ }
270
+ else {
271
+ lines.push("Suggested next step:", "Run the smallest project validation that covers the modified files.");
272
+ }
273
+ lines.push("Inspect failures, fix the root cause, then rerun validation before finalizing.", "</validation-hint>");
274
+ return {
275
+ role: "custom",
276
+ customType: VALIDATION_HINT_MESSAGE_TYPE,
277
+ content: [{ type: "text", text: lines.join("\n") }],
278
+ display: false,
279
+ details: { modifiedFiles, failedCommand: this._validationFailure?.command },
280
+ timestamp: Date.now(),
281
+ };
282
+ }
283
+ _withValidationHint(context) {
284
+ const messages = context.messages.filter((message) => !(message.role === "custom" && message.customType === VALIDATION_HINT_MESSAGE_TYPE));
285
+ const hint = this._createValidationHintMessage();
286
+ return {
287
+ ...context,
288
+ messages: hint ? [...messages, hint] : messages,
289
+ };
290
+ }
200
291
  /**
201
292
  * Install tool hooks once on the Agent instance.
202
293
  *
@@ -228,18 +319,20 @@ export class AgentSession {
228
319
  };
229
320
  this.agent.afterToolCall = async ({ toolCall, args, result, isError }) => {
230
321
  const runner = this._extensionRunner;
231
- if (!runner.hasHandlers("tool_result")) {
232
- return undefined;
233
- }
234
- const hookResult = await runner.emitToolResult({
235
- type: "tool_result",
236
- toolName: toolCall.name,
237
- toolCallId: toolCall.id,
238
- input: args,
239
- content: result.content,
240
- details: result.details,
241
- isError,
242
- });
322
+ const hookResult = runner.hasHandlers("tool_result")
323
+ ? await runner.emitToolResult({
324
+ type: "tool_result",
325
+ toolName: toolCall.name,
326
+ toolCallId: toolCall.id,
327
+ input: args,
328
+ content: result.content,
329
+ details: result.details,
330
+ isError,
331
+ })
332
+ : undefined;
333
+ const finalContent = hookResult?.content ?? result.content;
334
+ const finalIsError = hookResult?.isError ?? isError;
335
+ this._recordValidationToolResult(toolCall.name, args, finalContent, finalIsError);
243
336
  if (!hookResult) {
244
337
  return undefined;
245
338
  }
@@ -256,15 +349,17 @@ export class AgentSession {
256
349
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
257
350
  : undefined);
258
351
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
259
- const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
260
- const previousContext = previousSnapshot?.context ?? turn.context;
352
+ const compactedContext = await this._compactBeforeNextProviderRequest(turn, signal);
353
+ const nextTurn = compactedContext ? { ...turn, context: compactedContext } : turn;
354
+ const previousSnapshot = await previousPrepareNextTurnWithContext?.(nextTurn, signal);
355
+ const previousContext = previousSnapshot?.context ?? nextTurn.context;
261
356
  return {
262
357
  ...previousSnapshot,
263
- context: {
358
+ context: this._withValidationHint({
264
359
  ...previousContext,
265
360
  systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
266
361
  tools: this.agent.state.tools.slice(),
267
- },
362
+ }),
268
363
  model: this.agent.state.model,
269
364
  thinkingLevel: this.agent.state.thinkingLevel,
270
365
  };
@@ -394,6 +489,20 @@ export class AgentSession {
394
489
  _isTerminalAssistantFailure(message) {
395
490
  return message.stopReason === "error" || message.stopReason === "aborted" || message.stopReason === "length";
396
491
  }
492
+ _isPostAssistantPiServerSyncFailure(message) {
493
+ if (!isPiServerMode() || !message.errorMessage) {
494
+ return false;
495
+ }
496
+ if (!/^(Session init|Session tree (append|sync|switch)|Session history reconciliation) failed/.test(message.errorMessage)) {
497
+ return false;
498
+ }
499
+ const messages = this.agent.state.messages;
500
+ if (messages[messages.length - 1] !== message) {
501
+ return false;
502
+ }
503
+ const previous = messages[messages.length - 2];
504
+ return previous?.role === "assistant" && !this._isTerminalAssistantFailure(previous);
505
+ }
397
506
  _detachTerminalAssistantFailure() {
398
507
  const branch = this.sessionManager.getBranch();
399
508
  let terminalMessageEntry;
@@ -423,6 +532,16 @@ export class AgentSession {
423
532
  _detachPiServerTerminalAssistantFailure() {
424
533
  this._detachTerminalAssistantFailure();
425
534
  }
535
+ _restoreSessionLeaf(leafId) {
536
+ if (leafId) {
537
+ this.sessionManager.branch(leafId);
538
+ }
539
+ else {
540
+ this.sessionManager.resetLeaf();
541
+ }
542
+ const sessionContext = this.sessionManager.buildSessionContext();
543
+ this.agent.state.messages = sessionContext.messages;
544
+ }
426
545
  _replaceMessageInPlace(target, replacement) {
427
546
  // Agent-core stores the finalized message object in its state before emitting message_end.
428
547
  // SessionManager persistence happens later in _handleAgentEvent() with event.message.
@@ -743,10 +862,26 @@ export class AgentSession {
743
862
  // Prompting
744
863
  // =========================================================================
745
864
  async _runAgentPrompt(messages) {
865
+ this._resetValidationHintState();
746
866
  try {
747
867
  await this.agent.prompt(messages);
748
868
  while (await this._handlePostAgentRun()) {
749
- await this.agent.continue();
869
+ try {
870
+ await this.agent.continue();
871
+ }
872
+ catch (error) {
873
+ if (this._retryAttempt > 0) {
874
+ const attempt = this._retryAttempt;
875
+ this._retryAttempt = 0;
876
+ this._emit({
877
+ type: "auto_retry_end",
878
+ success: false,
879
+ attempt,
880
+ finalError: error instanceof Error ? error.message : String(error),
881
+ });
882
+ }
883
+ throw error;
884
+ }
750
885
  }
751
886
  }
752
887
  finally {
@@ -1636,7 +1771,7 @@ export class AgentSession {
1636
1771
  /**
1637
1772
  * Internal: Run auto-compaction with events.
1638
1773
  */
1639
- async _runAutoCompaction(reason, willRetry, preparationOverride) {
1774
+ async _runAutoCompaction(reason, willRetry, preparationOverride, preparationOptions) {
1640
1775
  const settings = this.settingsManager.getCompactionSettings();
1641
1776
  let started = false;
1642
1777
  try {
@@ -1672,6 +1807,7 @@ export class AgentSession {
1672
1807
  apiKey,
1673
1808
  headers,
1674
1809
  settings,
1810
+ preparation: preparationOptions,
1675
1811
  sessionTree: { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() },
1676
1812
  signal,
1677
1813
  reasoning: toProviderReasoning(this.thinkingLevel),
@@ -1812,6 +1948,63 @@ export class AgentSession {
1812
1948
  get autoCompactionEnabled() {
1813
1949
  return this.settingsManager.getCompactionEnabled();
1814
1950
  }
1951
+ _shouldCompactBeforeNextProviderRequest(turn) {
1952
+ const settings = this.settingsManager.getCompactionSettings();
1953
+ if (!settings.enabled || turn.toolResults.length === 0)
1954
+ return false;
1955
+ const contextWindow = this.model?.contextWindow ?? 0;
1956
+ if (contextWindow <= 0)
1957
+ return false;
1958
+ const projectedTokens = estimateContextTokens(turn.context.messages).tokens;
1959
+ if (projectedTokens >= contextWindow - settings.reserveTokens)
1960
+ return true;
1961
+ const toolResultTokens = estimateMessagesTokens(turn.toolResults);
1962
+ return toolResultTokens >= settings.keepRecentTokens;
1963
+ }
1964
+ _appendIntraTurnCompactionMarker() {
1965
+ const marker = {
1966
+ role: "custom",
1967
+ customType: INTRA_TURN_COMPACTION_MARKER_TYPE,
1968
+ content: [{ type: "text", text: INTRA_TURN_COMPACTION_MARKER_TEXT }],
1969
+ display: false,
1970
+ details: { reason: "intra-turn-compaction" },
1971
+ timestamp: Date.now(),
1972
+ };
1973
+ const markerId = this.sessionManager.appendCustomMessageEntry(marker.customType, marker.content, marker.display, marker.details);
1974
+ this.agent.state.messages = [...this.agent.state.messages, marker];
1975
+ return markerId;
1976
+ }
1977
+ _currentAgentContext(baseContext) {
1978
+ return {
1979
+ ...baseContext,
1980
+ messages: this.agent.state.messages.slice(),
1981
+ systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
1982
+ tools: this.agent.state.tools.slice(),
1983
+ };
1984
+ }
1985
+ async _compactBeforeNextProviderRequest(turn, signal) {
1986
+ if (signal?.aborted || !this._shouldCompactBeforeNextProviderRequest(turn)) {
1987
+ return undefined;
1988
+ }
1989
+ const settings = this.settingsManager.getCompactionSettings();
1990
+ const markerParentId = this.sessionManager.getLeafId();
1991
+ const markerId = this._appendIntraTurnCompactionMarker();
1992
+ const preparationOptions = { firstKeptEntryId: markerId };
1993
+ const branchWithMarker = this.sessionManager.getBranch();
1994
+ const previousCompactionId = getLatestCompactionEntry(branchWithMarker)?.id;
1995
+ const preparation = prepareCompaction(branchWithMarker, settings, preparationOptions);
1996
+ if (!preparation) {
1997
+ this._restoreSessionLeaf(markerParentId);
1998
+ return undefined;
1999
+ }
2000
+ await this._runAutoCompaction("threshold", false, preparation, preparationOptions);
2001
+ const currentCompactionId = getLatestCompactionEntry(this.sessionManager.getBranch())?.id;
2002
+ if (currentCompactionId === previousCompactionId) {
2003
+ this._restoreSessionLeaf(markerParentId);
2004
+ return undefined;
2005
+ }
2006
+ return this._currentAgentContext(turn.context);
2007
+ }
1815
2008
  async bindExtensions(bindings) {
1816
2009
  if (bindings.uiContext !== undefined) {
1817
2010
  this._extensionUIContext = bindings.uiContext;
@@ -2150,6 +2343,8 @@ export class AgentSession {
2150
2343
  // Context overflow is handled by compaction, not retry.
2151
2344
  if (isContextOverflow(message, this.model?.contextWindow ?? 0))
2152
2345
  return false;
2346
+ if (this._isPostAssistantPiServerSyncFailure(message))
2347
+ return false;
2153
2348
  if (message.errorMessage && this._isNonRetryableProviderLimitError(message.errorMessage))
2154
2349
  return false;
2155
2350
  return isRetryableAssistantError(message);
@@ -2187,6 +2382,18 @@ export class AgentSession {
2187
2382
  this.agent.state.messages = messages.slice(0, -1);
2188
2383
  }
2189
2384
  }
2385
+ const lastMessage = this.agent.state.messages[this.agent.state.messages.length - 1];
2386
+ if (!lastMessage || lastMessage.role === "assistant") {
2387
+ const attempt = this._retryAttempt;
2388
+ this._retryAttempt = 0;
2389
+ this._emit({
2390
+ type: "auto_retry_end",
2391
+ success: false,
2392
+ attempt,
2393
+ finalError: lastMessage ? "Cannot continue from message role: assistant" : "No messages to continue from",
2394
+ });
2395
+ return false;
2396
+ }
2190
2397
  // Wait with exponential backoff (abortable)
2191
2398
  this._retryAbortController = new AbortController();
2192
2399
  try {