@oh-my-pi/pi-coding-agent 6.9.0 → 6.9.69
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/CHANGELOG.md +41 -0
- package/package.json +6 -5
- package/src/cli/stats-cli.ts +191 -0
- package/src/core/agent-session.ts +103 -1
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/runner.ts +31 -0
- package/src/core/extensions/types.ts +24 -0
- package/src/core/messages.ts +48 -0
- package/src/core/session-manager.ts +10 -1
- package/src/core/tools/bash.ts +5 -7
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/patch/applicator.ts +115 -17
- package/src/core/tools/patch/index.ts +1 -1
- package/src/core/tools/patch/normalize.ts +185 -10
- package/src/core/tools/python.ts +444 -86
- package/src/core/tools/task/executor.ts +2 -6
- package/src/core/tools/task/index.ts +30 -12
- package/src/core/tools/task/render.ts +163 -30
- package/src/core/tools/task/template.ts +37 -0
- package/src/core/tools/task/types.ts +6 -2
- package/src/core/tools/task/worker.ts +1 -1
- package/src/index.ts +2 -0
- package/src/main.ts +12 -0
- package/src/modes/interactive/components/python-execution.ts +180 -0
- package/src/modes/interactive/components/welcome.ts +1 -0
- package/src/modes/interactive/controllers/command-controller.ts +46 -0
- package/src/modes/interactive/controllers/input-controller.ts +28 -1
- package/src/modes/interactive/interactive-mode.ts +10 -0
- package/src/modes/interactive/theme/dark.json +2 -9
- package/src/modes/interactive/theme/defaults/alabaster.json +2 -8
- package/src/modes/interactive/theme/defaults/amethyst.json +2 -9
- package/src/modes/interactive/theme/defaults/anthracite.json +2 -9
- package/src/modes/interactive/theme/defaults/basalt.json +89 -88
- package/src/modes/interactive/theme/defaults/birch.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-abyss.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-arctic.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-aurora.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-cavern.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-copper.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-dracula.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-ember.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-equinox.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-forest.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-github.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-lavender.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-lunar.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-midnight.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-monokai.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-nebula.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-nord.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-ocean.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-one.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-reef.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-retro.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-sakura.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-slate.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-solarized.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-solstice.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-starfall.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-sunset.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-swamp.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-taiga.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-terminal.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-tundra.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-twilight.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +2 -8
- package/src/modes/interactive/theme/defaults/graphite.json +2 -9
- package/src/modes/interactive/theme/defaults/light-arctic.json +2 -1
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +2 -8
- package/src/modes/interactive/theme/defaults/light-canyon.json +2 -8
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +2 -1
- package/src/modes/interactive/theme/defaults/light-cirrus.json +2 -8
- package/src/modes/interactive/theme/defaults/light-coral.json +3 -2
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +2 -9
- package/src/modes/interactive/theme/defaults/light-dawn.json +2 -8
- package/src/modes/interactive/theme/defaults/light-dunes.json +2 -8
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +3 -2
- package/src/modes/interactive/theme/defaults/light-forest.json +2 -9
- package/src/modes/interactive/theme/defaults/light-frost.json +3 -2
- package/src/modes/interactive/theme/defaults/light-github.json +2 -1
- package/src/modes/interactive/theme/defaults/light-glacier.json +2 -8
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +2 -9
- package/src/modes/interactive/theme/defaults/light-haze.json +2 -8
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +3 -2
- package/src/modes/interactive/theme/defaults/light-lagoon.json +2 -8
- package/src/modes/interactive/theme/defaults/light-lavender.json +3 -2
- package/src/modes/interactive/theme/defaults/light-meadow.json +2 -8
- package/src/modes/interactive/theme/defaults/light-mint.json +3 -2
- package/src/modes/interactive/theme/defaults/light-monochrome.json +2 -1
- package/src/modes/interactive/theme/defaults/light-ocean.json +2 -9
- package/src/modes/interactive/theme/defaults/light-one.json +2 -8
- package/src/modes/interactive/theme/defaults/light-opal.json +2 -8
- package/src/modes/interactive/theme/defaults/light-orchard.json +2 -8
- package/src/modes/interactive/theme/defaults/light-paper.json +3 -2
- package/src/modes/interactive/theme/defaults/light-prism.json +2 -8
- package/src/modes/interactive/theme/defaults/light-retro.json +2 -9
- package/src/modes/interactive/theme/defaults/light-sand.json +3 -2
- package/src/modes/interactive/theme/defaults/light-savanna.json +2 -8
- package/src/modes/interactive/theme/defaults/light-solarized.json +2 -1
- package/src/modes/interactive/theme/defaults/light-soleil.json +2 -8
- package/src/modes/interactive/theme/defaults/light-sunset.json +2 -9
- package/src/modes/interactive/theme/defaults/light-synthwave.json +2 -9
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +2 -9
- package/src/modes/interactive/theme/defaults/light-wetland.json +2 -8
- package/src/modes/interactive/theme/defaults/light-zenith.json +2 -8
- package/src/modes/interactive/theme/defaults/limestone.json +2 -8
- package/src/modes/interactive/theme/defaults/mahogany.json +2 -9
- package/src/modes/interactive/theme/defaults/marble.json +2 -8
- package/src/modes/interactive/theme/defaults/obsidian.json +89 -88
- package/src/modes/interactive/theme/defaults/onyx.json +89 -88
- package/src/modes/interactive/theme/defaults/pearl.json +2 -8
- package/src/modes/interactive/theme/defaults/porcelain.json +89 -88
- package/src/modes/interactive/theme/defaults/quartz.json +2 -8
- package/src/modes/interactive/theme/defaults/sandstone.json +2 -8
- package/src/modes/interactive/theme/defaults/titanium.json +88 -87
- package/src/modes/interactive/theme/light.json +2 -8
- package/src/modes/interactive/theme/theme-schema.json +5 -0
- package/src/modes/interactive/theme/theme.ts +7 -0
- package/src/modes/interactive/types.ts +5 -0
- package/src/modes/interactive/utils/ui-helpers.ts +20 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/python.md +40 -2
- package/src/prompts/tools/task.md +8 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [6.9.69] - 2026-01-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added cell-by-cell status tracking with duration and exit code for Python execution
|
|
10
|
+
- Added syntax highlighting for Python code in execution display
|
|
11
|
+
- Added template system with {{placeholders}} for task tool context
|
|
12
|
+
- Added task variables support for filling context placeholders
|
|
13
|
+
- Added enhanced task progress display with variable values
|
|
14
|
+
- Added concurrent work handling guidance in system prompt
|
|
15
|
+
- Added extension system support for user Python execution events
|
|
16
|
+
- Added Python mode border color theming across all themes
|
|
17
|
+
- Added Python execution indicator to welcome screen help text
|
|
18
|
+
- Added `omp stats` command for viewing AI usage statistics dashboard
|
|
19
|
+
- Added support for JSON output and console summary of usage statistics
|
|
20
|
+
- Added configurable port option for stats dashboard server
|
|
21
|
+
- Added multi-cell Python execution with sequential processing in persistent kernel
|
|
22
|
+
- Added cell titles for better Python code organization and debugging
|
|
23
|
+
- Added `$` command prefix for user-initiated Python execution in shared kernel
|
|
24
|
+
- Added `$$` prefix variant for Python execution excluded from LLM context
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Updated Python execution to display cells in bordered blocks with status indicators
|
|
29
|
+
- Changed task tool to use template-based context instead of simple concatenation
|
|
30
|
+
- Enhanced Python execution component with proper syntax highlighting
|
|
31
|
+
- Improved patch applicator to preserve exact indentation when intended
|
|
32
|
+
- Updated task tool schema to require vars instead of task field
|
|
33
|
+
- Updated Python execution component to use pythonMode theming instead of bashMode
|
|
34
|
+
- Enhanced UI helpers to handle pending Python components properly
|
|
35
|
+
- Changed Python tool to use `cells` array instead of single `code` parameter
|
|
36
|
+
- Renamed `workdir` parameter to `cwd` in Bash and Python tools for consistency
|
|
37
|
+
- Updated Python tool to display cell-by-cell output when multiple cells are provided
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Fixed indentation preservation for exact matches and indentation-only patches
|
|
42
|
+
- Fixed Python execution status updates to show real-time cell progress
|
|
43
|
+
- Fixed indentation adjustment logic to handle edge cases with mixed indentation levels
|
|
44
|
+
- Fixed patch indentation normalization for fuzzy matches, tab/space diffs, and ambiguous context alignment
|
|
45
|
+
|
|
5
46
|
## [6.9.0] - 2026-01-21
|
|
6
47
|
### Removed
|
|
7
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "6.9.
|
|
3
|
+
"version": "6.9.69",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@oh-my-pi/
|
|
44
|
-
"@oh-my-pi/pi-
|
|
45
|
-
"@oh-my-pi/pi-
|
|
46
|
-
"@oh-my-pi/pi-
|
|
43
|
+
"@oh-my-pi/omp-stats": "6.9.69",
|
|
44
|
+
"@oh-my-pi/pi-agent-core": "6.9.69",
|
|
45
|
+
"@oh-my-pi/pi-ai": "6.9.69",
|
|
46
|
+
"@oh-my-pi/pi-tui": "6.9.69",
|
|
47
|
+
"@oh-my-pi/pi-utils": "6.9.69",
|
|
47
48
|
"@openai/agents": "^0.3.7",
|
|
48
49
|
"@sinclair/typebox": "^0.34.46",
|
|
49
50
|
"ajv": "^8.17.1",
|
|
@@ -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
|
+
}
|
|
@@ -50,10 +50,11 @@ import type {
|
|
|
50
50
|
import type { CompactOptions, ContextUsage } from "./extensions/types";
|
|
51
51
|
import { extractFileMentions, generateFileMentionMessages } from "./file-mentions";
|
|
52
52
|
import type { HookCommandContext } from "./hooks/types";
|
|
53
|
-
import type { BashExecutionMessage, CustomMessage } from "./messages";
|
|
53
|
+
import type { BashExecutionMessage, CustomMessage, PythonExecutionMessage } from "./messages";
|
|
54
54
|
import type { ModelRegistry } from "./model-registry";
|
|
55
55
|
import { parseModelString } from "./model-resolver";
|
|
56
56
|
import { expandPromptTemplate, type PromptTemplate, parseCommandArgs, renderPromptTemplate } from "./prompt-templates";
|
|
57
|
+
import { executePython as executePythonCommand, type PythonResult } from "./python-executor";
|
|
57
58
|
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
|
|
58
59
|
import type { SettingsManager, SkillsSettings } from "./settings-manager";
|
|
59
60
|
import type { Skill, SkillWarning } from "./skills";
|
|
@@ -248,6 +249,10 @@ export class AgentSession {
|
|
|
248
249
|
private _bashAbortController: AbortController | undefined = undefined;
|
|
249
250
|
private _pendingBashMessages: BashExecutionMessage[] = [];
|
|
250
251
|
|
|
252
|
+
// Python execution state
|
|
253
|
+
private _pythonAbortController: AbortController | undefined = undefined;
|
|
254
|
+
private _pendingPythonMessages: PythonExecutionMessage[] = [];
|
|
255
|
+
|
|
251
256
|
// Extension system
|
|
252
257
|
private _extensionRunner: ExtensionRunner | undefined = undefined;
|
|
253
258
|
private _turnIndex = 0;
|
|
@@ -1005,6 +1010,7 @@ export class AgentSession {
|
|
|
1005
1010
|
|
|
1006
1011
|
// Flush any pending bash messages before the new prompt
|
|
1007
1012
|
this._flushPendingBashMessages();
|
|
1013
|
+
this._flushPendingPythonMessages();
|
|
1008
1014
|
|
|
1009
1015
|
// Reset todo reminder count on new user prompt
|
|
1010
1016
|
this._todoReminderCount = 0;
|
|
@@ -2631,6 +2637,102 @@ export class AgentSession {
|
|
|
2631
2637
|
this._pendingBashMessages = [];
|
|
2632
2638
|
}
|
|
2633
2639
|
|
|
2640
|
+
// =========================================================================
|
|
2641
|
+
// User-Initiated Python Execution
|
|
2642
|
+
// =========================================================================
|
|
2643
|
+
|
|
2644
|
+
/**
|
|
2645
|
+
* Execute Python code in the shared kernel.
|
|
2646
|
+
* Uses the same kernel session as the agent's Python tool, allowing collaborative editing.
|
|
2647
|
+
* @param code The Python code to execute
|
|
2648
|
+
* @param onChunk Optional streaming callback for output
|
|
2649
|
+
* @param options.excludeFromContext If true, execution won't be sent to LLM ($$ prefix)
|
|
2650
|
+
*/
|
|
2651
|
+
async executePython(
|
|
2652
|
+
code: string,
|
|
2653
|
+
onChunk?: (chunk: string) => void,
|
|
2654
|
+
options?: { excludeFromContext?: boolean },
|
|
2655
|
+
): Promise<PythonResult> {
|
|
2656
|
+
this._pythonAbortController = new AbortController();
|
|
2657
|
+
|
|
2658
|
+
try {
|
|
2659
|
+
// Use the same session ID as the Python tool for kernel sharing
|
|
2660
|
+
const sessionFile = this.sessionManager.getSessionFile();
|
|
2661
|
+
const cwd = this.sessionManager.getCwd();
|
|
2662
|
+
const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
|
|
2663
|
+
|
|
2664
|
+
const result = await executePythonCommand(code, {
|
|
2665
|
+
cwd,
|
|
2666
|
+
sessionId,
|
|
2667
|
+
kernelMode: this.settingsManager?.getPythonKernelMode?.() ?? "session",
|
|
2668
|
+
useSharedGateway: this.settingsManager?.getPythonSharedGateway?.() ?? true,
|
|
2669
|
+
onChunk,
|
|
2670
|
+
signal: this._pythonAbortController.signal,
|
|
2671
|
+
});
|
|
2672
|
+
|
|
2673
|
+
this.recordPythonResult(code, result, options);
|
|
2674
|
+
return result;
|
|
2675
|
+
} finally {
|
|
2676
|
+
this._pythonAbortController = undefined;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
/**
|
|
2681
|
+
* Record a Python execution result in session history.
|
|
2682
|
+
*/
|
|
2683
|
+
recordPythonResult(code: string, result: PythonResult, options?: { excludeFromContext?: boolean }): void {
|
|
2684
|
+
const pythonMessage: PythonExecutionMessage = {
|
|
2685
|
+
role: "pythonExecution",
|
|
2686
|
+
code,
|
|
2687
|
+
output: result.output,
|
|
2688
|
+
exitCode: result.exitCode,
|
|
2689
|
+
cancelled: result.cancelled,
|
|
2690
|
+
truncated: result.truncated,
|
|
2691
|
+
fullOutputPath: result.fullOutputPath,
|
|
2692
|
+
timestamp: Date.now(),
|
|
2693
|
+
excludeFromContext: options?.excludeFromContext,
|
|
2694
|
+
};
|
|
2695
|
+
|
|
2696
|
+
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
2697
|
+
if (this.isStreaming) {
|
|
2698
|
+
this._pendingPythonMessages.push(pythonMessage);
|
|
2699
|
+
} else {
|
|
2700
|
+
this.agent.appendMessage(pythonMessage);
|
|
2701
|
+
this.sessionManager.appendMessage(pythonMessage);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
/**
|
|
2706
|
+
* Cancel running Python execution.
|
|
2707
|
+
*/
|
|
2708
|
+
abortPython(): void {
|
|
2709
|
+
this._pythonAbortController?.abort();
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
/** Whether a Python execution is currently running */
|
|
2713
|
+
get isPythonRunning(): boolean {
|
|
2714
|
+
return this._pythonAbortController !== undefined;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
/** Whether there are pending Python messages waiting to be flushed */
|
|
2718
|
+
get hasPendingPythonMessages(): boolean {
|
|
2719
|
+
return this._pendingPythonMessages.length > 0;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/**
|
|
2723
|
+
* Flush pending Python messages to agent state and session.
|
|
2724
|
+
*/
|
|
2725
|
+
private _flushPendingPythonMessages(): void {
|
|
2726
|
+
if (this._pendingPythonMessages.length === 0) return;
|
|
2727
|
+
|
|
2728
|
+
for (const pythonMessage of this._pendingPythonMessages) {
|
|
2729
|
+
this.agent.appendMessage(pythonMessage);
|
|
2730
|
+
this.sessionManager.appendMessage(pythonMessage);
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
this._pendingPythonMessages = [];
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2634
2736
|
// =========================================================================
|
|
2635
2737
|
// Session Management
|
|
2636
2738
|
// =========================================================================
|
|
@@ -40,6 +40,8 @@ import type {
|
|
|
40
40
|
ToolResultEventResult,
|
|
41
41
|
UserBashEvent,
|
|
42
42
|
UserBashEventResult,
|
|
43
|
+
UserPythonEvent,
|
|
44
|
+
UserPythonEventResult,
|
|
43
45
|
} from "./types";
|
|
44
46
|
|
|
45
47
|
/** Combined result from all before_agent_start handlers */
|
|
@@ -461,6 +463,35 @@ export class ExtensionRunner {
|
|
|
461
463
|
return undefined;
|
|
462
464
|
}
|
|
463
465
|
|
|
466
|
+
async emitUserPython(event: UserPythonEvent): Promise<UserPythonEventResult | undefined> {
|
|
467
|
+
const ctx = this.createContext();
|
|
468
|
+
|
|
469
|
+
for (const ext of this.extensions) {
|
|
470
|
+
const handlers = ext.handlers.get("user_python");
|
|
471
|
+
if (!handlers || handlers.length === 0) continue;
|
|
472
|
+
|
|
473
|
+
for (const handler of handlers) {
|
|
474
|
+
try {
|
|
475
|
+
const handlerResult = await handler(event, ctx);
|
|
476
|
+
if (handlerResult) {
|
|
477
|
+
return handlerResult as UserPythonEventResult;
|
|
478
|
+
}
|
|
479
|
+
} catch (err) {
|
|
480
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
481
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
482
|
+
this.emitError({
|
|
483
|
+
extensionPath: ext.path,
|
|
484
|
+
event: "user_python",
|
|
485
|
+
error: message,
|
|
486
|
+
stack,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
|
|
464
495
|
/** Emit input event. Transforms chain, "handled" short-circuits. */
|
|
465
496
|
async emitInput(
|
|
466
497
|
text: string,
|
|
@@ -21,6 +21,7 @@ import type { ExecOptions, ExecResult } from "../exec";
|
|
|
21
21
|
import type { KeybindingsManager } from "../keybindings";
|
|
22
22
|
import type { CustomMessage } from "../messages";
|
|
23
23
|
import type { ModelRegistry } from "../model-registry";
|
|
24
|
+
import type { PythonResult } from "../python-executor";
|
|
24
25
|
import type {
|
|
25
26
|
BranchSummaryEntry,
|
|
26
27
|
CompactionEntry,
|
|
@@ -405,6 +406,21 @@ export interface UserBashEvent {
|
|
|
405
406
|
cwd: string;
|
|
406
407
|
}
|
|
407
408
|
|
|
409
|
+
// ============================================================================
|
|
410
|
+
// User Python Events
|
|
411
|
+
// ============================================================================
|
|
412
|
+
|
|
413
|
+
/** Fired when user executes Python code via $ or $$ prefix */
|
|
414
|
+
export interface UserPythonEvent {
|
|
415
|
+
type: "user_python";
|
|
416
|
+
/** The Python code to execute */
|
|
417
|
+
code: string;
|
|
418
|
+
/** True if $$ prefix was used (excluded from LLM context) */
|
|
419
|
+
excludeFromContext: boolean;
|
|
420
|
+
/** Current working directory */
|
|
421
|
+
cwd: string;
|
|
422
|
+
}
|
|
423
|
+
|
|
408
424
|
// ============================================================================
|
|
409
425
|
// Input Events
|
|
410
426
|
// ============================================================================
|
|
@@ -521,6 +537,7 @@ export type ExtensionEvent =
|
|
|
521
537
|
| TurnStartEvent
|
|
522
538
|
| TurnEndEvent
|
|
523
539
|
| UserBashEvent
|
|
540
|
+
| UserPythonEvent
|
|
524
541
|
| InputEvent
|
|
525
542
|
| ToolCallEvent
|
|
526
543
|
| ToolResultEvent;
|
|
@@ -554,6 +571,12 @@ export interface UserBashEventResult {
|
|
|
554
571
|
result?: BashResult;
|
|
555
572
|
}
|
|
556
573
|
|
|
574
|
+
/** Result from user_python event handler */
|
|
575
|
+
export interface UserPythonEventResult {
|
|
576
|
+
/** Full replacement: extension handled execution, use this result */
|
|
577
|
+
result?: PythonResult;
|
|
578
|
+
}
|
|
579
|
+
|
|
557
580
|
export interface ToolResultEventResult {
|
|
558
581
|
content?: (TextContent | ImageContent)[];
|
|
559
582
|
details?: unknown;
|
|
@@ -671,6 +694,7 @@ export interface ExtensionAPI {
|
|
|
671
694
|
on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
|
|
672
695
|
on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
|
|
673
696
|
on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
|
|
697
|
+
on(event: "user_python", handler: ExtensionHandler<UserPythonEvent, UserPythonEventResult>): void;
|
|
674
698
|
|
|
675
699
|
// =========================================================================
|
|
676
700
|
// Tool Registration
|
package/src/core/messages.ts
CHANGED
|
@@ -39,6 +39,23 @@ export interface BashExecutionMessage {
|
|
|
39
39
|
excludeFromContext?: boolean;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Message type for user-initiated Python executions via the $ command.
|
|
44
|
+
* Shares the same kernel session as the agent's Python tool.
|
|
45
|
+
*/
|
|
46
|
+
export interface PythonExecutionMessage {
|
|
47
|
+
role: "pythonExecution";
|
|
48
|
+
code: string;
|
|
49
|
+
output: string;
|
|
50
|
+
exitCode: number | undefined;
|
|
51
|
+
cancelled: boolean;
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
fullOutputPath?: string;
|
|
54
|
+
timestamp: number;
|
|
55
|
+
/** If true, this message is excluded from LLM context ($$ prefix) */
|
|
56
|
+
excludeFromContext?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
42
59
|
/**
|
|
43
60
|
* Message type for extension-injected messages via sendMessage().
|
|
44
61
|
*/
|
|
@@ -95,6 +112,7 @@ export interface FileMentionMessage {
|
|
|
95
112
|
declare module "@oh-my-pi/pi-agent-core" {
|
|
96
113
|
interface CustomAgentMessages {
|
|
97
114
|
bashExecution: BashExecutionMessage;
|
|
115
|
+
pythonExecution: PythonExecutionMessage;
|
|
98
116
|
custom: CustomMessage;
|
|
99
117
|
hookMessage: HookMessage;
|
|
100
118
|
branchSummary: BranchSummaryMessage;
|
|
@@ -124,6 +142,27 @@ export function bashExecutionToText(msg: BashExecutionMessage): string {
|
|
|
124
142
|
return text;
|
|
125
143
|
}
|
|
126
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Convert a PythonExecutionMessage to user message text for LLM context.
|
|
147
|
+
*/
|
|
148
|
+
export function pythonExecutionToText(msg: PythonExecutionMessage): string {
|
|
149
|
+
let text = `Ran Python:\n\`\`\`python\n${msg.code}\n\`\`\`\n`;
|
|
150
|
+
if (msg.output) {
|
|
151
|
+
text += `Output:\n\`\`\`\n${msg.output}\n\`\`\``;
|
|
152
|
+
} else {
|
|
153
|
+
text += "(no output)";
|
|
154
|
+
}
|
|
155
|
+
if (msg.cancelled) {
|
|
156
|
+
text += "\n\n(execution cancelled)";
|
|
157
|
+
} else if (msg.exitCode !== null && msg.exitCode !== undefined && msg.exitCode !== 0) {
|
|
158
|
+
text += `\n\nExecution failed with code ${msg.exitCode}`;
|
|
159
|
+
}
|
|
160
|
+
if (msg.truncated && msg.fullOutputPath) {
|
|
161
|
+
text += `\n\n[Output truncated. Full output: ${msg.fullOutputPath}]`;
|
|
162
|
+
}
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
|
|
127
166
|
export function createBranchSummaryMessage(summary: string, fromId: string, timestamp: string): BranchSummaryMessage {
|
|
128
167
|
return {
|
|
129
168
|
role: "branchSummary",
|
|
@@ -185,6 +224,15 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
|
185
224
|
content: [{ type: "text", text: bashExecutionToText(m) }],
|
|
186
225
|
timestamp: m.timestamp,
|
|
187
226
|
};
|
|
227
|
+
case "pythonExecution":
|
|
228
|
+
if (m.excludeFromContext) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
role: "user",
|
|
233
|
+
content: [{ type: "text", text: pythonExecutionToText(m) }],
|
|
234
|
+
timestamp: m.timestamp,
|
|
235
|
+
};
|
|
188
236
|
case "custom":
|
|
189
237
|
case "hookMessage": {
|
|
190
238
|
const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
createCustomMessage,
|
|
14
14
|
type FileMentionMessage,
|
|
15
15
|
type HookMessage,
|
|
16
|
+
type PythonExecutionMessage,
|
|
16
17
|
} from "./messages";
|
|
17
18
|
import type { SessionStorage, SessionStorageWriter } from "./session-storage";
|
|
18
19
|
import { FileSessionStorage, MemorySessionStorage } from "./session-storage";
|
|
@@ -1306,7 +1307,15 @@ export class SessionManager {
|
|
|
1306
1307
|
* so it is easier to find them.
|
|
1307
1308
|
* These need to be appended via appendCompaction() and appendBranchSummary() methods.
|
|
1308
1309
|
*/
|
|
1309
|
-
appendMessage(
|
|
1310
|
+
appendMessage(
|
|
1311
|
+
message:
|
|
1312
|
+
| Message
|
|
1313
|
+
| CustomMessage
|
|
1314
|
+
| HookMessage
|
|
1315
|
+
| BashExecutionMessage
|
|
1316
|
+
| PythonExecutionMessage
|
|
1317
|
+
| FileMentionMessage,
|
|
1318
|
+
): string {
|
|
1310
1319
|
const entry: SessionMessageEntry = {
|
|
1311
1320
|
type: "message",
|
|
1312
1321
|
id: generateId(this.byId),
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -20,9 +20,7 @@ export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
|
20
20
|
const bashSchema = Type.Object({
|
|
21
21
|
command: Type.String({ description: "Bash command to execute" }),
|
|
22
22
|
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
|
23
|
-
|
|
24
|
-
Type.String({ description: "Working directory for the command (default: current directory)" }),
|
|
25
|
-
),
|
|
23
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the command (default: current directory)" })),
|
|
26
24
|
});
|
|
27
25
|
|
|
28
26
|
export interface BashToolDetails {
|
|
@@ -53,7 +51,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
|
53
51
|
|
|
54
52
|
public async execute(
|
|
55
53
|
_toolCallId: string,
|
|
56
|
-
{ command, timeout,
|
|
54
|
+
{ command, timeout, cwd }: { command: string; timeout?: number; cwd?: string },
|
|
57
55
|
signal?: AbortSignal,
|
|
58
56
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
|
|
59
57
|
ctx?: AgentToolContext,
|
|
@@ -73,7 +71,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
|
73
71
|
}
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
const commandCwd =
|
|
74
|
+
const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
|
|
77
75
|
let cwdStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
|
|
78
76
|
try {
|
|
79
77
|
cwdStat = await Bun.file(commandCwd).stat();
|
|
@@ -143,7 +141,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
|
143
141
|
interface BashRenderArgs {
|
|
144
142
|
command?: string;
|
|
145
143
|
timeout?: number;
|
|
146
|
-
|
|
144
|
+
cwd?: string;
|
|
147
145
|
}
|
|
148
146
|
|
|
149
147
|
interface BashRenderContext {
|
|
@@ -166,7 +164,7 @@ export const bashToolRenderer = {
|
|
|
166
164
|
const command = args.command || uiTheme.format.ellipsis;
|
|
167
165
|
const prompt = uiTheme.fg("accent", "$");
|
|
168
166
|
const cwd = process.cwd();
|
|
169
|
-
let displayWorkdir = args.
|
|
167
|
+
let displayWorkdir = args.cwd;
|
|
170
168
|
|
|
171
169
|
if (displayWorkdir) {
|
|
172
170
|
const resolvedCwd = resolve(cwd);
|
package/src/core/tools/index.ts
CHANGED
|
@@ -221,7 +221,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
221
221
|
});
|
|
222
222
|
} else if (!isTestEnv && getPreludeDocs().length === 0) {
|
|
223
223
|
const sessionFile = session.getSessionFile?.() ?? undefined;
|
|
224
|
-
const warmSessionId = sessionFile ? `session:${sessionFile}:
|
|
224
|
+
const warmSessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
|
|
225
225
|
void warmPythonEnvironment(session.cwd, warmSessionId, session.settings?.getPythonSharedGateway?.()).catch(
|
|
226
226
|
(err) => {
|
|
227
227
|
logger.warn("Failed to warm Python environment", {
|