@oh-my-pi/pi-coding-agent 6.9.0 → 7.0.0

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 (143) hide show
  1. package/CHANGELOG.md +173 -51
  2. package/examples/sdk/04-skills.ts +1 -1
  3. package/package.json +6 -5
  4. package/src/cli/stats-cli.ts +191 -0
  5. package/src/core/agent-session.ts +214 -4
  6. package/src/core/auth-storage.ts +524 -202
  7. package/src/core/bash-executor.ts +1 -1
  8. package/src/core/extensions/index.ts +2 -0
  9. package/src/core/extensions/runner.ts +31 -0
  10. package/src/core/extensions/types.ts +24 -0
  11. package/src/core/messages.ts +48 -0
  12. package/src/core/model-registry.ts +7 -0
  13. package/src/core/python-executor.ts +29 -8
  14. package/src/core/python-gateway-coordinator.ts +55 -1
  15. package/src/core/python-prelude.py +201 -8
  16. package/src/core/session-manager.ts +10 -1
  17. package/src/core/tools/bash.ts +5 -7
  18. package/src/core/tools/find.ts +18 -5
  19. package/src/core/tools/index.ts +1 -1
  20. package/src/core/tools/lsp/index.ts +13 -2
  21. package/src/core/tools/patch/applicator.ts +115 -17
  22. package/src/core/tools/patch/index.ts +1 -1
  23. package/src/core/tools/patch/normalize.ts +185 -10
  24. package/src/core/tools/python.ts +445 -86
  25. package/src/core/tools/read.ts +4 -4
  26. package/src/core/tools/task/executor.ts +2 -6
  27. package/src/core/tools/task/index.ts +30 -12
  28. package/src/core/tools/task/render.ts +163 -30
  29. package/src/core/tools/task/template.ts +37 -0
  30. package/src/core/tools/task/types.ts +6 -2
  31. package/src/core/tools/task/worker.ts +1 -1
  32. package/src/index.ts +2 -0
  33. package/src/main.ts +12 -0
  34. package/src/modes/interactive/components/python-execution.ts +180 -0
  35. package/src/modes/interactive/components/welcome.ts +1 -0
  36. package/src/modes/interactive/controllers/command-controller.ts +395 -0
  37. package/src/modes/interactive/controllers/input-controller.ts +83 -8
  38. package/src/modes/interactive/interactive-mode.ts +16 -1
  39. package/src/modes/interactive/theme/dark.json +2 -9
  40. package/src/modes/interactive/theme/defaults/alabaster.json +2 -8
  41. package/src/modes/interactive/theme/defaults/amethyst.json +2 -9
  42. package/src/modes/interactive/theme/defaults/anthracite.json +2 -9
  43. package/src/modes/interactive/theme/defaults/basalt.json +89 -88
  44. package/src/modes/interactive/theme/defaults/birch.json +2 -8
  45. package/src/modes/interactive/theme/defaults/dark-abyss.json +2 -8
  46. package/src/modes/interactive/theme/defaults/dark-arctic.json +2 -9
  47. package/src/modes/interactive/theme/defaults/dark-aurora.json +3 -2
  48. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +2 -1
  49. package/src/modes/interactive/theme/defaults/dark-cavern.json +2 -8
  50. package/src/modes/interactive/theme/defaults/dark-copper.json +3 -2
  51. package/src/modes/interactive/theme/defaults/dark-cosmos.json +2 -8
  52. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +2 -9
  53. package/src/modes/interactive/theme/defaults/dark-dracula.json +2 -9
  54. package/src/modes/interactive/theme/defaults/dark-eclipse.json +2 -8
  55. package/src/modes/interactive/theme/defaults/dark-ember.json +3 -2
  56. package/src/modes/interactive/theme/defaults/dark-equinox.json +2 -8
  57. package/src/modes/interactive/theme/defaults/dark-forest.json +2 -9
  58. package/src/modes/interactive/theme/defaults/dark-github.json +2 -9
  59. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +2 -9
  60. package/src/modes/interactive/theme/defaults/dark-lavender.json +3 -2
  61. package/src/modes/interactive/theme/defaults/dark-lunar.json +2 -8
  62. package/src/modes/interactive/theme/defaults/dark-midnight.json +3 -2
  63. package/src/modes/interactive/theme/defaults/dark-monochrome.json +2 -9
  64. package/src/modes/interactive/theme/defaults/dark-monokai.json +2 -9
  65. package/src/modes/interactive/theme/defaults/dark-nebula.json +2 -8
  66. package/src/modes/interactive/theme/defaults/dark-nord.json +2 -9
  67. package/src/modes/interactive/theme/defaults/dark-ocean.json +2 -9
  68. package/src/modes/interactive/theme/defaults/dark-one.json +2 -9
  69. package/src/modes/interactive/theme/defaults/dark-rainforest.json +2 -8
  70. package/src/modes/interactive/theme/defaults/dark-reef.json +2 -8
  71. package/src/modes/interactive/theme/defaults/dark-retro.json +2 -9
  72. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +2 -1
  73. package/src/modes/interactive/theme/defaults/dark-sakura.json +3 -2
  74. package/src/modes/interactive/theme/defaults/dark-slate.json +3 -2
  75. package/src/modes/interactive/theme/defaults/dark-solarized.json +2 -1
  76. package/src/modes/interactive/theme/defaults/dark-solstice.json +2 -8
  77. package/src/modes/interactive/theme/defaults/dark-starfall.json +2 -8
  78. package/src/modes/interactive/theme/defaults/dark-sunset.json +2 -9
  79. package/src/modes/interactive/theme/defaults/dark-swamp.json +2 -8
  80. package/src/modes/interactive/theme/defaults/dark-synthwave.json +2 -1
  81. package/src/modes/interactive/theme/defaults/dark-taiga.json +2 -8
  82. package/src/modes/interactive/theme/defaults/dark-terminal.json +3 -2
  83. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +2 -9
  84. package/src/modes/interactive/theme/defaults/dark-tundra.json +2 -8
  85. package/src/modes/interactive/theme/defaults/dark-twilight.json +2 -8
  86. package/src/modes/interactive/theme/defaults/dark-volcanic.json +2 -8
  87. package/src/modes/interactive/theme/defaults/graphite.json +2 -9
  88. package/src/modes/interactive/theme/defaults/light-arctic.json +2 -1
  89. package/src/modes/interactive/theme/defaults/light-aurora-day.json +2 -8
  90. package/src/modes/interactive/theme/defaults/light-canyon.json +2 -8
  91. package/src/modes/interactive/theme/defaults/light-catppuccin.json +2 -1
  92. package/src/modes/interactive/theme/defaults/light-cirrus.json +2 -8
  93. package/src/modes/interactive/theme/defaults/light-coral.json +3 -2
  94. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +2 -9
  95. package/src/modes/interactive/theme/defaults/light-dawn.json +2 -8
  96. package/src/modes/interactive/theme/defaults/light-dunes.json +2 -8
  97. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +3 -2
  98. package/src/modes/interactive/theme/defaults/light-forest.json +2 -9
  99. package/src/modes/interactive/theme/defaults/light-frost.json +3 -2
  100. package/src/modes/interactive/theme/defaults/light-github.json +2 -1
  101. package/src/modes/interactive/theme/defaults/light-glacier.json +2 -8
  102. package/src/modes/interactive/theme/defaults/light-gruvbox.json +2 -9
  103. package/src/modes/interactive/theme/defaults/light-haze.json +2 -8
  104. package/src/modes/interactive/theme/defaults/light-honeycomb.json +3 -2
  105. package/src/modes/interactive/theme/defaults/light-lagoon.json +2 -8
  106. package/src/modes/interactive/theme/defaults/light-lavender.json +3 -2
  107. package/src/modes/interactive/theme/defaults/light-meadow.json +2 -8
  108. package/src/modes/interactive/theme/defaults/light-mint.json +3 -2
  109. package/src/modes/interactive/theme/defaults/light-monochrome.json +2 -1
  110. package/src/modes/interactive/theme/defaults/light-ocean.json +2 -9
  111. package/src/modes/interactive/theme/defaults/light-one.json +2 -8
  112. package/src/modes/interactive/theme/defaults/light-opal.json +2 -8
  113. package/src/modes/interactive/theme/defaults/light-orchard.json +2 -8
  114. package/src/modes/interactive/theme/defaults/light-paper.json +3 -2
  115. package/src/modes/interactive/theme/defaults/light-prism.json +2 -8
  116. package/src/modes/interactive/theme/defaults/light-retro.json +2 -9
  117. package/src/modes/interactive/theme/defaults/light-sand.json +3 -2
  118. package/src/modes/interactive/theme/defaults/light-savanna.json +2 -8
  119. package/src/modes/interactive/theme/defaults/light-solarized.json +2 -1
  120. package/src/modes/interactive/theme/defaults/light-soleil.json +2 -8
  121. package/src/modes/interactive/theme/defaults/light-sunset.json +2 -9
  122. package/src/modes/interactive/theme/defaults/light-synthwave.json +2 -9
  123. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +2 -9
  124. package/src/modes/interactive/theme/defaults/light-wetland.json +2 -8
  125. package/src/modes/interactive/theme/defaults/light-zenith.json +2 -8
  126. package/src/modes/interactive/theme/defaults/limestone.json +2 -8
  127. package/src/modes/interactive/theme/defaults/mahogany.json +2 -9
  128. package/src/modes/interactive/theme/defaults/marble.json +2 -8
  129. package/src/modes/interactive/theme/defaults/obsidian.json +89 -88
  130. package/src/modes/interactive/theme/defaults/onyx.json +89 -88
  131. package/src/modes/interactive/theme/defaults/pearl.json +2 -8
  132. package/src/modes/interactive/theme/defaults/porcelain.json +89 -88
  133. package/src/modes/interactive/theme/defaults/quartz.json +2 -8
  134. package/src/modes/interactive/theme/defaults/sandstone.json +2 -8
  135. package/src/modes/interactive/theme/defaults/titanium.json +88 -87
  136. package/src/modes/interactive/theme/light.json +2 -8
  137. package/src/modes/interactive/theme/theme-schema.json +5 -0
  138. package/src/modes/interactive/theme/theme.ts +7 -0
  139. package/src/modes/interactive/types.ts +7 -1
  140. package/src/modes/interactive/utils/ui-helpers.ts +20 -0
  141. package/src/prompts/system/system-prompt.md +88 -78
  142. package/src/prompts/tools/python.md +39 -2
  143. package/src/prompts/tools/task.md +8 -13
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Stats CLI command handlers.
3
+ *
4
+ * Handles `omp stats` subcommand for viewing AI usage statistics.
5
+ */
6
+
7
+ import chalk from "chalk";
8
+ import { APP_NAME } from "../config";
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ export interface StatsCommandArgs {
15
+ port: number;
16
+ json: boolean;
17
+ summary: boolean;
18
+ }
19
+
20
+ // =============================================================================
21
+ // Argument Parser
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Parse stats subcommand arguments.
26
+ * Returns undefined if not a stats command.
27
+ */
28
+ export function parseStatsArgs(args: string[]): StatsCommandArgs | undefined {
29
+ if (args.length === 0 || args[0] !== "stats") {
30
+ return undefined;
31
+ }
32
+
33
+ const result: StatsCommandArgs = {
34
+ port: 3847,
35
+ json: false,
36
+ summary: false,
37
+ };
38
+
39
+ for (let i = 1; i < args.length; i++) {
40
+ const arg = args[i];
41
+ if (arg === "--json" || arg === "-j") {
42
+ result.json = true;
43
+ } else if (arg === "--summary" || arg === "-s") {
44
+ result.summary = true;
45
+ } else if ((arg === "--port" || arg === "-p") && i + 1 < args.length) {
46
+ result.port = parseInt(args[++i], 10);
47
+ } else if (arg.startsWith("--port=")) {
48
+ result.port = parseInt(arg.split("=")[1], 10);
49
+ }
50
+ }
51
+
52
+ return result;
53
+ }
54
+
55
+ // =============================================================================
56
+ // Formatting Helpers
57
+ // =============================================================================
58
+
59
+ function formatNumber(n: number): string {
60
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
61
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
62
+ return n.toFixed(0);
63
+ }
64
+
65
+ function formatCost(n: number): string {
66
+ if (n < 0.01) return `$${n.toFixed(4)}`;
67
+ if (n < 1) return `$${n.toFixed(3)}`;
68
+ return `$${n.toFixed(2)}`;
69
+ }
70
+
71
+ function formatDuration(ms: number | null): string {
72
+ if (ms === null) return "-";
73
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
74
+ return `${(ms / 1000).toFixed(1)}s`;
75
+ }
76
+
77
+ function formatPercent(n: number): string {
78
+ return `${(n * 100).toFixed(1)}%`;
79
+ }
80
+
81
+ // =============================================================================
82
+ // Command Handler
83
+ // =============================================================================
84
+
85
+ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
86
+ // Lazy import to avoid loading stats module when not needed
87
+ const { getDashboardStats, syncAllSessions, getTotalMessageCount } = await import("@oh-my-pi/omp-stats");
88
+ const { startServer } = await import("@oh-my-pi/omp-stats/src/server");
89
+ const { closeDb } = await import("@oh-my-pi/omp-stats/src/db");
90
+
91
+ // Sync session files first
92
+ console.log("Syncing session files...");
93
+ const { processed, files } = await syncAllSessions();
94
+ const total = await getTotalMessageCount();
95
+ console.log(`Synced ${processed} new entries from ${files} files (${total} total)\n`);
96
+
97
+ if (cmd.json) {
98
+ const stats = await getDashboardStats();
99
+ console.log(JSON.stringify(stats, null, 2));
100
+ return;
101
+ }
102
+
103
+ if (cmd.summary) {
104
+ await printStatsSummary();
105
+ return;
106
+ }
107
+
108
+ // Start the dashboard server
109
+ const { port } = startServer(cmd.port);
110
+ console.log(chalk.green(`Dashboard available at: http://localhost:${port}`));
111
+ console.log("Press Ctrl+C to stop\n");
112
+
113
+ // Keep process running
114
+ process.on("SIGINT", () => {
115
+ console.log("\nShutting down...");
116
+ closeDb();
117
+ process.exit(0);
118
+ });
119
+
120
+ // Keep the process alive
121
+ await new Promise(() => {});
122
+ }
123
+
124
+ async function printStatsSummary(): Promise<void> {
125
+ const { getDashboardStats } = await import("@oh-my-pi/omp-stats");
126
+ const stats = await getDashboardStats();
127
+ const { overall, byModel, byFolder } = stats;
128
+
129
+ console.log(chalk.bold("\n=== AI Usage Statistics ===\n"));
130
+
131
+ console.log(chalk.bold("Overall:"));
132
+ console.log(` Requests: ${formatNumber(overall.totalRequests)} (${formatNumber(overall.failedRequests)} errors)`);
133
+ console.log(` Error Rate: ${formatPercent(overall.errorRate)}`);
134
+ console.log(` Total Tokens: ${formatNumber(overall.totalInputTokens + overall.totalOutputTokens)}`);
135
+ console.log(` Cache Rate: ${formatPercent(overall.cacheRate)}`);
136
+ console.log(` Total Cost: ${formatCost(overall.totalCost)}`);
137
+ console.log(` Avg Duration: ${formatDuration(overall.avgDuration)}`);
138
+ console.log(` Avg TTFT: ${formatDuration(overall.avgTtft)}`);
139
+ if (overall.avgTokensPerSecond !== null) {
140
+ console.log(` Avg Tokens/s: ${overall.avgTokensPerSecond.toFixed(1)}`);
141
+ }
142
+
143
+ if (byModel.length > 0) {
144
+ console.log(chalk.bold("\nBy Model:"));
145
+ for (const m of byModel.slice(0, 10)) {
146
+ console.log(
147
+ ` ${m.model}: ${formatNumber(m.totalRequests)} reqs, ${formatCost(m.totalCost)}, ${formatPercent(m.cacheRate)} cache`,
148
+ );
149
+ }
150
+ }
151
+
152
+ if (byFolder.length > 0) {
153
+ console.log(chalk.bold("\nBy Folder:"));
154
+ for (const f of byFolder.slice(0, 10)) {
155
+ console.log(` ${f.folder}: ${formatNumber(f.totalRequests)} reqs, ${formatCost(f.totalCost)}`);
156
+ }
157
+ }
158
+
159
+ console.log("");
160
+ }
161
+
162
+ // =============================================================================
163
+ // Help
164
+ // =============================================================================
165
+
166
+ export function printStatsHelp(): void {
167
+ console.log(`${chalk.bold(`${APP_NAME} stats`)} - AI Usage Statistics Dashboard
168
+
169
+ ${chalk.bold("Usage:")}
170
+ ${APP_NAME} stats [options]
171
+
172
+ ${chalk.bold("Options:")}
173
+ -p, --port <port> Port for the dashboard server (default: 3847)
174
+ -j, --json Output stats as JSON and exit
175
+ -s, --summary Print summary to console and exit
176
+ -h, --help Show this help message
177
+
178
+ ${chalk.bold("Examples:")}
179
+ ${APP_NAME} stats # Start dashboard server
180
+ ${APP_NAME} stats --json # Print stats as JSON
181
+ ${APP_NAME} stats --summary # Print summary to console
182
+ ${APP_NAME} stats --port 8080 # Start on custom port
183
+
184
+ ${chalk.bold("Metrics:")}
185
+ - Total requests and error rate
186
+ - Token usage (input, output, cache)
187
+ - Cost breakdown
188
+ - Average duration and time to first token (TTFT)
189
+ - Tokens per second throughput
190
+ `);
191
+ }
@@ -15,9 +15,19 @@
15
15
 
