@oh-my-pi/pi-coding-agent 14.0.5 → 14.1.1
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 +120 -0
- package/package.json +8 -8
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +43 -10
- package/src/async/support.ts +5 -0
- package/src/cli/list-models.ts +96 -57
- package/src/commit/agentic/tools/analyze-file.ts +1 -2
- package/src/commit/model-selection.ts +16 -13
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-equivalence.ts +675 -0
- package/src/config/model-registry.ts +242 -45
- package/src/config/model-resolver.ts +282 -65
- package/src/config/settings-schema.ts +27 -3
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +64 -23
- package/src/edit/index.ts +254 -89
- package/src/edit/modes/chunk.ts +336 -57
- package/src/edit/modes/hashline.ts +51 -26
- package/src/edit/modes/patch.ts +16 -10
- package/src/edit/modes/replace.ts +15 -7
- package/src/edit/renderer.ts +248 -94
- package/src/export/html/template.css +82 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +614 -97
- package/src/extensibility/custom-tools/types.ts +0 -3
- package/src/extensibility/extensions/loader.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -7
- package/src/extensibility/extensions/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/jobs-protocol.ts +2 -1
- package/src/ipy/executor.ts +447 -52
- package/src/ipy/kernel.ts +39 -13
- package/src/lsp/client.ts +55 -1
- package/src/lsp/index.ts +8 -0
- package/src/lsp/types.ts +6 -0
- package/src/main.ts +6 -2
- package/src/memories/index.ts +7 -6
- package/src/modes/acp/acp-agent.ts +4 -1
- package/src/modes/components/bash-execution.ts +16 -4
- package/src/modes/components/model-selector.ts +221 -64
- package/src/modes/components/status-line/presets.ts +17 -6
- package/src/modes/components/status-line/segments.ts +15 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/tool-execution.ts +145 -75
- package/src/modes/controllers/command-controller.ts +42 -1
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +28 -5
- package/src/modes/controllers/input-controller.ts +9 -3
- package/src/modes/controllers/selector-controller.ts +17 -6
- package/src/modes/interactive-mode.ts +19 -3
- package/src/modes/print-mode.ts +13 -4
- package/src/modes/prompt-action-autocomplete.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -2
- package/src/modes/shared.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +16 -1
- package/src/prompts/tools/cancel-job.md +1 -1
- package/src/prompts/tools/chunk-edit.md +191 -163
- package/src/prompts/tools/hashline.md +11 -11
- package/src/prompts/tools/patch.md +10 -5
- package/src/prompts/tools/{await.md → poll.md} +1 -1
- package/src/prompts/tools/read-chunk.md +12 -3
- package/src/prompts/tools/read.md +9 -0
- package/src/prompts/tools/task.md +2 -2
- package/src/prompts/tools/vim.md +98 -0
- package/src/prompts/tools/write.md +1 -0
- package/src/sdk.ts +758 -725
- package/src/session/agent-session.ts +187 -40
- package/src/session/session-manager.ts +50 -4
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/task/executor.ts +9 -5
- package/src/task/index.ts +3 -5
- package/src/task/types.ts +2 -2
- package/src/tools/bash.ts +240 -57
- package/src/tools/cancel-job.ts +2 -1
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +77 -8
- package/src/tools/index.ts +48 -19
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
- package/src/tools/python.ts +293 -278
- package/src/tools/read.ts +218 -1
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/submit-result.ts +5 -2
- package/src/tools/todo-write.ts +8 -2
- package/src/tools/vim.ts +966 -0
- package/src/tools/write.ts +187 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/git.ts +24 -1
- package/src/utils/session-color.ts +55 -0
- package/src/utils/title-generator.ts +16 -7
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
|
@@ -49,13 +49,22 @@ import {
|
|
|
49
49
|
modelsAreEqual,
|
|
50
50
|
parseRateLimitReason,
|
|
51
51
|
} from "@oh-my-pi/pi-ai";
|
|
52
|
-
import { killTree, MacOSPowerAssertion
|
|
53
|
-
import {
|
|
52
|
+
import { killTree, MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
53
|
+
import {
|
|
54
|
+
abortableSleep,
|
|
55
|
+
getAgentDbPath,
|
|
56
|
+
isEnoent,
|
|
57
|
+
logger,
|
|
58
|
+
prompt,
|
|
59
|
+
Snowflake,
|
|
60
|
+
setNativeKillTree,
|
|
61
|
+
} from "@oh-my-pi/pi-utils";
|
|
54
62
|
import type { AsyncJob, AsyncJobManager } from "../async";
|
|
55
63
|
import type { Rule } from "../capability/rule";
|
|
56
64
|
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
57
65
|
import {
|
|
58
66
|
extractExplicitThinkingSelector,
|
|
67
|
+
formatModelSelectorValue,
|
|
59
68
|
formatModelString,
|
|
60
69
|
parseModelString,
|
|
61
70
|
type ResolvedModelRoleValue,
|
|
@@ -94,7 +103,11 @@ import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
|
94
103
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
95
104
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
96
105
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
97
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
disposeKernelSessionsByOwner,
|
|
108
|
+
executePython as executePythonCommand,
|
|
109
|
+
type PythonResult,
|
|
110
|
+
} from "../ipy/executor";
|
|
98
111
|
import {
|
|
99
112
|
buildDiscoverableMCPSearchIndex,
|
|
100
113
|
collectDiscoverableMCPTools,
|
|
@@ -125,6 +138,7 @@ import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from ".
|
|
|
125
138
|
import { ToolError } from "../tools/tool-errors";
|
|
126
139
|
import { clampTimeout } from "../tools/tool-timeouts";
|
|
127
140
|
import { parseCommandArgs } from "../utils/command-args";
|
|
141
|
+
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
128
142
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
129
143
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
130
144
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
@@ -243,8 +257,8 @@ export interface AgentSessionConfig {
|
|
|
243
257
|
ttsrManager?: TtsrManager;
|
|
244
258
|
/** Secret obfuscator for deobfuscating streaming edit content */
|
|
245
259
|
obfuscator?: SecretObfuscator;
|
|
246
|
-
/**
|
|
247
|
-
|
|
260
|
+
/** Logical owner for retained Python kernels created by this session. */
|
|
261
|
+
pythonKernelOwnerId?: string;
|
|
248
262
|
}
|
|
249
263
|
|
|
250
264
|
/** Options for AgentSession.prompt() */
|
|
@@ -396,7 +410,6 @@ export class AgentSession {
|
|
|
396
410
|
readonly agent: Agent;
|
|
397
411
|
readonly sessionManager: SessionManager;
|
|
398
412
|
readonly settings: Settings;
|
|
399
|
-
readonly searchDb: SearchDb | undefined;
|
|
400
413
|
|
|
401
414
|
#powerAssertion: MacOSPowerAssertion | undefined;
|
|
402
415
|
|
|
@@ -451,9 +464,11 @@ export class AgentSession {
|
|
|
451
464
|
#pendingBashMessages: BashExecutionMessage[] = [];
|
|
452
465
|
|
|
453
466
|
// Python execution state
|
|
454
|
-
#
|
|
467
|
+
#pythonAbortControllers = new Set<AbortController>();
|
|
468
|
+
#pythonKernelOwnerId: string;
|
|
455
469
|
#pendingPythonMessages: PythonExecutionMessage[] = [];
|
|
456
|
-
|
|
470
|
+
#activePythonExecutions = new Set<Promise<unknown>>();
|
|
471
|
+
#pythonExecutionDisposing = false;
|
|
457
472
|
// Extension system
|
|
458
473
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
459
474
|
#turnIndex = 0;
|
|
@@ -543,9 +558,9 @@ export class AgentSession {
|
|
|
543
558
|
this.agent = config.agent;
|
|
544
559
|
this.sessionManager = config.sessionManager;
|
|
545
560
|
this.settings = config.settings;
|
|
546
|
-
this.searchDb = config.searchDb;
|
|
547
561
|
this.#startPowerAssertion();
|
|
548
562
|
this.#asyncJobManager = config.asyncJobManager;
|
|
563
|
+
this.#pythonKernelOwnerId = config.pythonKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
549
564
|
this.#scopedModels = config.scopedModels ?? [];
|
|
550
565
|
this.#thinkingLevel = config.thinkingLevel;
|
|
551
566
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
@@ -691,7 +706,23 @@ export class AgentSession {
|
|
|
691
706
|
}
|
|
692
707
|
}
|
|
693
708
|
|
|
709
|
+
#queuedExtensionEvents: Promise<void> = Promise.resolve();
|
|
710
|
+
|
|
711
|
+
#queueExtensionEvent(event: AgentSessionEvent): Promise<void> {
|
|
712
|
+
const emit = async () => {
|
|
713
|
+
await this.#emitExtensionEvent(event);
|
|
714
|
+
};
|
|
715
|
+
const queued = this.#queuedExtensionEvents.then(emit, emit);
|
|
716
|
+
this.#queuedExtensionEvents = queued.catch(() => {});
|
|
717
|
+
return queued;
|
|
718
|
+
}
|
|
719
|
+
|
|
694
720
|
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
721
|
+
if (event.type === "message_update") {
|
|
722
|
+
this.#emit(event);
|
|
723
|
+
void this.#queueExtensionEvent(event);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
695
726
|
await this.#emitExtensionEvent(event);
|
|
696
727
|
this.#emit(event);
|
|
697
728
|
}
|
|
@@ -1797,6 +1828,7 @@ export class AgentSession {
|
|
|
1797
1828
|
* Call this when completely done with the session.
|
|
1798
1829
|
*/
|
|
1799
1830
|
async dispose(): Promise<void> {
|
|
1831
|
+
this.#pythonExecutionDisposing = true;
|
|
1800
1832
|
try {
|
|
1801
1833
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
1802
1834
|
await this.#extensionRunner.emit({ type: "session_shutdown" });
|
|
@@ -1811,6 +1843,13 @@ export class AgentSession {
|
|
|
1811
1843
|
if (drained === false && deliveryState) {
|
|
1812
1844
|
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
1813
1845
|
}
|
|
1846
|
+
const pythonExecutionsSettled = await this.#preparePythonExecutionsForDispose();
|
|
1847
|
+
if (!pythonExecutionsSettled) {
|
|
1848
|
+
logger.warn(
|
|
1849
|
+
"Detaching retained Python kernel ownership during dispose while Python execution is still active",
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
await disposeKernelSessionsByOwner(this.#pythonKernelOwnerId);
|
|
1814
1853
|
this.#stopPowerAssertion();
|
|
1815
1854
|
await this.sessionManager.close();
|
|
1816
1855
|
this.#closeAllProviderSessions("dispose");
|
|
@@ -1969,6 +2008,24 @@ export class AgentSession {
|
|
|
1969
2008
|
return Array.from(this.#toolRegistry.keys());
|
|
1970
2009
|
}
|
|
1971
2010
|
|
|
2011
|
+
#getEditModeSession() {
|
|
2012
|
+
return {
|
|
2013
|
+
settings: this.settings,
|
|
2014
|
+
getActiveModelString: () => (this.model ? formatModelString(this.model) : undefined),
|
|
2015
|
+
} as const;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
#resolveActiveEditMode(): EditMode {
|
|
2019
|
+
return resolveEditMode(this.#getEditModeSession());
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
async #syncEditToolModeAfterModelChange(previousEditMode: EditMode): Promise<void> {
|
|
2023
|
+
const currentEditMode = this.#resolveActiveEditMode();
|
|
2024
|
+
if (previousEditMode !== currentEditMode && this.getActiveToolNames().includes("edit")) {
|
|
2025
|
+
await this.refreshBaseSystemPrompt();
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1972
2029
|
isMCPDiscoveryEnabled(): boolean {
|
|
1973
2030
|
return this.#mcpDiscoveryEnabled;
|
|
1974
2031
|
}
|
|
@@ -2016,6 +2073,7 @@ export class AgentSession {
|
|
|
2016
2073
|
toolNames: string[],
|
|
2017
2074
|
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
|
|
2018
2075
|
): Promise<void> {
|
|
2076
|
+
toolNames = [...new Set(toolNames.map(name => name.toLowerCase()))];
|
|
2019
2077
|
const previousSelectedMCPToolNames = options?.previousSelectedMCPToolNames ?? this.getSelectedMCPToolNames();
|
|
2020
2078
|
const tools: AgentTool[] = [];
|
|
2021
2079
|
const validToolNames: string[] = [];
|
|
@@ -2107,7 +2165,6 @@ export class AgentSession {
|
|
|
2107
2165
|
sessionManager: this.sessionManager,
|
|
2108
2166
|
modelRegistry: this.#modelRegistry,
|
|
2109
2167
|
model: this.model,
|
|
2110
|
-
searchDb: this.searchDb,
|
|
2111
2168
|
isIdle: () => !this.isStreaming,
|
|
2112
2169
|
hasQueuedMessages: () => this.queuedMessageCount > 0,
|
|
2113
2170
|
abort: () => {
|
|
@@ -3313,8 +3370,8 @@ export class AgentSession {
|
|
|
3313
3370
|
/**
|
|
3314
3371
|
* Set a display name for the current session.
|
|
3315
3372
|
*/
|
|
3316
|
-
setSessionName(name: string):
|
|
3317
|
-
this.sessionManager.setSessionName(name);
|
|
3373
|
+
setSessionName(name: string, source: "auto" | "user" = "auto"): Promise<boolean> {
|
|
3374
|
+
return this.sessionManager.setSessionName(name, source);
|
|
3318
3375
|
}
|
|
3319
3376
|
|
|
3320
3377
|
/**
|
|
@@ -3390,7 +3447,12 @@ export class AgentSession {
|
|
|
3390
3447
|
* Validates API key, saves to session and settings.
|
|
3391
3448
|
* @throws Error if no API key available for the model
|
|
3392
3449
|
*/
|
|
3393
|
-
async setModel(
|
|
3450
|
+
async setModel(
|
|
3451
|
+
model: Model,
|
|
3452
|
+
role: string = "default",
|
|
3453
|
+
options?: { selector?: string; thinkingLevel?: ThinkingLevel },
|
|
3454
|
+
): Promise<void> {
|
|
3455
|
+
const previousEditMode = this.#resolveActiveEditMode();
|
|
3394
3456
|
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
3395
3457
|
if (!apiKey) {
|
|
3396
3458
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -3399,11 +3461,15 @@ export class AgentSession {
|
|
|
3399
3461
|
this.#clearActiveRetryFallback();
|
|
3400
3462
|
this.#setModelWithProviderSessionReset(model);
|
|
3401
3463
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
|
|
3402
|
-
this.settings.setModelRole(
|
|
3464
|
+
this.settings.setModelRole(
|
|
3465
|
+
role,
|
|
3466
|
+
this.#formatRoleModelValue(role, model, options?.selector, options?.thinkingLevel),
|
|
3467
|
+
);
|
|
3403
3468
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
3404
3469
|
|
|
3405
3470
|
// Re-apply the current thinking level for the newly selected model
|
|
3406
3471
|
this.setThinkingLevel(this.thinkingLevel);
|
|
3472
|
+
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
3407
3473
|
}
|
|
3408
3474
|
|
|
3409
3475
|
/**
|
|
@@ -3412,6 +3478,7 @@ export class AgentSession {
|
|
|
3412
3478
|
* @throws Error if no API key available for the model
|
|
3413
3479
|
*/
|
|
3414
3480
|
async setModelTemporary(model: Model, thinkingLevel?: ThinkingLevel): Promise<void> {
|
|
3481
|
+
const previousEditMode = this.#resolveActiveEditMode();
|
|
3415
3482
|
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
3416
3483
|
if (!apiKey) {
|
|
3417
3484
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -3424,6 +3491,7 @@ export class AgentSession {
|
|
|
3424
3491
|
|
|
3425
3492
|
// Apply explicit thinking level, or re-clamp current level to new model's capabilities
|
|
3426
3493
|
this.setThinkingLevel(thinkingLevel ?? this.thinkingLevel);
|
|
3494
|
+
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
3427
3495
|
}
|
|
3428
3496
|
|
|
3429
3497
|
/**
|
|
@@ -3472,6 +3540,7 @@ export class AgentSession {
|
|
|
3472
3540
|
const resolved = resolveModelRoleValue(roleModelStr, availableModels, {
|
|
3473
3541
|
settings: this.settings,
|
|
3474
3542
|
matchPreferences,
|
|
3543
|
+
modelRegistry: this.#modelRegistry,
|
|
3475
3544
|
});
|
|
3476
3545
|
if (!resolved.model) continue;
|
|
3477
3546
|
|
|
@@ -3530,6 +3599,7 @@ export class AgentSession {
|
|
|
3530
3599
|
}
|
|
3531
3600
|
|
|
3532
3601
|
async #cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
|
3602
|
+
const previousEditMode = this.#resolveActiveEditMode();
|
|
3533
3603
|
const scopedModels = await this.#getScopedModelsWithApiKey();
|
|
3534
3604
|
if (scopedModels.length <= 1) return undefined;
|
|
3535
3605
|
|
|
@@ -3550,11 +3620,13 @@ export class AgentSession {
|
|
|
3550
3620
|
|
|
3551
3621
|
// Apply the scoped model's configured thinking level
|
|
3552
3622
|
this.setThinkingLevel(next.thinkingLevel);
|
|
3623
|
+
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
3553
3624
|
|
|
3554
3625
|
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
|
3555
3626
|
}
|
|
3556
3627
|
|
|
3557
3628
|
async #cycleAvailableModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
|
3629
|
+
const previousEditMode = this.#resolveActiveEditMode();
|
|
3558
3630
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
3559
3631
|
if (availableModels.length <= 1) return undefined;
|
|
3560
3632
|
|
|
@@ -3578,6 +3650,7 @@ export class AgentSession {
|
|
|
3578
3650
|
this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
|
3579
3651
|
// Re-apply the current thinking level for the newly selected model
|
|
3580
3652
|
this.setThinkingLevel(this.thinkingLevel);
|
|
3653
|
+
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
3581
3654
|
|
|
3582
3655
|
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
|
|
3583
3656
|
}
|
|
@@ -4236,6 +4309,14 @@ export class AgentSession {
|
|
|
4236
4309
|
return undefined;
|
|
4237
4310
|
}
|
|
4238
4311
|
|
|
4312
|
+
// Only inject on the first user message of the conversation. Subsequent user
|
|
4313
|
+
// turns must not receive the eager todo reminder — they often correct, clarify,
|
|
4314
|
+
// or redirect the prior task, and forcing a brand-new todo list there is wrong.
|
|
4315
|
+
const hasPriorUserMessage = this.agent.state.messages.some(m => m.role === "user");
|
|
4316
|
+
if (hasPriorUserMessage) {
|
|
4317
|
+
return undefined;
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4239
4320
|
const trimmedPromptText = promptText.trimEnd();
|
|
4240
4321
|
if (trimmedPromptText.endsWith("?") || trimmedPromptText.endsWith("!")) {
|
|
4241
4322
|
return undefined;
|
|
@@ -4588,14 +4669,21 @@ export class AgentSession {
|
|
|
4588
4669
|
return `${model.provider}/${model.id}`;
|
|
4589
4670
|
}
|
|
4590
4671
|
|
|
4591
|
-
#formatRoleModelValue(
|
|
4592
|
-
|
|
4672
|
+
#formatRoleModelValue(
|
|
4673
|
+
role: string,
|
|
4674
|
+
model: Model,
|
|
4675
|
+
selectorOverride?: string,
|
|
4676
|
+
thinkingLevelOverride?: ThinkingLevel,
|
|
4677
|
+
): string {
|
|
4678
|
+
const modelKey = selectorOverride ?? `${model.provider}/${model.id}`;
|
|
4679
|
+
if (thinkingLevelOverride !== undefined) {
|
|
4680
|
+
return formatModelSelectorValue(modelKey, thinkingLevelOverride);
|
|
4681
|
+
}
|
|
4593
4682
|
const existingRoleValue = this.settings.getModelRole(role);
|
|
4594
4683
|
if (!existingRoleValue) return modelKey;
|
|
4595
4684
|
|
|
4596
4685
|
const thinkingLevel = extractExplicitThinkingSelector(existingRoleValue, this.settings);
|
|
4597
|
-
|
|
4598
|
-
return `${modelKey}:${thinkingLevel}`;
|
|
4686
|
+
return formatModelSelectorValue(modelKey, thinkingLevel);
|
|
4599
4687
|
}
|
|
4600
4688
|
#resolveContextPromotionConfiguredTarget(currentModel: Model, availableModels: Model[]): Model | undefined {
|
|
4601
4689
|
const configuredTarget = currentModel.contextPromotionTarget?.trim();
|
|
@@ -4628,6 +4716,7 @@ export class AgentSession {
|
|
|
4628
4716
|
return resolveModelRoleValue(roleModelStr, availableModels, {
|
|
4629
4717
|
settings: this.settings,
|
|
4630
4718
|
matchPreferences: { usageOrder: this.settings.getStorage()?.getModelUsageOrder() },
|
|
4719
|
+
modelRegistry: this.#modelRegistry,
|
|
4631
4720
|
});
|
|
4632
4721
|
}
|
|
4633
4722
|
|
|
@@ -5644,43 +5733,67 @@ export class AgentSession {
|
|
|
5644
5733
|
): Promise<PythonResult> {
|
|
5645
5734
|
const excludeFromContext = options?.excludeFromContext === true;
|
|
5646
5735
|
const cwd = this.sessionManager.getCwd();
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5736
|
+
this.assertPythonExecutionAllowed();
|
|
5737
|
+
|
|
5738
|
+
const abortController = new AbortController();
|
|
5739
|
+
const execution = (async (): Promise<PythonResult> => {
|
|
5740
|
+
if (this.#extensionRunner?.hasHandlers("user_python")) {
|
|
5741
|
+
const hookResult = await this.#extensionRunner.emitUserPython({
|
|
5742
|
+
type: "user_python",
|
|
5743
|
+
code,
|
|
5744
|
+
excludeFromContext,
|
|
5745
|
+
cwd,
|
|
5746
|
+
});
|
|
5747
|
+
this.assertPythonExecutionAllowed();
|
|
5748
|
+
if (hookResult?.result) {
|
|
5749
|
+
this.recordPythonResult(code, hookResult.result, options);
|
|
5750
|
+
return hookResult.result;
|
|
5751
|
+
}
|
|
5658
5752
|
}
|
|
5659
|
-
}
|
|
5660
5753
|
|
|
5661
|
-
this.#pythonAbortController = new AbortController();
|
|
5662
|
-
|
|
5663
|
-
try {
|
|
5664
5754
|
// Use the same session ID as the Python tool for kernel sharing
|
|
5665
5755
|
const sessionFile = this.sessionManager.getSessionFile();
|
|
5666
5756
|
const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
|
|
5667
|
-
|
|
5668
5757
|
const result = await executePythonCommand(code, {
|
|
5669
5758
|
cwd,
|
|
5670
5759
|
sessionId,
|
|
5760
|
+
kernelOwnerId: this.#pythonKernelOwnerId,
|
|
5671
5761
|
kernelMode: this.settings.get("python.kernelMode"),
|
|
5672
5762
|
useSharedGateway: this.settings.get("python.sharedGateway"),
|
|
5673
5763
|
onChunk,
|
|
5674
|
-
signal:
|
|
5764
|
+
signal: abortController.signal,
|
|
5675
5765
|
});
|
|
5676
|
-
|
|
5677
5766
|
this.recordPythonResult(code, result, options);
|
|
5678
5767
|
return result;
|
|
5679
|
-
}
|
|
5680
|
-
|
|
5768
|
+
})();
|
|
5769
|
+
return await this.trackPythonExecution(execution, abortController);
|
|
5770
|
+
}
|
|
5771
|
+
|
|
5772
|
+
assertPythonExecutionAllowed(): void {
|
|
5773
|
+
if (this.#pythonExecutionDisposing) {
|
|
5774
|
+
throw new Error("Python execution is unavailable while session disposal is in progress");
|
|
5681
5775
|
}
|
|
5682
5776
|
}
|
|
5683
5777
|
|
|
5778
|
+
/**
|
|
5779
|
+
* Track Python work started outside AgentSession.executePython so dispose can await and abort it too.
|
|
5780
|
+
*/
|
|
5781
|
+
trackPythonExecution<T>(execution: Promise<T>, abortController: AbortController): Promise<T> {
|
|
5782
|
+
this.#pythonAbortControllers.add(abortController);
|
|
5783
|
+
this.#activePythonExecutions.add(execution);
|
|
5784
|
+
void execution.then(
|
|
5785
|
+
() => {
|
|
5786
|
+
this.#pythonAbortControllers.delete(abortController);
|
|
5787
|
+
this.#activePythonExecutions.delete(execution);
|
|
5788
|
+
},
|
|
5789
|
+
() => {
|
|
5790
|
+
this.#pythonAbortControllers.delete(abortController);
|
|
5791
|
+
this.#activePythonExecutions.delete(execution);
|
|
5792
|
+
},
|
|
5793
|
+
);
|
|
5794
|
+
return execution;
|
|
5795
|
+
}
|
|
5796
|
+
|
|
5684
5797
|
/**
|
|
5685
5798
|
* Record a Python execution result in session history.
|
|
5686
5799
|
*/
|
|
@@ -5711,12 +5824,46 @@ export class AgentSession {
|
|
|
5711
5824
|
* Cancel running Python execution.
|
|
5712
5825
|
*/
|
|
5713
5826
|
abortPython(): void {
|
|
5714
|
-
this.#
|
|
5827
|
+
for (const abortController of this.#pythonAbortControllers) {
|
|
5828
|
+
abortController.abort();
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5831
|
+
|
|
5832
|
+
async #waitForPythonExecutionsToSettle(timeoutMs: number): Promise<boolean> {
|
|
5833
|
+
const deadline = Date.now() + timeoutMs;
|
|
5834
|
+
while (this.#activePythonExecutions.size > 0) {
|
|
5835
|
+
const remainingMs = deadline - Date.now();
|
|
5836
|
+
if (remainingMs <= 0) {
|
|
5837
|
+
return false;
|
|
5838
|
+
}
|
|
5839
|
+
const settled = await Promise.race([
|
|
5840
|
+
Promise.allSettled(Array.from(this.#activePythonExecutions)).then(() => true),
|
|
5841
|
+
Bun.sleep(remainingMs).then(() => false),
|
|
5842
|
+
]);
|
|
5843
|
+
if (!settled && this.#activePythonExecutions.size > 0) {
|
|
5844
|
+
return false;
|
|
5845
|
+
}
|
|
5846
|
+
}
|
|
5847
|
+
return true;
|
|
5848
|
+
}
|
|
5849
|
+
|
|
5850
|
+
async #preparePythonExecutionsForDispose(): Promise<boolean> {
|
|
5851
|
+
if (!(await this.#waitForPythonExecutionsToSettle(3_000))) {
|
|
5852
|
+
logger.warn("Aborting active Python execution during dispose before retained kernel cleanup");
|
|
5853
|
+
this.abortPython();
|
|
5854
|
+
if (!(await this.#waitForPythonExecutionsToSettle(1_000))) {
|
|
5855
|
+
logger.warn(
|
|
5856
|
+
"Python execution is still active after dispose aborted all active runs; retained kernel ownership will still be detached",
|
|
5857
|
+
);
|
|
5858
|
+
return false;
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
return true;
|
|
5715
5862
|
}
|
|
5716
5863
|
|
|
5717
5864
|
/** Whether a Python execution is currently running */
|
|
5718
5865
|
get isPythonRunning(): boolean {
|
|
5719
|
-
return this.#
|
|
5866
|
+
return this.#pythonAbortControllers.size > 0;
|
|
5720
5867
|
}
|
|
5721
5868
|
|
|
5722
5869
|
/** Whether there are pending Python messages waiting to be flushed */
|
|
@@ -58,6 +58,7 @@ export interface SessionHeader {
|
|
|
58
58
|
version?: number; // v1 sessions don't have this
|
|
59
59
|
id: string;
|
|
60
60
|
title?: string; // Auto-generated title from first message
|
|
61
|
+
titleSource?: "auto" | "user";
|
|
61
62
|
timestamp: string;
|
|
62
63
|
cwd: string;
|
|
63
64
|
parentSession?: string;
|
|
@@ -268,6 +269,7 @@ export type ReadonlySessionManager = Pick<
|
|
|
268
269
|
| "getSessionDir"
|
|
269
270
|
| "getSessionId"
|
|
270
271
|
| "getSessionFile"
|
|
272
|
+
| "getSessionName"
|
|
271
273
|
| "getArtifactsDir"
|
|
272
274
|
| "allocateArtifactPath"
|
|
273
275
|
| "saveArtifact"
|
|
@@ -1270,7 +1272,14 @@ async function collectSessionsFromFiles(files: string[], storage: SessionStorage
|
|
|
1270
1272
|
if (entries.length === 0) return;
|
|
1271
1273
|
|
|
1272
1274
|
// Check first entry for valid session header
|
|
1273
|
-
type SessionHeaderShape = {
|
|
1275
|
+
type SessionHeaderShape = {
|
|
1276
|
+
type: string;
|
|
1277
|
+
id: string;
|
|
1278
|
+
cwd?: string;
|
|
1279
|
+
title?: string;
|
|
1280
|
+
titleSource?: "auto" | "user";
|
|
1281
|
+
timestamp: string;
|
|
1282
|
+
};
|
|
1274
1283
|
const header = entries[0] as SessionHeaderShape;
|
|
1275
1284
|
if (header.type !== "session" || !header.id) return;
|
|
1276
1285
|
|
|
@@ -1378,6 +1387,7 @@ export async function resolveResumableSession(
|
|
|
1378
1387
|
interface SessionManagerStateSnapshot {
|
|
1379
1388
|
sessionId: string;
|
|
1380
1389
|
sessionName: string | undefined;
|
|
1390
|
+
titleSource: "auto" | "user" | undefined;
|
|
1381
1391
|
sessionFile: string | undefined;
|
|
1382
1392
|
flushed: boolean;
|
|
1383
1393
|
needsFullRewriteOnNextPersist: boolean;
|
|
@@ -1387,6 +1397,7 @@ interface SessionManagerStateSnapshot {
|
|
|
1387
1397
|
export class SessionManager {
|
|
1388
1398
|
#sessionId: string = "";
|
|
1389
1399
|
#sessionName: string | undefined;
|
|
1400
|
+
#titleSource: "auto" | "user" | undefined;
|
|
1390
1401
|
#sessionFile: string | undefined;
|
|
1391
1402
|
#flushed: boolean = false;
|
|
1392
1403
|
#needsFullRewriteOnNextPersist: boolean = false;
|
|
@@ -1438,6 +1449,7 @@ export class SessionManager {
|
|
|
1438
1449
|
return {
|
|
1439
1450
|
sessionId: this.#sessionId,
|
|
1440
1451
|
sessionName: this.#sessionName,
|
|
1452
|
+
titleSource: this.#titleSource,
|
|
1441
1453
|
sessionFile: this.#sessionFile,
|
|
1442
1454
|
flushed: this.#flushed,
|
|
1443
1455
|
needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
|
|
@@ -1450,6 +1462,7 @@ export class SessionManager {
|
|
|
1450
1462
|
restoreState(snapshot: SessionManagerStateSnapshot): void {
|
|
1451
1463
|
this.#sessionId = snapshot.sessionId;
|
|
1452
1464
|
this.#sessionName = snapshot.sessionName;
|
|
1465
|
+
this.#titleSource = snapshot.titleSource;
|
|
1453
1466
|
this.#sessionFile = snapshot.sessionFile;
|
|
1454
1467
|
this.#flushed = snapshot.flushed;
|
|
1455
1468
|
this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
|
|
@@ -1489,6 +1502,7 @@ export class SessionManager {
|
|
|
1489
1502
|
const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
1490
1503
|
this.#sessionId = header?.id ?? Snowflake.next();
|
|
1491
1504
|
this.#sessionName = header?.title;
|
|
1505
|
+
this.#titleSource = header?.titleSource;
|
|
1492
1506
|
|
|
1493
1507
|
this.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(this.#fileEntries);
|
|
1494
1508
|
|
|
@@ -1547,11 +1561,13 @@ export class SessionManager {
|
|
|
1547
1561
|
version: CURRENT_SESSION_VERSION,
|
|
1548
1562
|
id: this.#sessionId,
|
|
1549
1563
|
title: oldHeader?.title ?? this.#sessionName,
|
|
1564
|
+
titleSource: oldHeader?.titleSource ?? this.#titleSource,
|
|
1550
1565
|
timestamp,
|
|
1551
1566
|
cwd: this.cwd,
|
|
1552
1567
|
parentSession: oldSessionId,
|
|
1553
1568
|
};
|
|
1554
1569
|
this.#sessionName = newHeader.title;
|
|
1570
|
+
this.#titleSource = newHeader.titleSource;
|
|
1555
1571
|
|
|
1556
1572
|
// Replace the header in fileEntries
|
|
1557
1573
|
const entries = this.#fileEntries.filter((e): e is SessionEntry => e.type !== "session");
|
|
@@ -1666,6 +1682,7 @@ export class SessionManager {
|
|
|
1666
1682
|
this.#persistErrorReported = false;
|
|
1667
1683
|
this.#sessionId = Snowflake.next();
|
|
1668
1684
|
this.#sessionName = undefined;
|
|
1685
|
+
this.#titleSource = undefined;
|
|
1669
1686
|
const timestamp = new Date().toISOString();
|
|
1670
1687
|
const header: SessionHeader = {
|
|
1671
1688
|
type: "session",
|
|
@@ -1953,17 +1970,43 @@ export class SessionManager {
|
|
|
1953
1970
|
return manager.getPath(id);
|
|
1954
1971
|
}
|
|
1955
1972
|
|
|
1973
|
+
/** The source that set the session name: "user" (manual /rename or RPC) or "auto" (generated title). */
|
|
1974
|
+
get titleSource(): "auto" | "user" | undefined {
|
|
1975
|
+
return this.#titleSource;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1956
1978
|
getSessionName(): string | undefined {
|
|
1957
1979
|
return this.#sessionName;
|
|
1958
1980
|
}
|
|
1959
1981
|
|
|
1960
|
-
|
|
1961
|
-
|
|
1982
|
+
/** Strip C0/C1 control characters (includes ESC, so removes ANSI sequences) and collapse whitespace. */
|
|
1983
|
+
static #sanitizeName(name: string): string {
|
|
1984
|
+
return name
|
|
1985
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
1986
|
+
.replace(/ +/g, " ")
|
|
1987
|
+
.trim();
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
/**
|
|
1991
|
+
* Set the session display name.
|
|
1992
|
+
* @param source - "user" for explicit renames (/rename command, RPC); "auto" for generated titles.
|
|
1993
|
+
* Auto-generated titles are silently ignored when the user has already set a name.
|
|
1994
|
+
*/
|
|
1995
|
+
async setSessionName(name: string, source: "auto" | "user" = "auto"): Promise<boolean> {
|
|
1996
|
+
// User-set names take permanent precedence over auto-generated ones.
|
|
1997
|
+
if (this.#titleSource === "user" && source === "auto") return false;
|
|
1998
|
+
|
|
1999
|
+
const sanitized = SessionManager.#sanitizeName(name);
|
|
2000
|
+
if (!sanitized) return false;
|
|
2001
|
+
|
|
2002
|
+
this.#sessionName = sanitized;
|
|
2003
|
+
this.#titleSource = source;
|
|
1962
2004
|
|
|
1963
2005
|
// Update the in-memory header (so first flush includes title)
|
|
1964
2006
|
const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
1965
2007
|
if (header) {
|
|
1966
|
-
header.title =
|
|
2008
|
+
header.title = sanitized;
|
|
2009
|
+
header.titleSource = source;
|
|
1967
2010
|
}
|
|
1968
2011
|
|
|
1969
2012
|
// Update the session file header with the title (if already flushed)
|
|
@@ -1971,6 +2014,7 @@ export class SessionManager {
|
|
|
1971
2014
|
if (this.persist && sessionFile && this.storage.existsSync(sessionFile)) {
|
|
1972
2015
|
await this.#rewriteFile();
|
|
1973
2016
|
}
|
|
2017
|
+
return true;
|
|
1974
2018
|
}
|
|
1975
2019
|
|
|
1976
2020
|
_persist(entry: SessionEntry): void {
|
|
@@ -2630,8 +2674,10 @@ export class SessionManager {
|
|
|
2630
2674
|
manager.#newSessionSync({ parentSession: sourceHeader?.id });
|
|
2631
2675
|
const newHeader = manager.#fileEntries[0] as SessionHeader;
|
|
2632
2676
|
newHeader.title = sourceHeader?.title;
|
|
2677
|
+
newHeader.titleSource = sourceHeader?.titleSource;
|
|
2633
2678
|
manager.#fileEntries = [newHeader, ...historyEntries];
|
|
2634
2679
|
manager.#sessionName = newHeader.title;
|
|
2680
|
+
manager.#titleSource = newHeader.titleSource;
|
|
2635
2681
|
manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
2636
2682
|
manager.#buildIndex();
|
|
2637
2683
|
await manager.#rewriteFile();
|
|
@@ -561,6 +561,23 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
561
561
|
await runtime.ctx.handleMemoryCommand(command.text);
|
|
562
562
|
},
|
|
563
563
|
},
|
|
564
|
+
{
|
|
565
|
+
name: "rename",
|
|
566
|
+
description: "Rename the current session",
|
|
567
|
+
inlineHint: "<title>",
|
|
568
|
+
allowArgs: true,
|
|
569
|
+
handle: async (command, runtime) => {
|
|
570
|
+
const title = command.args.trim();
|
|
571
|
+
if (!title) {
|
|
572
|
+
runtime.ctx.showError("Usage: /rename <title>");
|
|
573
|
+
runtime.ctx.editor.setText("");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
runtime.ctx.editor.setText("");
|
|
577
|
+
await runtime.ctx.handleRenameCommand(title);
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
|
|
564
581
|
{
|
|
565
582
|
name: "move",
|
|
566
583
|
description: "Move session to a different working directory",
|
package/src/task/executor.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
|
-
import type { SearchDb } from "@oh-my-pi/pi-natives";
|
|
9
8
|
import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
9
|
import type { TSchema } from "@sinclair/typebox";
|
|
11
10
|
import Ajv, { type ValidateFunction } from "ajv";
|
|
@@ -149,7 +148,6 @@ export interface ExecutorOptions {
|
|
|
149
148
|
mcpManager?: MCPManager;
|
|
150
149
|
authStorage?: AuthStorage;
|
|
151
150
|
modelRegistry?: ModelRegistry;
|
|
152
|
-
searchDb?: SearchDb;
|
|
153
151
|
settings?: Settings;
|
|
154
152
|
}
|
|
155
153
|
|
|
@@ -435,7 +433,11 @@ function createSubagentSettings(baseSettings: Settings): Settings {
|
|
|
435
433
|
for (const key of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
|
|
436
434
|
snapshot[key] = baseSettings.get(key);
|
|
437
435
|
}
|
|
438
|
-
return Settings.isolated({
|
|
436
|
+
return Settings.isolated({
|
|
437
|
+
...snapshot,
|
|
438
|
+
"async.enabled": false,
|
|
439
|
+
"bash.autoBackground.enabled": false,
|
|
440
|
+
});
|
|
439
441
|
}
|
|
440
442
|
|
|
441
443
|
/**
|
|
@@ -954,7 +956,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
954
956
|
cwd: worktree ?? cwd,
|
|
955
957
|
authStorage,
|
|
956
958
|
modelRegistry,
|
|
957
|
-
searchDb: options.searchDb,
|
|
958
959
|
settings: subagentSettings,
|
|
959
960
|
model,
|
|
960
961
|
thinkingLevel: effectiveThinkingLevel,
|
|
@@ -1057,10 +1058,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1057
1058
|
},
|
|
1058
1059
|
getThinkingLevel: () => session.thinkingLevel,
|
|
1059
1060
|
setThinkingLevel: level => session.setThinkingLevel(level),
|
|
1061
|
+
getSessionName: () => session.sessionManager.getSessionName(),
|
|
1062
|
+
setSessionName: async name => {
|
|
1063
|
+
await session.sessionManager.setSessionName(name, "user");
|
|
1064
|
+
},
|
|
1060
1065
|
},
|
|
1061
1066
|
{
|
|
1062
1067
|
getModel: () => session.model,
|
|
1063
|
-
getSearchDb: () => session.searchDb,
|
|
1064
1068
|
isIdle: () => !session.isStreaming,
|
|
1065
1069
|
abort: () => session.abort(),
|
|
1066
1070
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|