@mariozechner/pi-coding-agent 0.27.4 → 0.27.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/core/agent-session.d.ts +1 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +137 -40
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/compaction.d.ts +10 -0
  7. package/dist/core/compaction.d.ts.map +1 -1
  8. package/dist/core/compaction.js +35 -0
  9. package/dist/core/compaction.js.map +1 -1
  10. package/dist/core/export-html.d.ts +11 -2
  11. package/dist/core/export-html.d.ts.map +1 -1
  12. package/dist/core/export-html.js +551 -94
  13. package/dist/core/export-html.js.map +1 -1
  14. package/dist/core/hooks/runner.d.ts.map +1 -1
  15. package/dist/core/hooks/runner.js +11 -3
  16. package/dist/core/hooks/runner.js.map +1 -1
  17. package/dist/core/hooks/types.d.ts +28 -2
  18. package/dist/core/hooks/types.d.ts.map +1 -1
  19. package/dist/core/hooks/types.js.map +1 -1
  20. package/dist/core/model-config.d.ts +7 -2
  21. package/dist/core/model-config.d.ts.map +1 -1
  22. package/dist/core/model-config.js +7 -2
  23. package/dist/core/model-config.js.map +1 -1
  24. package/dist/core/sdk.d.ts.map +1 -1
  25. package/dist/core/sdk.js +1 -1
  26. package/dist/core/sdk.js.map +1 -1
  27. package/dist/core/session-manager.d.ts +24 -11
  28. package/dist/core/session-manager.d.ts.map +1 -1
  29. package/dist/core/session-manager.js +25 -21
  30. package/dist/core/session-manager.js.map +1 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  36. package/dist/modes/interactive/interactive-mode.js +5 -5
  37. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  38. package/dist/modes/print-mode.d.ts.map +1 -1
  39. package/dist/modes/print-mode.js +1 -1
  40. package/dist/modes/print-mode.js.map +1 -1
  41. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  42. package/dist/modes/rpc/rpc-mode.js +1 -1
  43. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  44. package/docs/hooks.md +126 -12
  45. package/examples/hooks/README.md +3 -0
  46. package/examples/hooks/custom-compaction.ts +115 -0
  47. package/package.json +6 -5
@@ -15,10 +15,9 @@
15
15
  import { isContextOverflow, supportsXhigh } from "@mariozechner/pi-ai";
16
16
  import { getModelsPath } from "../config.js";
17
17
  import { executeBash as executeBashCommand } from "./bash-executor.js";
18
- import { calculateContextTokens, compact, shouldCompact } from "./compaction.js";
18
+ import { calculateContextTokens, compact, prepareCompaction, shouldCompact } from "./compaction.js";
19
19
  import { exportSessionToHtml } from "./export-html.js";
20
20
  import { getApiKeyForModel, getAvailableModels } from "./model-config.js";
21
- import { loadSessionFromEntries } from "./session-manager.js";
22
21
  import { expandSlashCommand } from "./slash-commands.js";
23
22
  // ============================================================================
24
23
  // Constants