16
16
  import { existsSync, readFileSync } from "node:fs";
17
17
  import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
18
- import type { AssistantMessage, ImageContent, Message, Model, TextContent, ToolCall, Usage } from "@oh-my-pi/pi-ai";
18
+ import type {
19
+ AssistantMessage,
20
+ ImageContent,
21
+ Message,
22
+ Model,
23
+ TextContent,
24
+ ToolCall,
25
+ Usage,
26
+ UsageReport,
27
+ } from "@oh-my-pi/pi-ai";
19
28
  import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
20
29
  import { abortableSleep, logger } from "@oh-my-pi/pi-utils";
30
+ import { YAML } from "bun";
21
31
  import type { Rule } from "../capability/rule";
22
32
  import { getAgentDbPath } from "../config";
23
33
  import { theme } from "../modes/interactive/theme/theme";
@@ -50,10 +60,21 @@ import type {
50
60
  import type { CompactOptions, ContextUsage } from "./extensions/types";
51
61
  import { extractFileMentions, generateFileMentionMessages } from "./file-mentions";
52
62
  import type { HookCommandContext } from "./hooks/types";
53
- import type { BashExecutionMessage, CustomMessage } from "./messages";
63
+ import {
64
+ type BashExecutionMessage,
65
+ type BranchSummaryMessage,
66
+ bashExecutionToText,
67
+ type CompactionSummaryMessage,
68
+ type CustomMessage,
69
+ type FileMentionMessage,
70
+ type HookMessage,
71
+ type PythonExecutionMessage,
72
+ pythonExecutionToText,
73
+ } from "./messages";
54
74
  import type { ModelRegistry } from "./model-registry";
55
75
  import { parseModelString } from "./model-resolver";
56
76
  import { expandPromptTemplate, type PromptTemplate, parseCommandArgs, renderPromptTemplate } from "./prompt-templates";
77
+ import { executePython as executePythonCommand, type PythonResult } from "./python-executor";
57
78
  import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
58
79
  import type { SettingsManager, SkillsSettings } from "./settings-manager";
59
80
  import type { Skill, SkillWarning } from "./skills";
@@ -248,6 +269,10 @@ export class AgentSession {
248
269
  private _bashAbortController: AbortController | undefined = undefined;
249
270
  private _pendingBashMessages: BashExecutionMessage[] = [];
250
271
 
272
+ // Python execution state
273
+ private _pythonAbortController: AbortController | undefined = undefined;
274
+ private _pendingPythonMessages: PythonExecutionMessage[] = [];
275
+
251
276
  // Extension system
252
277
  private _extensionRunner: ExtensionRunner | undefined = undefined;
253
278
  private _turnIndex = 0;
@@ -1005,6 +1030,7 @@ export class AgentSession {
1005
1030
 
1006
1031
  // Flush any pending bash messages before the new prompt
1007
1032
  this._flushPendingBashMessages();
1033
+ this._flushPendingPythonMessages();
1008
1034
 
1009
1035
  // Reset todo reminder count on new user prompt
1010
1036
  this._todoReminderCount = 0;
@@ -2631,6 +2657,102 @@ export class AgentSession {
2631
2657
  this._pendingBashMessages = [];
2632
2658
  }
2633
2659
 
2660
+ // =========================================================================
2661
+ // User-Initiated Python Execution
2662
+ // =========================================================================
2663
+
2664
+ /**
2665
+ * Execute Python code in the shared kernel.
2666
+ * Uses the same kernel session as the agent's Python tool, allowing collaborative editing.
2667
+ * @param code The Python code to execute
2668
+ * @param onChunk Optional streaming callback for output
2669
+ * @param options.excludeFromContext If true, execution won't be sent to LLM ($$ prefix)
2670
+ */
2671
+ async executePython(
2672
+ code: string,
2673
+ onChunk?: (chunk: string) => void,
2674
+ options?: { excludeFromContext?: boolean },
2675
+ ): Promise<PythonResult> {
2676
+ this._pythonAbortController = new AbortController();
2677
+
2678
+ try {
2679
+ // Use the same session ID as the Python tool for kernel sharing
2680
+ const sessionFile = this.sessionManager.getSessionFile();
2681
+ const cwd = this.sessionManager.getCwd();
2682
+ const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
2683
+
2684
+ const result = await executePythonCommand(code, {
2685
+ cwd,
2686
+ sessionId,
2687
+ kernelMode: this.settingsManager?.getPythonKernelMode?.() ?? "session",
2688
+ useSharedGateway: this.settingsManager?.getPythonSharedGateway?.() ?? true,
2689
+ onChunk,
2690
+ signal: this._pythonAbortController.signal,
2691
+ });
2692
+
2693
+ this.recordPythonResult(code, result, options);
2694
+ return result;
2695
+ } finally {
2696
+ this._pythonAbortController = undefined;
2697
+ }
2698
+ }
2699
+
2700
+ /**
2701
+ * Record a Python execution result in session history.
2702
+ */
2703
+ recordPythonResult(code: string, result: PythonResult, options?: { excludeFromContext?: boolean }): void {
2704
+ const pythonMessage: PythonExecutionMessage = {
2705
+ role: "pythonExecution",
2706
+ code,
2707
+ output: result.output,
2708
+ exitCode: result.exitCode,
2709
+ cancelled: result.cancelled,
2710
+ truncated: result.truncated,
2711
+ fullOutputPath: result.fullOutputPath,
2712
+ timestamp: Date.now(),
2713
+ excludeFromContext: options?.excludeFromContext,
2714
+ };
2715
+
2716
+ // If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
2717
+ if (this.isStreaming) {
2718
+ this._pendingPythonMessages.push(pythonMessage);
2719
+ } else {
2720
+ this.agent.appendMessage(pythonMessage);
2721
+ this.sessionManager.appendMessage(pythonMessage);
2722
+ }
2723
+ }
2724
+
2725
+ /**
2726
+ * Cancel running Python execution.
2727
+ */
2728
+ abortPython(): void {
2729
+ this._pythonAbortController?.abort();
2730
+ }
2731
+
2732
+ /** Whether a Python execution is currently running */
2733
+ get isPythonRunning(): boolean {
2734
+ return this._pythonAbortController !== undefined;
2735
+ }
2736
+
2737
+ /** Whether there are pending Python messages waiting to be flushed */
2738
+ get hasPendingPythonMessages(): boolean {
2739
+ return this._pendingPythonMessages.length > 0;
2740
+ }
2741
+
2742
+ /**
2743
+ * Flush pending Python messages to agent state and session.
2744
+ */
2745
+ private _flushPendingPythonMessages(): void {
2746
+ if (this._pendingPythonMessages.length === 0) return;
2747
+
2748
+ for (const pythonMessage of this._pendingPythonMessages) {
2749
+ this.agent.appendMessage(pythonMessage);
2750
+ this.sessionManager.appendMessage(pythonMessage);
2751
+ }
2752
+
2753
+ this._pendingPythonMessages = [];
2754
+ }
2755
+
2634
2756
  // =========================================================================
2635
2757
  // Session Management
2636
2758
  // =========================================================================
@@ -3059,6 +3181,14 @@ export class AgentSession {
3059
3181
  };
3060
3182
  }
3061
3183
 
3184
+ async fetchUsageReports(): Promise<UsageReport[] | null> {
3185
+ const authStorage = this._modelRegistry.authStorage;
3186
+ if (!authStorage.fetchUsageReports) return null;
3187
+ return authStorage.fetchUsageReports({
3188
+ baseUrlResolver: (provider) => this._modelRegistry.getProviderBaseUrl?.(provider),
3189
+ });
3190
+ }
3191
+
3062
3192
  /**
3063
3193
  * Estimate context tokens from messages, using the last assistant usage when available.
3064
3194
  */
@@ -3163,6 +3293,36 @@ export class AgentSession {
3163
3293
  formatSessionAsText(): string {
3164
3294
  const lines: string[] = [];
3165
3295
 
3296
+ // Include system prompt at the beginning
3297
+ const systemPrompt = this.agent.state.systemPrompt;
3298
+ if (systemPrompt) {
3299
+ lines.push("## System Prompt\n");
3300
+ lines.push(systemPrompt);
3301
+ lines.push("\n");
3302
+ }
3303
+
3304
+ // Include model and thinking level
3305
+ const model = this.agent.state.model;
3306
+ const thinkingLevel = this.agent.state.thinkingLevel;
3307
+ lines.push("## Configuration\n");
3308
+ lines.push(`Model: ${model.provider}/${model.id}`);
3309
+ lines.push(`Thinking Level: ${thinkingLevel}`);
3310
+ lines.push("\n");
3311
+
3312
+ // Include available tools
3313
+ const tools = this.agent.state.tools;
3314
+ if (tools.length > 0) {
3315
+ lines.push("## Available Tools\n");
3316
+ for (const tool of tools) {
3317
+ lines.push(`### ${tool.name}\n`);
3318
+ lines.push(tool.description);
3319
+ lines.push("\n```yaml");
3320
+ lines.push(YAML.stringify(tool.parameters, null, 2));
3321
+ lines.push("```\n");
3322
+ }
3323
+ lines.push("\n");
3324
+ }
3325
+
3166
3326
  for (const msg of this.messages) {
3167
3327
  if (msg.role === "user") {
3168
3328
  lines.push("## User\n");
@@ -3191,8 +3351,8 @@ export class AgentSession {
3191
3351
  lines.push("</thinking>\n");
3192
3352
  } else if (c.type === "toolCall") {
3193
3353
  lines.push(`### Tool: ${c.name}`);
3194
- lines.push("```json");
3195
- lines.push(JSON.stringify(c.arguments, null, 2));
3354
+ lines.push("```yaml");
3355
+ lines.push(YAML.stringify(c.arguments, null, 2));
3196
3356
  lines.push("```\n");
3197
3357
  }
3198
3358
  }
@@ -3212,6 +3372,56 @@ export class AgentSession {
3212
3372
  }
3213
3373
  }
3214
3374
  lines.push("");
3375
+ } else if (msg.role === "bashExecution") {
3376
+ const bashMsg = msg as BashExecutionMessage;
3377
+ if (!bashMsg.excludeFromContext) {
3378
+ lines.push("## Bash Execution\n");
3379
+ lines.push(bashExecutionToText(bashMsg));
3380
+ lines.push("\n");
3381
+ }
3382
+ } else if (msg.role === "pythonExecution") {
3383
+ const pythonMsg = msg as PythonExecutionMessage;
3384
+ if (!pythonMsg.excludeFromContext) {
3385
+ lines.push("## Python Execution\n");
3386
+ lines.push(pythonExecutionToText(pythonMsg));
3387
+ lines.push("\n");
3388
+ }
3389
+ } else if (msg.role === "custom" || msg.role === "hookMessage") {
3390
+ const customMsg = msg as CustomMessage | HookMessage;
3391
+ lines.push(`## ${customMsg.customType}\n`);
3392
+ if (typeof customMsg.content === "string") {
3393
+ lines.push(customMsg.content);
3394
+ } else {
3395
+ for (const c of customMsg.content) {
3396
+ if (c.type === "text") {
3397
+ lines.push(c.text);
3398
+ } else if (c.type === "image") {
3399
+ lines.push("[Image]");
3400
+ }
3401
+ }
3402
+ }
3403
+ lines.push("\n");
3404
+ } else if (msg.role === "branchSummary") {
3405
+ const branchMsg = msg as BranchSummaryMessage;
3406
+ lines.push("## Branch Summary\n");
3407
+ lines.push(`(from branch: ${branchMsg.fromId})\n`);
3408
+ lines.push(branchMsg.summary);
3409
+ lines.push("\n");
3410
+ } else if (msg.role === "compactionSummary") {
3411
+ const compactMsg = msg as CompactionSummaryMessage;
3412
+ lines.push("## Compaction Summary\n");
3413
+ lines.push(`(${compactMsg.tokensBefore} tokens before compaction)\n`);
3414
+ lines.push(compactMsg.summary);
3415
+ lines.push("\n");
3416
+ } else if (msg.role === "fileMention") {
3417
+ const fileMsg = msg as FileMentionMessage;
3418
+ lines.push("## File Mention\n");
3419
+ for (const file of fileMsg.files) {
3420
+ lines.push(`<file path="${file.path}">`);
3421
+ lines.push(file.content);
3422
+ lines.push("</file>\n");
3423
+ }
3424
+ lines.push("\n");
3215
3425
  }
3216
3426
  }
3217
3427