@averyyy/pi-coding-agent 0.80.3-piclient.5 → 0.80.3-piclient.7

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,84 @@ 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;
70
+ const AUTO_SESSION_NAME_INPUT_CHAR_LIMIT = 4000;
71
+ const AUTO_SESSION_NAME_MAX_CHARS = 80;
72
+ const AUTO_SESSION_NAME_PROMPT = "Name this coding session from the transcript below. Return only a concise title, in the user's language when clear. No quotes, no markdown, no explanation. Do not call tools.";
65
73
  function isPiServerMode() {
66
74
  return process.env.PI_SERVER_MODE === "true";
67
75
  }
68
76
  function toProviderReasoning(thinkingLevel) {
69
77
  return thinkingLevel === "off" ? undefined : thinkingLevel;
70
78
  }
79
+ function getStringField(input, key) {
80
+ if (!input || typeof input !== "object" || Array.isArray(input))
81
+ return undefined;
82
+ const value = input[key];
83
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
84
+ }
85
+ function getPiServerFailurePhase(message) {
86
+ for (const diagnostic of message.diagnostics ?? []) {
87
+ if (diagnostic.type !== "pi_server_failure")
88
+ continue;
89
+ const phase = diagnostic.details?.phase;
90
+ if (typeof phase === "string")
91
+ return phase;
92
+ }
93
+ return undefined;
94
+ }
95
+ function toolTextContent(content) {
96
+ return content
97
+ .filter((part) => part.type === "text")
98
+ .map((part) => part.text)
99
+ .join("\n")
100
+ .trim();
101
+ }
102
+ function compactValidationSummary(text) {
103
+ const trimmed = text.trim();
104
+ if (trimmed.length <= VALIDATION_HINT_FAILURE_CHAR_LIMIT)
105
+ return trimmed;
106
+ return `...${trimmed.slice(trimmed.length - VALIDATION_HINT_FAILURE_CHAR_LIMIT + 3)}`;
107
+ }
108
+ function messageTextContent(message) {
109
+ if (!("content" in message))
110
+ return "";
111
+ const content = message.content;
112
+ if (typeof content === "string")
113
+ return content;
114
+ if (!Array.isArray(content))
115
+ return "";
116
+ return content
117
+ .filter((part) => part.type === "text")
118
+ .map((part) => part.text)
119
+ .join("\n")
120
+ .trim();
121
+ }
122
+ function truncateForSessionNameInput(text) {
123
+ if (text.length <= AUTO_SESSION_NAME_INPUT_CHAR_LIMIT)
124
+ return text;
125
+ return text.slice(0, AUTO_SESSION_NAME_INPUT_CHAR_LIMIT).trim();
126
+ }
127
+ function sanitizeGeneratedSessionName(text) {
128
+ const firstLine = text
129
+ .split(/\r?\n/u)
130
+ .map((line) => line.trim())
131
+ .find((line) => line.length > 0);
132
+ if (!firstLine)
133
+ return undefined;
134
+ const title = firstLine
135
+ .replace(/^title:\s*/iu, "")
136
+ .replace(/^["'`]+|["'`]+$/g, "")
137
+ .replace(/\s+/g, " ")
138
+ .trim();
139
+ if (!title)
140
+ return undefined;
141
+ return title.length > AUTO_SESSION_NAME_MAX_CHARS ? title.slice(0, AUTO_SESSION_NAME_MAX_CHARS).trim() : title;
142
+ }
71
143
  // ============================================================================
72
144
  // AgentSession Class
73
145
  // ============================================================================
@@ -98,6 +170,8 @@ export class AgentSession {
98
170
  // Bash execution state
99
171
  _bashAbortController = undefined;
100
172
  _pendingBashMessages = [];
173
+ _validationModifiedFiles = new Set();
174
+ _validationFailure = undefined;
101
175
  // Extension system
102
176
  _extensionRunner;
103
177
  _turnIndex = 0;
@@ -118,6 +192,7 @@ export class AgentSession {
118
192
  _extensionShutdownHandler;
119
193
  _extensionErrorListener;
120
194
  _extensionErrorUnsubscriber;
195
+ _autoSessionNameAttempted = false;
121
196
  // Model registry for API key resolution
122
197
  _modelRegistry;
123
198
  // Tool registry for extension getTools/setTools
@@ -197,6 +272,71 @@ export class AgentSession {
197
272
  const result = await this._modelRegistry.getApiKeyAndHeaders(model);
198
273
  return result.ok ? { apiKey: result.apiKey, headers: result.headers, env: result.env } : {};
199
274
  }
275
+ _resetValidationHintState() {
276
+ this._validationModifiedFiles.clear();
277
+ this._validationFailure = undefined;
278
+ }
279
+ _recordValidationToolResult(toolName, input, content, isError) {
280
+ if ((toolName === "edit" || toolName === "write") && !isError) {
281
+ const path = getStringField(input, "path") ?? getStringField(input, "file_path");
282
+ if (path) {
283
+ this._validationModifiedFiles.add(path);
284
+ }
285
+ return;
286
+ }
287
+ if (toolName !== "bash") {
288
+ return;
289
+ }
290
+ const command = getStringField(input, "command");
291
+ if (!command) {
292
+ return;
293
+ }
294
+ if (isError) {
295
+ this._validationFailure = {
296
+ command,
297
+ summary: compactValidationSummary(toolTextContent(content)),
298
+ };
299
+ }
300
+ else if (this._validationFailure?.command === command) {
301
+ this._resetValidationHintState();
302
+ }
303
+ else if (!this._validationFailure) {
304
+ this._validationModifiedFiles.clear();
305
+ }
306
+ }
307
+ _createValidationHintMessage() {
308
+ const modifiedFiles = Array.from(this._validationModifiedFiles).slice(-VALIDATION_HINT_FILE_LIMIT);
309
+ if (modifiedFiles.length === 0 && !this._validationFailure) {
310
+ return undefined;
311
+ }
312
+ const lines = ["<validation-hint>"];
313
+ if (modifiedFiles.length > 0) {
314
+ lines.push("Recent modified files:", ...modifiedFiles.map((file) => `- ${file}`));
315
+ }
316
+ if (this._validationFailure) {
317
+ lines.push("Recent failed command:", this._validationFailure.command, "Failure summary:", this._validationFailure.summary || "(no output)", "Suggested next validation command:", this._validationFailure.command);
318
+ }
319
+ else {
320
+ lines.push("Suggested next step:", "Run the smallest project validation that covers the modified files.");
321
+ }
322
+ lines.push("Inspect failures, fix the root cause, then rerun validation before finalizing.", "</validation-hint>");
323
+ return {
324
+ role: "custom",
325
+ customType: VALIDATION_HINT_MESSAGE_TYPE,
326
+ content: [{ type: "text", text: lines.join("\n") }],
327
+ display: false,
328
+ details: { modifiedFiles, failedCommand: this._validationFailure?.command },
329
+ timestamp: Date.now(),
330
+ };
331
+ }
332
+ _withValidationHint(context) {
333
+ const messages = context.messages.filter((message) => !(message.role === "custom" && message.customType === VALIDATION_HINT_MESSAGE_TYPE));
334
+ const hint = this._createValidationHintMessage();
335
+ return {
336
+ ...context,
337
+ messages: hint ? [...messages, hint] : messages,
338
+ };
339
+ }
200
340
  /**
201
341
  * Install tool hooks once on the Agent instance.
202
342
  *
@@ -228,18 +368,20 @@ export class AgentSession {
228
368
  };
229
369
  this.agent.afterToolCall = async ({ toolCall, args, result, isError }) => {
230
370
  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
- });
371
+ const hookResult = runner.hasHandlers("tool_result")
372
+ ? await runner.emitToolResult({
373
+ type: "tool_result",
374
+ toolName: toolCall.name,
375
+ toolCallId: toolCall.id,
376
+ input: args,
377
+ content: result.content,
378
+ details: result.details,
379
+ isError,
380
+ })
381
+ : undefined;
382
+ const finalContent = hookResult?.content ?? result.content;
383
+ const finalIsError = hookResult?.isError ?? isError;
384
+ this._recordValidationToolResult(toolCall.name, args, finalContent, finalIsError);
243
385
  if (!hookResult) {
244
386
  return undefined;
245
387
  }
@@ -256,15 +398,17 @@ export class AgentSession {
256
398
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
257
399
  : undefined);
258
400
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
259
- const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
260
- const previousContext = previousSnapshot?.context ?? turn.context;
401
+ const compactedContext = await this._compactBeforeNextProviderRequest(turn, signal);
402
+ const nextTurn = compactedContext ? { ...turn, context: compactedContext } : turn;
403
+ const previousSnapshot = await previousPrepareNextTurnWithContext?.(nextTurn, signal);
404
+ const previousContext = previousSnapshot?.context ?? nextTurn.context;
261
405
  return {
262
406
  ...previousSnapshot,
263
- context: {
407
+ context: this._withValidationHint({
264
408
  ...previousContext,
265
409
  systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
266
410
  tools: this.agent.state.tools.slice(),
267
- },
411
+ }),
268
412
  model: this.agent.state.model,
269
413
  thinkingLevel: this.agent.state.thinkingLevel,
270
414
  };
@@ -394,6 +538,20 @@ export class AgentSession {
394
538
  _isTerminalAssistantFailure(message) {
395
539
  return message.stopReason === "error" || message.stopReason === "aborted" || message.stopReason === "length";
396
540
  }
541
+ _isPostAssistantPiServerSyncFailure(message) {
542
+ if (!isPiServerMode() || !message.errorMessage) {
543
+ return false;
544
+ }
545
+ if (!/^(Session init|Session tree (append|sync|switch)|Session history reconciliation) failed/.test(message.errorMessage)) {
546
+ return false;
547
+ }
548
+ const messages = this.agent.state.messages;
549
+ if (messages[messages.length - 1] !== message) {
550
+ return false;
551
+ }
552
+ const previous = messages[messages.length - 2];
553
+ return previous?.role === "assistant" && !this._isTerminalAssistantFailure(previous);
554
+ }
397
555
  _detachTerminalAssistantFailure() {
398
556
  const branch = this.sessionManager.getBranch();
399
557
  let terminalMessageEntry;
@@ -423,6 +581,16 @@ export class AgentSession {
423
581
  _detachPiServerTerminalAssistantFailure() {
424
582
  this._detachTerminalAssistantFailure();
425
583
  }
584
+ _restoreSessionLeaf(leafId) {
585
+ if (leafId) {
586
+ this.sessionManager.branch(leafId);
587
+ }
588
+ else {
589
+ this.sessionManager.resetLeaf();
590
+ }
591
+ const sessionContext = this.sessionManager.buildSessionContext();
592
+ this.agent.state.messages = sessionContext.messages;
593
+ }
426
594
  _replaceMessageInPlace(target, replacement) {
427
595
  // Agent-core stores the finalized message object in its state before emitting message_end.
428
596
  // SessionManager persistence happens later in _handleAgentEvent() with event.message.
@@ -743,17 +911,100 @@ export class AgentSession {
743
911
  // Prompting
744
912
  // =========================================================================
745
913
  async _runAgentPrompt(messages) {
914
+ this._resetValidationHintState();
746
915
  try {
747
916
  await this.agent.prompt(messages);
748
917
  while (await this._handlePostAgentRun()) {
749
- await this.agent.continue();
918
+ try {
919
+ await this.agent.continue();
920
+ }
921
+ catch (error) {
922
+ if (this._retryAttempt > 0) {
923
+ const attempt = this._retryAttempt;
924
+ this._retryAttempt = 0;
925
+ this._emit({
926
+ type: "auto_retry_end",
927
+ success: false,
928
+ attempt,
929
+ finalError: error instanceof Error ? error.message : String(error),
930
+ });
931
+ }
932
+ throw error;
933
+ }
750
934
  }
935
+ await this._maybeGenerateSessionName();
751
936
  }
752
937
  finally {
753
938
  this._systemPromptOverride = undefined;
754
939
  this._flushPendingBashMessages();
755
940
  }
756
941
  }
942
+ _sessionHasInfoEntry() {
943
+ return this.sessionManager.getEntries().some((entry) => entry.type === "session_info");
944
+ }
945
+ _buildSessionNameTranscript() {
946
+ const lines = [];
947
+ for (const message of this.agent.state.messages) {
948
+ if (message.role !== "user" && message.role !== "assistant")
949
+ continue;
950
+ const text = messageTextContent(message);
951
+ if (!text)
952
+ continue;
953
+ lines.push(`${message.role === "user" ? "User" : "Assistant"}: ${text}`);
954
+ }
955
+ const transcript = lines.join("\n\n").trim();
956
+ return transcript ? truncateForSessionNameInput(transcript) : undefined;
957
+ }
958
+ _shouldGenerateSessionName() {
959
+ if (this._autoSessionNameAttempted || this._sessionHasInfoEntry())
960
+ return false;
961
+ const userMessages = this.agent.state.messages.filter((message) => message.role === "user");
962
+ if (userMessages.length !== 1)
963
+ return false;
964
+ const lastAssistant = this._findLastAssistantMessage();
965
+ return lastAssistant !== undefined && !this._isTerminalAssistantFailure(lastAssistant);
966
+ }
967
+ async _maybeGenerateSessionName() {
968
+ if (!this._shouldGenerateSessionName())
969
+ return;
970
+ this._autoSessionNameAttempted = true;
971
+ const model = this.model;
972
+ const transcript = this._buildSessionNameTranscript();
973
+ if (!model || !transcript)
974
+ return;
975
+ try {
976
+ const stream = await this.agent.streamFn(model, {
977
+ systemPrompt: this.systemPrompt,
978
+ messages: [
979
+ {
980
+ role: "user",
981
+ content: [{ type: "text", text: `${AUTO_SESSION_NAME_PROMPT}\n\n${transcript}` }],
982
+ timestamp: Date.now(),
983
+ },
984
+ ],
985
+ tools: this.agent.state.tools,
986
+ }, {
987
+ maxTokens: 32,
988
+ reasoning: undefined,
989
+ sessionId: this.sessionId,
990
+ transport: this.agent.transport,
991
+ thinkingBudgets: this.agent.thinkingBudgets,
992
+ maxRetryDelayMs: this.agent.maxRetryDelayMs,
993
+ });
994
+ for await (const _event of stream) {
995
+ // Drain the title stream without surfacing it as a chat message.
996
+ }
997
+ const response = await stream.result();
998
+ if (response.stopReason === "error" || response.stopReason === "aborted")
999
+ return;
1000
+ const title = sanitizeGeneratedSessionName(messageTextContent(response));
1001
+ if (title)
1002
+ this.setSessionName(title);
1003
+ }
1004
+ catch {
1005
+ // Session naming is a post-turn convenience; the completed assistant turn stays authoritative.
1006
+ }
1007
+ }
757
1008
  async _handlePostAgentRun() {
758
1009
  const msg = this._lastAssistantMessage;
759
1010
  this._lastAssistantMessage = undefined;
@@ -1636,7 +1887,7 @@ export class AgentSession {
1636
1887
  /**
1637
1888
  * Internal: Run auto-compaction with events.
1638
1889
  */
1639
- async _runAutoCompaction(reason, willRetry, preparationOverride) {
1890
+ async _runAutoCompaction(reason, willRetry, preparationOverride, preparationOptions) {
1640
1891
  const settings = this.settingsManager.getCompactionSettings();
1641
1892
  let started = false;
1642
1893
  try {
@@ -1672,6 +1923,7 @@ export class AgentSession {
1672
1923
  apiKey,
1673
1924
  headers,
1674
1925
  settings,
1926
+ preparation: preparationOptions,
1675
1927
  sessionTree: { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() },
1676
1928
  signal,
1677
1929
  reasoning: toProviderReasoning(this.thinkingLevel),
@@ -1812,6 +2064,63 @@ export class AgentSession {
1812
2064
  get autoCompactionEnabled() {
1813
2065
  return this.settingsManager.getCompactionEnabled();
1814
2066
  }
2067
+ _shouldCompactBeforeNextProviderRequest(turn) {
2068
+ const settings = this.settingsManager.getCompactionSettings();
2069
+ if (!settings.enabled || turn.toolResults.length === 0)
2070
+ return false;
2071
+ const contextWindow = this.model?.contextWindow ?? 0;
2072
+ if (contextWindow <= 0)
2073
+ return false;
2074
+ const projectedTokens = estimateContextTokens(turn.context.messages).tokens;
2075
+ if (projectedTokens >= contextWindow - settings.reserveTokens)
2076
+ return true;
2077
+ const toolResultTokens = estimateMessagesTokens(turn.toolResults);
2078
+ return toolResultTokens >= settings.keepRecentTokens;
2079
+ }
2080
+ _appendIntraTurnCompactionMarker() {
2081
+ const marker = {
2082
+ role: "custom",
2083
+ customType: INTRA_TURN_COMPACTION_MARKER_TYPE,
2084
+ content: [{ type: "text", text: INTRA_TURN_COMPACTION_MARKER_TEXT }],
2085
+ display: false,
2086
+ details: { reason: "intra-turn-compaction" },
2087
+ timestamp: Date.now(),
2088
+ };
2089
+ const markerId = this.sessionManager.appendCustomMessageEntry(marker.customType, marker.content, marker.display, marker.details);
2090
+ this.agent.state.messages = [...this.agent.state.messages, marker];
2091
+ return markerId;
2092
+ }
2093
+ _currentAgentContext(baseContext) {
2094
+ return {
2095
+ ...baseContext,
2096
+ messages: this.agent.state.messages.slice(),
2097
+ systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
2098
+ tools: this.agent.state.tools.slice(),
2099
+ };
2100
+ }
2101
+ async _compactBeforeNextProviderRequest(turn, signal) {
2102
+ if (signal?.aborted || !this._shouldCompactBeforeNextProviderRequest(turn)) {
2103
+ return undefined;
2104
+ }
2105
+ const settings = this.settingsManager.getCompactionSettings();
2106
+ const markerParentId = this.sessionManager.getLeafId();
2107
+ const markerId = this._appendIntraTurnCompactionMarker();
2108
+ const preparationOptions = { firstKeptEntryId: markerId };
2109
+ const branchWithMarker = this.sessionManager.getBranch();
2110
+ const previousCompactionId = getLatestCompactionEntry(branchWithMarker)?.id;
2111
+ const preparation = prepareCompaction(branchWithMarker, settings, preparationOptions);
2112
+ if (!preparation) {
2113
+ this._restoreSessionLeaf(markerParentId);
2114
+ return undefined;
2115
+ }
2116
+ await this._runAutoCompaction("threshold", false, preparation, preparationOptions);
2117
+ const currentCompactionId = getLatestCompactionEntry(this.sessionManager.getBranch())?.id;
2118
+ if (currentCompactionId === previousCompactionId) {
2119
+ this._restoreSessionLeaf(markerParentId);
2120
+ return undefined;
2121
+ }
2122
+ return this._currentAgentContext(turn.context);
2123
+ }
1815
2124
  async bindExtensions(bindings) {
1816
2125
  if (bindings.uiContext !== undefined) {
1817
2126
  this._extensionUIContext = bindings.uiContext;
@@ -2150,6 +2459,11 @@ export class AgentSession {
2150
2459
  // Context overflow is handled by compaction, not retry.
2151
2460
  if (isContextOverflow(message, this.model?.contextWindow ?? 0))
2152
2461
  return false;
2462
+ if (this._isPostAssistantPiServerSyncFailure(message))
2463
+ return false;
2464
+ const piServerPhase = getPiServerFailurePhase(message);
2465
+ if (piServerPhase && piServerPhase !== "provider_stream")
2466
+ return false;
2153
2467
  if (message.errorMessage && this._isNonRetryableProviderLimitError(message.errorMessage))
2154
2468
  return false;
2155
2469
  return isRetryableAssistantError(message);
@@ -2187,6 +2501,18 @@ export class AgentSession {
2187
2501
  this.agent.state.messages = messages.slice(0, -1);
2188
2502
  }
2189
2503
  }
2504
+ const lastMessage = this.agent.state.messages[this.agent.state.messages.length - 1];
2505
+ if (!lastMessage || lastMessage.role === "assistant") {
2506
+ const attempt = this._retryAttempt;
2507
+ this._retryAttempt = 0;
2508
+ this._emit({
2509
+ type: "auto_retry_end",
2510
+ success: false,
2511
+ attempt,
2512
+ finalError: lastMessage ? "Cannot continue from message role: assistant" : "No messages to continue from",
2513
+ });
2514
+ return false;
2515
+ }
2190
2516
  // Wait with exponential backoff (abortable)
2191
2517
  this._retryAbortController = new AbortController();
2192
2518
  try {