@@ -370,7 +369,7 @@ export class AgentSession {
370
369
  */
371
370
  async reset() {
372
371
  const previousSessionFile = this.sessionFile;
373
- const entries = this.sessionManager.loadEntries();
372
+ const entries = this.sessionManager.getEntries();
374
373
  // Emit before_clear event (can be cancelled)
375
374
  if (this._hookRunner?.hasHandlers("session")) {
376
375
  const result = (await this._hookRunner.emit({
@@ -561,10 +560,8 @@ export class AgentSession {
561
560
  * @param customInstructions Optional instructions for the compaction summary
562
561
  */
563
562
  async compact(customInstructions) {
564
- // Abort any running operation
565
563
  this._disconnectFromAgent();
566
564
  await this.abort();
567
- // Create abort controller
568
565
  this._compactionAbortController = new AbortController();
569
566
  try {
570
567
  if (!this.model) {
@@ -574,16 +571,69 @@ export class AgentSession {
574
571
  if (!apiKey) {
575
572
  throw new Error(`No API key for ${this.model.provider}`);
576
573
  }
577
- const entries = this.sessionManager.loadEntries();
574
+ const entries = this.sessionManager.getEntries();
578
575
  const settings = this.settingsManager.getCompactionSettings();
579
- const compactionEntry = await compact(entries, this.model, settings, apiKey, this._compactionAbortController.signal, customInstructions);
576
+ const preparation = prepareCompaction(entries, settings);
577
+ if (!preparation) {
578
+ throw new Error("Already compacted");
579
+ }
580
+ // Find previous compaction summary if any
581
+ let previousSummary;
582
+ for (let i = entries.length - 1; i >= 0; i--) {
583
+ if (entries[i].type === "compaction") {
584
+ previousSummary = entries[i].summary;
585
+ break;
586
+ }
587
+ }
588
+ let compactionEntry;
589
+ let fromHook = false;
590
+ if (this._hookRunner?.hasHandlers("session")) {
591
+ const result = (await this._hookRunner.emit({
592
+ type: "session",
593
+ entries,
594
+ sessionFile: this.sessionFile,
595
+ previousSessionFile: null,
596
+ reason: "before_compact",
597
+ cutPoint: preparation.cutPoint,
598
+ previousSummary,
599
+ messagesToSummarize: [...preparation.messagesToSummarize],
600
+ messagesToKeep: [...preparation.messagesToKeep],
601
+ tokensBefore: preparation.tokensBefore,
602
+ customInstructions,
603
+ model: this.model,
604
+ resolveApiKey: this._resolveApiKey,
605
+ signal: this._compactionAbortController.signal,
606
+ }));
607
+ if (result?.cancel) {
608
+ throw new Error("Compaction cancelled");
609
+ }
610
+ if (result?.compactionEntry) {
611
+ compactionEntry = result.compactionEntry;
612
+ fromHook = true;
613
+ }
614
+ }
615
+ if (!compactionEntry) {
616
+ compactionEntry = await compact(entries, this.model, settings, apiKey, this._compactionAbortController.signal, customInstructions);
617
+ }
580
618
  if (this._compactionAbortController.signal.aborted) {
581
619
  throw new Error("Compaction cancelled");
582
620
  }
583
- // Save and reload
584
621
  this.sessionManager.saveCompaction(compactionEntry);
585
- const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
586
- this.agent.replaceMessages(loaded.messages);
622
+ const newEntries = this.sessionManager.getEntries();
623
+ const sessionContext = this.sessionManager.buildSessionContext();
624
+ this.agent.replaceMessages(sessionContext.messages);
625
+ if (this._hookRunner) {
626
+ await this._hookRunner.emit({
627
+ type: "session",
628
+ entries: newEntries,
629
+ sessionFile: this.sessionFile,
630
+ previousSessionFile: null,
631
+ reason: "compact",
632
+ compactionEntry,
633
+ tokensBefore: compactionEntry.tokensBefore,
634
+ fromHook,
635
+ });
636
+ }
587
637
  return {
588
638
  tokensBefore: compactionEntry.tokensBefore,
589
639
  summary: compactionEntry.summary,
@@ -657,41 +707,89 @@ export class AgentSession {
657
707
  this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
658
708
  return;
659
709
  }
660
- const entries = this.sessionManager.loadEntries();
661
- const compactionEntry = await compact(entries, this.model, settings, apiKey, this._autoCompactionAbortController.signal);
710
+ const entries = this.sessionManager.getEntries();
711
+ const preparation = prepareCompaction(entries, settings);
712
+ if (!preparation) {
713
+ this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
714
+ return;
715
+ }
716
+ // Find previous compaction summary if any
717
+ let previousSummary;
718
+ for (let i = entries.length - 1; i >= 0; i--) {
719
+ if (entries[i].type === "compaction") {
720
+ previousSummary = entries[i].summary;
721
+ break;
722
+ }
723
+ }
724
+ let compactionEntry;
725
+ let fromHook = false;
726
+ if (this._hookRunner?.hasHandlers("session")) {
727
+ const hookResult = (await this._hookRunner.emit({
728
+ type: "session",
729
+ entries,
730
+ sessionFile: this.sessionFile,
731
+ previousSessionFile: null,
732
+ reason: "before_compact",
733
+ cutPoint: preparation.cutPoint,
734
+ previousSummary,
735
+ messagesToSummarize: [...preparation.messagesToSummarize],
736
+ messagesToKeep: [...preparation.messagesToKeep],
737
+ tokensBefore: preparation.tokensBefore,
738
+ customInstructions: undefined,
739
+ model: this.model,
740
+ resolveApiKey: this._resolveApiKey,
741
+ signal: this._autoCompactionAbortController.signal,
742
+ }));
743
+ if (hookResult?.cancel) {
744
+ this._emit({ type: "auto_compaction_end", result: null, aborted: true, willRetry: false });
745
+ return;
746
+ }
747
+ if (hookResult?.compactionEntry) {
748
+ compactionEntry = hookResult.compactionEntry;
749
+ fromHook = true;
750
+ }
751
+ }
752
+ if (!compactionEntry) {
753
+ compactionEntry = await compact(entries, this.model, settings, apiKey, this._autoCompactionAbortController.signal);
754
+ }
662
755
  if (this._autoCompactionAbortController.signal.aborted) {
663
756
  this._emit({ type: "auto_compaction_end", result: null, aborted: true, willRetry: false });
664
757
  return;
665
758
  }
666
759
  this.sessionManager.saveCompaction(compactionEntry);
667
- const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
668
- this.agent.replaceMessages(loaded.messages);
760
+ const newEntries = this.sessionManager.getEntries();
761
+ const sessionContext = this.sessionManager.buildSessionContext();
762
+ this.agent.replaceMessages(sessionContext.messages);
763
+ if (this._hookRunner) {
764
+ await this._hookRunner.emit({
765
+ type: "session",
766
+ entries: newEntries,
767
+ sessionFile: this.sessionFile,
768
+ previousSessionFile: null,
769
+ reason: "compact",
770
+ compactionEntry,
771
+ tokensBefore: compactionEntry.tokensBefore,
772
+ fromHook,
773
+ });
774
+ }
669
775
  const result = {
670
776
  tokensBefore: compactionEntry.tokensBefore,
671
777
  summary: compactionEntry.summary,
672
778
  };
673
779
  this._emit({ type: "auto_compaction_end", result, aborted: false, willRetry });
674
- // Auto-retry if needed - use continue() since user message is already in context
675
780
  if (willRetry) {
676
- // Remove trailing error message from agent state (it's kept in session file for history)
677
- // This is needed because continue() requires last message to be user or toolResult
678
781
  const messages = this.agent.state.messages;
679
782
  const lastMsg = messages[messages.length - 1];
680
783
  if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") {
681
784
  this.agent.replaceMessages(messages.slice(0, -1));
682
785
  }
683
- // Use setTimeout to break out of the event handler chain
684
786
  setTimeout(() => {
685
- this.agent.continue().catch(() => {
686
- // Retry failed - silently ignore, user can manually retry
687
- });
787
+ this.agent.continue().catch(() => { });
688
788
  }, 100);
689
789
  }
690
790
  }
691
791
  catch (error) {
692
- // Compaction failed - emit end event without retry
693
792
  this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
694
- // If this was overflow recovery and compaction failed, we have a hard stop
695
793
  if (reason === "overflow") {
696
794
  throw new Error(`Context overflow: ${error instanceof Error ? error.message : "compaction failed"}. Your input may be too large for the context window.`);
697
795
  }
@@ -927,7 +1025,7 @@ export class AgentSession {
927
1025
  */
928
1026
  async switchSession(sessionPath) {
929
1027
  const previousSessionFile = this.sessionFile;
930
- const oldEntries = this.sessionManager.loadEntries();
1028
+ const oldEntries = this.sessionManager.getEntries();
931
1029
  // Emit before_switch event (can be cancelled)
932
1030
  if (this._hookRunner?.hasHandlers("session")) {
933
1031
  const result = (await this._hookRunner.emit({
@@ -947,8 +1045,8 @@ export class AgentSession {
947
1045
  // Set new session
948
1046
  this.sessionManager.setSessionFile(sessionPath);
949
1047
  // Reload messages
950
- const entries = this.sessionManager.loadEntries();
951
- const loaded = loadSessionFromEntries(entries);
1048
+ const entries = this.sessionManager.getEntries();
1049
+ const sessionContext = this.sessionManager.buildSessionContext();
952
1050
  // Emit session event to hooks
953
1051
  if (this._hookRunner) {
954
1052
  this._hookRunner.setSessionFile(sessionPath);
@@ -962,20 +1060,18 @@ export class AgentSession {
962
1060
  }
963
1061
  // Emit session event to custom tools
964
1062
  await this._emitToolSessionEvent("switch", previousSessionFile);
965
- this.agent.replaceMessages(loaded.messages);
1063
+ this.agent.replaceMessages(sessionContext.messages);
966
1064
  // Restore model if saved
967
- const savedModel = this.sessionManager.loadModel();
968
- if (savedModel) {
1065
+ if (sessionContext.model) {
969
1066
  const availableModels = (await getAvailableModels()).models;
970
- const match = availableModels.find((m) => m.provider === savedModel.provider && m.id === savedModel.modelId);
1067
+ const match = availableModels.find((m) => m.provider === sessionContext.model.provider && m.id === sessionContext.model.modelId);
971
1068
  if (match) {
972
1069
  this.agent.setModel(match);
973
1070
  }
974
1071
  }
975
1072
  // Restore thinking level if saved (setThinkingLevel clamps to model capabilities)
976
- const savedThinking = this.sessionManager.loadThinkingLevel();
977
- if (savedThinking) {
978
- this.setThinkingLevel(savedThinking);
1073
+ if (sessionContext.thinkingLevel) {
1074
+ this.setThinkingLevel(sessionContext.thinkingLevel);
979
1075
  }
980
1076
  this._reconnectToAgent();
981
1077
  return true;
@@ -991,7 +1087,7 @@ export class AgentSession {
991
1087
  */
992
1088
  async branch(entryIndex) {
993
1089
  const previousSessionFile = this.sessionFile;
994
- const entries = this.sessionManager.loadEntries();
1090
+ const entries = this.sessionManager.getEntries();
995
1091
  const selectedEntry = entries[entryIndex];
996
1092
  if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
997
1093
  throw new Error("Invalid entry index for branching");
@@ -1020,8 +1116,8 @@ export class AgentSession {
1020
1116
  this.sessionManager.setSessionFile(newSessionFile);
1021
1117
  }
1022
1118
  // Reload messages from entries (works for both file and in-memory mode)
1023
- const newEntries = this.sessionManager.loadEntries();
1024
- const loaded = loadSessionFromEntries(newEntries);
1119
+ const newEntries = this.sessionManager.getEntries();
1120
+ const sessionContext = this.sessionManager.buildSessionContext();
1025
1121
  // Emit branch event to hooks (after branch completes)
1026
1122
  if (this._hookRunner) {
1027
1123
  this._hookRunner.setSessionFile(newSessionFile);
@@ -1037,7 +1133,7 @@ export class AgentSession {
1037
1133
  // Emit session event to custom tools (with reason "branch")
1038
1134
  await this._emitToolSessionEvent("branch", previousSessionFile);
1039
1135
  if (!skipConversationRestore) {
1040
- this.agent.replaceMessages(loaded.messages);
1136
+ this.agent.replaceMessages(sessionContext.messages);
1041
1137
  }
1042
1138
  return { selectedText, cancelled: false };
1043
1139
  }
@@ -1045,7 +1141,7 @@ export class AgentSession {
1045
1141
  * Get all user messages from session for branch selector.
1046
1142
  */
1047
1143
  getUserMessagesForBranching() {
1048
- const entries = this.sessionManager.loadEntries();
1144
+ const entries = this.sessionManager.getEntries();
1049
1145
  const result = [];
1050
1146
  for (let i = 0; i < entries.length; i++) {
1051
1147
  const entry = entries[i];
@@ -1120,7 +1216,8 @@ export class AgentSession {
1120
1216
  * @returns Path to exported file
1121
1217
  */
1122
1218
  exportToHtml(outputPath) {
1123
- return exportSessionToHtml(this.sessionManager, this.state, outputPath);
1219
+ const themeName = this.settingsManager.getTheme();
1220
+ return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
1124
1221
  }
1125
1222
  // =========================================================================
1126
1223
  // Utilities
@@ -1172,7 +1269,7 @@ export class AgentSession {
1172
1269
  */
1173
1270
  async _emitToolSessionEvent(reason, previousSessionFile) {
1174
1271
  const event = {
1175
- entries: this.sessionManager.loadEntries(),
1272
+ entries: this.sessionManager.getEntries(),
1176
1273
  sessionFile: this.sessionFile,
1177
1274
  previousSessionFile,
1178
1275
  reason,