@oh-my-pi/pi-coding-agent 8.4.0 → 8.4.2
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 +15 -0
- package/package.json +6 -6
- package/scripts/format-prompts.ts +65 -23
- package/src/commit/agentic/prompts/session-user.md +0 -1
- package/src/commit/agentic/prompts/split-confirm.md +1 -1
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/prompts/analysis-system.md +23 -26
- package/src/commit/prompts/analysis-user.md +1 -1
- package/src/commit/prompts/changelog-system.md +1 -2
- package/src/commit/prompts/changelog-user.md +1 -2
- package/src/commit/prompts/file-observer-system.md +1 -3
- package/src/commit/prompts/file-observer-user.md +1 -2
- package/src/commit/prompts/reduce-system.md +16 -16
- package/src/commit/prompts/reduce-user.md +1 -1
- package/src/commit/prompts/summary-retry.md +1 -2
- package/src/commit/prompts/summary-system.md +10 -10
- package/src/commit/prompts/summary-user.md +1 -1
- package/src/commit/prompts/types-description.md +1 -1
- package/src/config/keybindings.ts +3 -0
- package/src/config/settings-manager.ts +5 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/plan-protocol.ts +95 -0
- package/src/modes/components/status-line/presets.ts +7 -7
- package/src/modes/components/status-line/segments.ts +16 -0
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +16 -2
- package/src/modes/controllers/command-controller.ts +42 -0
- package/src/modes/controllers/event-controller.ts +13 -0
- package/src/modes/controllers/input-controller.ts +16 -0
- package/src/modes/interactive-mode.ts +219 -1
- package/src/modes/theme/theme.ts +7 -0
- package/src/modes/types.ts +7 -0
- package/src/patch/index.ts +9 -3
- package/src/plan-mode/state.ts +6 -0
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/frontmatter.md +1 -1
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/agents/plan.md +33 -49
- package/src/prompts/agents/reviewer.md +7 -7
- package/src/prompts/agents/task.md +1 -2
- package/src/prompts/compaction/branch-summary-preamble.md +1 -1
- package/src/prompts/compaction/branch-summary.md +3 -1
- package/src/prompts/compaction/compaction-summary.md +3 -1
- package/src/prompts/compaction/compaction-turn-prefix.md +2 -1
- package/src/prompts/compaction/compaction-update-summary.md +3 -1
- package/src/prompts/review-request.md +4 -1
- package/src/prompts/system/custom-system-prompt.md +8 -8
- package/src/prompts/system/file-operations.md +1 -1
- package/src/prompts/system/plan-mode-active.md +113 -0
- package/src/prompts/system/plan-mode-approved.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +14 -0
- package/src/prompts/system/plan-mode-subagent.md +36 -0
- package/src/prompts/system/summarization-system.md +1 -1
- package/src/prompts/system/system-prompt.md +17 -27
- package/src/prompts/system/title-system.md +1 -1
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/system/web-search.md +1 -1
- package/src/prompts/tools/ask.md +1 -3
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/calculator.md +1 -1
- package/src/prompts/tools/enter-plan-mode.md +92 -0
- package/src/prompts/tools/exit-plan-mode.md +38 -0
- package/src/prompts/tools/fetch.md +1 -1
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/gemini-image.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/patch.md +1 -3
- package/src/prompts/tools/python.md +2 -4
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/replace.md +16 -16
- package/src/prompts/tools/ssh.md +1 -4
- package/src/prompts/tools/task.md +1 -3
- package/src/prompts/tools/todo-write.md +13 -16
- package/src/prompts/tools/web-search.md +1 -1
- package/src/prompts/tools/write.md +1 -1
- package/src/sdk.ts +61 -10
- package/src/session/agent-session.ts +267 -0
- package/src/task/executor.ts +1 -0
- package/src/task/index.ts +18 -4
- package/src/tools/enter-plan-mode.ts +76 -0
- package/src/tools/exit-plan-mode.ts +62 -0
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +13 -12
- package/src/tools/index.ts +19 -1
- package/src/tools/plan-mode-guard.ts +46 -0
- package/src/tools/read.ts +8 -4
- package/src/tools/write.ts +3 -2
- package/src/utils/tools-manager.ts +38 -9
- package/src/web/search/providers/perplexity.ts +3 -1
- package/src/web/search/types.ts +3 -1
package/src/sdk.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
loadCustomCommands as loadCustomCommandsInternal,
|
|
51
51
|
} from "./extensibility/custom-commands";
|
|
52
52
|
import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./extensibility/custom-tools/types";
|
|
53
|
+
import { CustomToolAdapter } from "./extensibility/custom-tools/wrapper";
|
|
53
54
|
import {
|
|
54
55
|
discoverAndLoadExtensions,
|
|
55
56
|
type ExtensionContext,
|
|
@@ -69,6 +70,7 @@ import {
|
|
|
69
70
|
AgentProtocolHandler,
|
|
70
71
|
ArtifactProtocolHandler,
|
|
71
72
|
InternalUrlRouter,
|
|
73
|
+
PlanProtocolHandler,
|
|
72
74
|
RuleProtocolHandler,
|
|
73
75
|
SkillProtocolHandler,
|
|
74
76
|
} from "./internal-urls";
|
|
@@ -743,12 +745,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
743
745
|
outputSchema: options.outputSchema,
|
|
744
746
|
requireCompleteTool: options.requireCompleteTool,
|
|
745
747
|
getSessionFile: () => sessionManager.getSessionFile() ?? null,
|
|
748
|
+
getSessionId: () => sessionManager.getSessionId?.() ?? null,
|
|
746
749
|
getSessionSpawns: () => options.spawns ?? "*",
|
|
747
750
|
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
|
748
751
|
getActiveModelString: () => {
|
|
749
752
|
const activeModel = agent?.state.model;
|
|
750
753
|
return activeModel ? formatModelString(activeModel) : undefined;
|
|
751
754
|
},
|
|
755
|
+
getPlanModeState: () => session.getPlanModeState(),
|
|
752
756
|
settings: settingsManager,
|
|
753
757
|
settingsManager,
|
|
754
758
|
authStorage,
|
|
@@ -763,6 +767,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
763
767
|
};
|
|
764
768
|
internalRouter.register(new AgentProtocolHandler({ getArtifactsDir }));
|
|
765
769
|
internalRouter.register(new ArtifactProtocolHandler({ getArtifactsDir }));
|
|
770
|
+
internalRouter.register(
|
|
771
|
+
new PlanProtocolHandler({
|
|
772
|
+
getPlansDirectory: settingsManager.getPlansDirectory.bind(settingsManager),
|
|
773
|
+
cwd,
|
|
774
|
+
}),
|
|
775
|
+
);
|
|
766
776
|
internalRouter.register(
|
|
767
777
|
new SkillProtocolHandler({
|
|
768
778
|
getSkills: () => skills,
|
|
@@ -924,14 +934,33 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
924
934
|
const toolContextStore = new ToolContextStore(getSessionContext);
|
|
925
935
|
|
|
926
936
|
const registeredTools = extensionRunner?.getAllRegisteredTools() ?? [];
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
937
|
+
let wrappedExtensionTools: AgentTool[];
|
|
938
|
+
|
|
939
|
+
if (extensionRunner) {
|
|
940
|
+
// With extension runner: convert CustomTools to ToolDefinitions and wrap all together
|
|
941
|
+
const allCustomTools = [
|
|
942
|
+
...registeredTools,
|
|
943
|
+
...(options.customTools?.map(tool => {
|
|
944
|
+
const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool;
|
|
945
|
+
return { definition, extensionPath: "<sdk>" };
|
|
946
|
+
}) ?? []),
|
|
947
|
+
];
|
|
948
|
+
wrappedExtensionTools = wrapRegisteredTools(allCustomTools, extensionRunner);
|
|
949
|
+
} else {
|
|
950
|
+
// Without extension runner: wrap CustomTools directly with CustomToolAdapter
|
|
951
|
+
// ToolDefinition items require ExtensionContext and cannot be used without a runner
|
|
952
|
+
const customToolContext = (): CustomToolContext => ({
|
|
953
|
+
sessionManager,
|
|
954
|
+
modelRegistry,
|
|
955
|
+
model: agent?.state.model,
|
|
956
|
+
isIdle: () => !session?.isStreaming,
|
|
957
|
+
hasQueuedMessages: () => (session?.queuedMessageCount ?? 0) > 0,
|
|
958
|
+
abort: () => session?.abort(),
|
|
959
|
+
});
|
|
960
|
+
wrappedExtensionTools = (options.customTools ?? [])
|
|
961
|
+
.filter(isCustomTool)
|
|
962
|
+
.map(tool => CustomToolAdapter.wrap(tool, customToolContext) as AgentTool);
|
|
963
|
+
}
|
|
935
964
|
|
|
936
965
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
937
966
|
const toolRegistry = new Map<string, AgentTool>();
|
|
@@ -989,7 +1018,25 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
989
1018
|
return options.systemPrompt(defaultPrompt);
|
|
990
1019
|
};
|
|
991
1020
|
|
|
992
|
-
const
|
|
1021
|
+
const toolNamesFromRegistry = Array.from(toolRegistry.keys());
|
|
1022
|
+
const requestedToolNames = options.toolNames ?? toolNamesFromRegistry;
|
|
1023
|
+
const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
|
|
1024
|
+
const includeExitPlanMode = options.toolNames?.includes("exit_plan_mode") ?? false;
|
|
1025
|
+
const initialToolNames = includeExitPlanMode
|
|
1026
|
+
? normalizedRequested
|
|
1027
|
+
: normalizedRequested.filter(name => name !== "exit_plan_mode");
|
|
1028
|
+
|
|
1029
|
+
// Custom tools are always included regardless of toolNames filter
|
|
1030
|
+
if (options.customTools) {
|
|
1031
|
+
const customToolNames = options.customTools.map(t => (isCustomTool(t) ? t.name : t.name));
|
|
1032
|
+
for (const name of customToolNames) {
|
|
1033
|
+
if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
|
|
1034
|
+
initialToolNames.push(name);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const systemPrompt = await rebuildSystemPrompt(initialToolNames, toolRegistry);
|
|
993
1040
|
time("buildSystemPrompt");
|
|
994
1041
|
|
|
995
1042
|
const promptTemplates = options.promptTemplates ?? (await discoverPromptTemplates(cwd, agentDir));
|
|
@@ -1038,12 +1085,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1038
1085
|
toolContextStore.setUIContext(uiContext, hasUI);
|
|
1039
1086
|
};
|
|
1040
1087
|
|
|
1088
|
+
const initialTools = initialToolNames
|
|
1089
|
+
.map(name => toolRegistry.get(name))
|
|
1090
|
+
.filter((tool): tool is AgentTool => tool !== undefined);
|
|
1091
|
+
|
|
1041
1092
|
agent = new Agent({
|
|
1042
1093
|
initialState: {
|
|
1043
1094
|
systemPrompt,
|
|
1044
1095
|
model,
|
|
1045
1096
|
thinkingLevel,
|
|
1046
|
-
tools:
|
|
1097
|
+
tools: initialTools,
|
|
1047
1098
|
},
|
|
1048
1099
|
convertToLlm: convertToLlmWithBlockImages,
|
|
1049
1100
|
sessionId: sessionManager.getSessionId(),
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
|
+
|
|
17
18
|
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
18
19
|
import type {
|
|
19
20
|
AssistantMessage,
|
|
@@ -26,6 +27,7 @@ import type {
|
|
|
26
27
|
UsageReport,
|
|
27
28
|
} from "@oh-my-pi/pi-ai";
|
|
28
29
|
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
30
|
+
import { resolvePlanUrlToPath } from "@oh-my-pi/pi-coding-agent/internal-urls";
|
|
29
31
|
import { abortableSleep, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
30
32
|
import { YAML } from "bun";
|
|
31
33
|
import type { Rule } from "../capability/rule";
|
|
@@ -62,6 +64,9 @@ import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slas
|
|
|
62
64
|
import { executePython as executePythonCommand, type PythonResult } from "../ipy/executor";
|
|
63
65
|
import { theme } from "../modes/theme/theme";
|
|
64
66
|
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
|
|
67
|
+
import type { PlanModeState } from "../plan-mode/state";
|
|
68
|
+
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
69
|
+
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
65
70
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
66
71
|
import { closeAllConnections } from "../ssh/connection-manager";
|
|
67
72
|
import { unmountAll } from "../ssh/sshfs-mount";
|
|
@@ -188,6 +193,11 @@ export interface SessionStats {
|
|
|
188
193
|
cost: number;
|
|
189
194
|
}
|
|
190
195
|
|
|
196
|
+
/** Result from handoff() */
|
|
197
|
+
export interface HandoffResult {
|
|
198
|
+
document: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
191
201
|
/** Internal marker for hook messages queued through the agent loop */
|
|
192
202
|
// ============================================================================
|
|
193
203
|
// Constants
|
|
@@ -255,6 +265,8 @@ export class AgentSession {
|
|
|
255
265
|
private _followUpMessages: string[] = [];
|
|
256
266
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
257
267
|
private _pendingNextTurnMessages: CustomMessage[] = [];
|
|
268
|
+
private _planModeState: PlanModeState | undefined;
|
|
269
|
+
private _planReferenceSent = false;
|
|
258
270
|
|
|
259
271
|
// Compaction state
|
|
260
272
|
private _compactionAbortController: AbortController | undefined = undefined;
|
|
@@ -263,6 +275,9 @@ export class AgentSession {
|
|
|
263
275
|
// Branch summarization state
|
|
264
276
|
private _branchSummaryAbortController: AbortController | undefined = undefined;
|
|
265
277
|
|
|
278
|
+
// Handoff state
|
|
279
|
+
private _handoffAbortController: AbortController | undefined = undefined;
|
|
280
|
+
|
|
266
281
|
// Retry state
|
|
267
282
|
private _retryAbortController: AbortController | undefined = undefined;
|
|
268
283
|
private _retryAttempt = 0;
|
|
@@ -970,6 +985,25 @@ export class AgentSession {
|
|
|
970
985
|
}
|
|
971
986
|
|
|
972
987
|
/** Prompt templates */
|
|
988
|
+
getPlanModeState(): PlanModeState | undefined {
|
|
989
|
+
return this._planModeState;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
setPlanModeState(state: PlanModeState | undefined): void {
|
|
993
|
+
this._planModeState = state;
|
|
994
|
+
if (state?.enabled) {
|
|
995
|
+
this._planReferenceSent = false;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
markPlanReferenceSent(): void {
|
|
1000
|
+
this._planReferenceSent = true;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
resolveRoleModel(role: string): Model<any> | undefined {
|
|
1004
|
+
return this._resolveRoleModel(role, this._modelRegistry.getAvailable(), this.model);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
973
1007
|
get promptTemplates(): ReadonlyArray<PromptTemplate> {
|
|
974
1008
|
return this._promptTemplates;
|
|
975
1009
|
}
|
|
@@ -983,6 +1017,95 @@ export class AgentSession {
|
|
|
983
1017
|
// Prompting
|
|
984
1018
|
// =========================================================================
|
|
985
1019
|
|
|
1020
|
+
/**
|
|
1021
|
+
* Build a plan mode message.
|
|
1022
|
+
* Returns null if plan mode is not enabled.
|
|
1023
|
+
* @returns The plan mode message, or null if plan mode is not enabled.
|
|
1024
|
+
*/
|
|
1025
|
+
private async _buildPlanReferenceMessage(): Promise<CustomMessage | null> {
|
|
1026
|
+
if (this._planModeState?.enabled) return null;
|
|
1027
|
+
if (this._planReferenceSent) return null;
|
|
1028
|
+
|
|
1029
|
+
const planFilePath = `plan://${this.sessionManager.getSessionId()}/plan.md`;
|
|
1030
|
+
const resolvedPlanPath = resolvePlanUrlToPath(planFilePath, {
|
|
1031
|
+
getPlansDirectory: this.settingsManager.getPlansDirectory.bind(this.settingsManager),
|
|
1032
|
+
cwd: this.sessionManager.getCwd(),
|
|
1033
|
+
});
|
|
1034
|
+
let planContent: string;
|
|
1035
|
+
try {
|
|
1036
|
+
planContent = await fs.promises.readFile(resolvedPlanPath, "utf-8");
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
if (isEnoent(error)) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
throw error;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const content = renderPromptTemplate(planModeReferencePrompt, {
|
|
1045
|
+
planFilePath,
|
|
1046
|
+
planContent,
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
this._planReferenceSent = true;
|
|
1050
|
+
|
|
1051
|
+
return {
|
|
1052
|
+
role: "custom",
|
|
1053
|
+
customType: "plan-mode-reference",
|
|
1054
|
+
content,
|
|
1055
|
+
display: false,
|
|
1056
|
+
timestamp: Date.now(),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
private async _buildPlanModeMessage(): Promise<CustomMessage | null> {
|
|
1061
|
+
const state = this._planModeState;
|
|
1062
|
+
if (!state?.enabled) return null;
|
|
1063
|
+
const sessionPlanUrl = `plan://${this.sessionManager.getSessionId()}/plan.md`;
|
|
1064
|
+
const resolvedPlanPath = state.planFilePath.startsWith("plan://")
|
|
1065
|
+
? resolvePlanUrlToPath(state.planFilePath, {
|
|
1066
|
+
getPlansDirectory: this.settingsManager.getPlansDirectory.bind(this.settingsManager),
|
|
1067
|
+
cwd: this.sessionManager.getCwd(),
|
|
1068
|
+
})
|
|
1069
|
+
: resolveToCwd(state.planFilePath, this.sessionManager.getCwd());
|
|
1070
|
+
const resolvedSessionPlan = resolvePlanUrlToPath(sessionPlanUrl, {
|
|
1071
|
+
getPlansDirectory: this.settingsManager.getPlansDirectory.bind(this.settingsManager),
|
|
1072
|
+
cwd: this.sessionManager.getCwd(),
|
|
1073
|
+
});
|
|
1074
|
+
const displayPlanPath =
|
|
1075
|
+
state.planFilePath.startsWith("plan://") || resolvedPlanPath !== resolvedSessionPlan
|
|
1076
|
+
? state.planFilePath
|
|
1077
|
+
: sessionPlanUrl;
|
|
1078
|
+
|
|
1079
|
+
let planExists = false;
|
|
1080
|
+
try {
|
|
1081
|
+
const stat = await fs.promises.stat(resolvedPlanPath);
|
|
1082
|
+
planExists = stat.isFile();
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
if (!isEnoent(error)) {
|
|
1085
|
+
throw error;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const content = renderPromptTemplate(planModeActivePrompt, {
|
|
1090
|
+
planFilePath: displayPlanPath,
|
|
1091
|
+
planExists,
|
|
1092
|
+
askToolName: "ask",
|
|
1093
|
+
writeToolName: "write",
|
|
1094
|
+
editToolName: "edit",
|
|
1095
|
+
exitToolName: "exit_plan_mode",
|
|
1096
|
+
reentry: state.reentry ?? false,
|
|
1097
|
+
iterative: state.workflow === "iterative",
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
return {
|
|
1101
|
+
role: "custom",
|
|
1102
|
+
customType: "plan-mode-context",
|
|
1103
|
+
content,
|
|
1104
|
+
display: false,
|
|
1105
|
+
timestamp: Date.now(),
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
986
1109
|
/**
|
|
987
1110
|
* Send a prompt to the agent.
|
|
988
1111
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -1069,6 +1192,14 @@ export class AgentSession {
|
|
|
1069
1192
|
|
|
1070
1193
|
// Build messages array (custom messages if any, then user message)
|
|
1071
1194
|
const messages: AgentMessage[] = [];
|
|
1195
|
+
const planReferenceMessage = await this._buildPlanReferenceMessage?.();
|
|
1196
|
+
if (planReferenceMessage) {
|
|
1197
|
+
messages.push(planReferenceMessage);
|
|
1198
|
+
}
|
|
1199
|
+
const planModeMessage = await this._buildPlanModeMessage();
|
|
1200
|
+
if (planModeMessage) {
|
|
1201
|
+
messages.push(planModeMessage);
|
|
1202
|
+
}
|
|
1072
1203
|
|
|
1073
1204
|
// Add user message
|
|
1074
1205
|
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
@@ -1512,6 +1643,7 @@ export class AgentSession {
|
|
|
1512
1643
|
this._followUpMessages = [];
|
|
1513
1644
|
this._pendingNextTurnMessages = [];
|
|
1514
1645
|
this._todoReminderCount = 0;
|
|
1646
|
+
this._planReferenceSent = false;
|
|
1515
1647
|
this._reconnectToAgent();
|
|
1516
1648
|
|
|
1517
1649
|
// Emit session_switch event with reason "new" to hooks
|
|
@@ -1945,6 +2077,141 @@ export class AgentSession {
|
|
|
1945
2077
|
this._branchSummaryAbortController?.abort();
|
|
1946
2078
|
}
|
|
1947
2079
|
|
|
2080
|
+
/**
|
|
2081
|
+
* Cancel in-progress handoff generation.
|
|
2082
|
+
*/
|
|
2083
|
+
abortHandoff(): void {
|
|
2084
|
+
this._handoffAbortController?.abort();
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/**
|
|
2088
|
+
* Check if handoff generation is in progress.
|
|
2089
|
+
*/
|
|
2090
|
+
get isGeneratingHandoff(): boolean {
|
|
2091
|
+
return this._handoffAbortController !== undefined;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
/**
|
|
2095
|
+
* Generate a handoff document by asking the agent, then start a new session with it.
|
|
2096
|
+
*
|
|
2097
|
+
* This prompts the current agent to write a comprehensive handoff document,
|
|
2098
|
+
* waits for completion, then starts a fresh session with the handoff as context.
|
|
2099
|
+
*
|
|
2100
|
+
* @param customInstructions Optional focus for the handoff document
|
|
2101
|
+
* @returns The handoff document text, or undefined if cancelled/failed
|
|
2102
|
+
*/
|
|
2103
|
+
async handoff(customInstructions?: string): Promise<HandoffResult | undefined> {
|
|
2104
|
+
const entries = this.sessionManager.getBranch();
|
|
2105
|
+
const messageCount = entries.filter(e => e.type === "message").length;
|
|
2106
|
+
|
|
2107
|
+
if (messageCount < 2) {
|
|
2108
|
+
throw new Error("Nothing to hand off (no messages yet)");
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
this._handoffAbortController = new AbortController();
|
|
2112
|
+
|
|
2113
|
+
// Build the handoff prompt
|
|
2114
|
+
let handoffPrompt = `Write a comprehensive handoff document that will allow another instance of yourself to seamlessly continue this work. The document should capture everything needed to resume without access to this conversation.
|
|
2115
|
+
|
|
2116
|
+
Use this format:
|
|
2117
|
+
|
|
2118
|
+
## Goal
|
|
2119
|
+
[What the user is trying to accomplish]
|
|
2120
|
+
|
|
2121
|
+
## Constraints & Preferences
|
|
2122
|
+
- [Any constraints, preferences, or requirements mentioned]
|
|
2123
|
+
|
|
2124
|
+
## Progress
|
|
2125
|
+
### Done
|
|
2126
|
+
- [x] [Completed tasks with specifics]
|
|
2127
|
+
|
|
2128
|
+
### In Progress
|
|
2129
|
+
- [ ] [Current work if any]
|
|
2130
|
+
|
|
2131
|
+
### Pending
|
|
2132
|
+
- [ ] [Tasks mentioned but not started]
|
|
2133
|
+
|
|
2134
|
+
## Key Decisions
|
|
2135
|
+
- **[Decision]**: [Rationale]
|
|
2136
|
+
|
|
2137
|
+
## Critical Context
|
|
2138
|
+
- [Code snippets, file paths, error messages, or data essential to continue]
|
|
2139
|
+
- [Repository state if relevant]
|
|
2140
|
+
|
|
2141
|
+
## Next Steps
|
|
2142
|
+
1. [What should happen next]
|
|
2143
|
+
|
|
2144
|
+
Be thorough - include exact file paths, function names, error messages, and technical details. Output ONLY the handoff document, no other text.`;
|
|
2145
|
+
|
|
2146
|
+
if (customInstructions) {
|
|
2147
|
+
handoffPrompt += `\n\nAdditional focus: ${customInstructions}`;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// Create a promise that resolves when the agent completes
|
|
2151
|
+
let handoffText: string | undefined;
|
|
2152
|
+
const completionPromise = new Promise<void>((resolve, reject) => {
|
|
2153
|
+
const unsubscribe = this.subscribe(event => {
|
|
2154
|
+
if (this._handoffAbortController?.signal.aborted) {
|
|
2155
|
+
unsubscribe();
|
|
2156
|
+
reject(new Error("Handoff cancelled"));
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
if (event.type === "agent_end") {
|
|
2161
|
+
unsubscribe();
|
|
2162
|
+
// Extract text from the last assistant message
|
|
2163
|
+
const messages = this.agent.state.messages;
|
|
2164
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2165
|
+
const msg = messages[i];
|
|
2166
|
+
if (msg.role === "assistant") {
|
|
2167
|
+
const content = (msg as AssistantMessage).content;
|
|
2168
|
+
const textParts = content
|
|
2169
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
2170
|
+
.map(c => c.text);
|
|
2171
|
+
if (textParts.length > 0) {
|
|
2172
|
+
handoffText = textParts.join("\n");
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
resolve();
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
try {
|
|
2183
|
+
// Send the prompt and wait for completion
|
|
2184
|
+
await this.prompt(handoffPrompt, { expandPromptTemplates: false });
|
|
2185
|
+
await completionPromise;
|
|
2186
|
+
|
|
2187
|
+
if (!handoffText || this._handoffAbortController.signal.aborted) {
|
|
2188
|
+
return undefined;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// Start a new session
|
|
2192
|
+
await this.sessionManager.flush();
|
|
2193
|
+
this.sessionManager.newSession();
|
|
2194
|
+
this.agent.reset();
|
|
2195
|
+
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
2196
|
+
this._steeringMessages = [];
|
|
2197
|
+
this._followUpMessages = [];
|
|
2198
|
+
this._pendingNextTurnMessages = [];
|
|
2199
|
+
this._todoReminderCount = 0;
|
|
2200
|
+
|
|
2201
|
+
// Inject the handoff document as a custom message
|
|
2202
|
+
const handoffContent = `<handoff-context>\n${handoffText}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
|
|
2203
|
+
this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true);
|
|
2204
|
+
|
|
2205
|
+
// Rebuild agent messages from session
|
|
2206
|
+
const sessionContext = this.sessionManager.buildSessionContext();
|
|
2207
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
2208
|
+
|
|
2209
|
+
return { document: handoffText };
|
|
2210
|
+
} finally {
|
|
2211
|
+
this._handoffAbortController = undefined;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
1948
2215
|
/**
|
|
1949
2216
|
* Check if compaction is needed and run it.
|
|
1950
2217
|
* Called after agent_end and before prompt submission.
|
package/src/task/executor.ts
CHANGED
|
@@ -81,6 +81,7 @@ export interface ExecutorOptions {
|
|
|
81
81
|
modelRegistry?: ModelRegistry;
|
|
82
82
|
settingsManager?: {
|
|
83
83
|
serialize: () => import("@oh-my-pi/pi-coding-agent/config/settings-manager").Settings;
|
|
84
|
+
getPlansDirectory: (cwd?: string) => string;
|
|
84
85
|
getPythonToolMode?: () => "ipy-only" | "bash-only" | "both";
|
|
85
86
|
getPythonKernelMode?: () => "session" | "per-call";
|
|
86
87
|
getPythonSharedGateway?: () => boolean;
|
package/src/task/index.ts
CHANGED
|
@@ -17,6 +17,9 @@ import * as os from "node:os";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
20
|
+
import planModeSubagentPrompt from "@oh-my-pi/pi-coding-agent/prompts/system/plan-mode-subagent.md" with {
|
|
21
|
+
type: "text",
|
|
22
|
+
};
|
|
20
23
|
import { $ } from "bun";
|
|
21
24
|
import { nanoid } from "nanoid";
|
|
22
25
|
import type { ToolSession } from "..";
|
|
@@ -192,14 +195,25 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
192
195
|
};
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
const
|
|
198
|
+
const planModeState = this.session.getPlanModeState?.();
|
|
199
|
+
const planModeTools = ["read", "grep", "find", "ls", "lsp", "fetch", "web_search"];
|
|
200
|
+
const effectiveAgent: typeof agent = planModeState?.enabled
|
|
201
|
+
? {
|
|
202
|
+
...agent,
|
|
203
|
+
systemPrompt: `${planModeSubagentPrompt}\n\n${agent.systemPrompt}`,
|
|
204
|
+
tools: planModeTools,
|
|
205
|
+
spawns: undefined,
|
|
206
|
+
}
|
|
207
|
+
: agent;
|
|
208
|
+
|
|
209
|
+
const effectiveAgentModel = isDefaultModelAlias(effectiveAgent.model) ? undefined : effectiveAgent.model;
|
|
196
210
|
const modelOverride =
|
|
197
211
|
effectiveAgentModel ?? this.session.getActiveModelString?.() ?? this.session.getModelString?.();
|
|
198
|
-
const thinkingLevelOverride =
|
|
212
|
+
const thinkingLevelOverride = effectiveAgent.thinkingLevel;
|
|
199
213
|
|
|
200
214
|
// Output schema priority: agent frontmatter > params > inherited from parent session
|
|
201
|
-
const schemaOverridden = outputSchema !== undefined &&
|
|
202
|
-
const effectiveOutputSchema =
|
|
215
|
+
const schemaOverridden = outputSchema !== undefined && effectiveAgent.output !== undefined;
|
|
216
|
+
const effectiveOutputSchema = effectiveAgent.output ?? outputSchema ?? this.session.outputSchema;
|
|
203
217
|
|
|
204
218
|
// Handle empty or missing tasks
|
|
205
219
|
if (!params.tasks || params.tasks.length === 0) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
|
|
4
|
+
import { resolvePlanUrlToPath } from "@oh-my-pi/pi-coding-agent/internal-urls";
|
|
5
|
+
import enterPlanModeDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/enter-plan-mode.md" with { type: "text" };
|
|
6
|
+
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
|
7
|
+
import { ToolError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
|
|
8
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
|
|
11
|
+
const enterPlanModeSchema = Type.Object({
|
|
12
|
+
workflow: Type.Optional(Type.Union([Type.Literal("parallel"), Type.Literal("iterative")])),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export interface EnterPlanModeDetails {
|
|
16
|
+
planFilePath: string;
|
|
17
|
+
planExists: boolean;
|
|
18
|
+
workflow?: "parallel" | "iterative";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class EnterPlanModeTool implements AgentTool<typeof enterPlanModeSchema, EnterPlanModeDetails> {
|
|
22
|
+
public readonly name = "enter_plan_mode";
|
|
23
|
+
public readonly label = "EnterPlanMode";
|
|
24
|
+
public readonly description: string;
|
|
25
|
+
public readonly parameters = enterPlanModeSchema;
|
|
26
|
+
|
|
27
|
+
private readonly session: ToolSession;
|
|
28
|
+
|
|
29
|
+
constructor(session: ToolSession) {
|
|
30
|
+
this.session = session;
|
|
31
|
+
this.description = renderPromptTemplate(enterPlanModeDescription);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async execute(
|
|
35
|
+
_toolCallId: string,
|
|
36
|
+
params: { workflow?: "parallel" | "iterative" },
|
|
37
|
+
_signal?: AbortSignal,
|
|
38
|
+
_onUpdate?: AgentToolUpdateCallback<EnterPlanModeDetails>,
|
|
39
|
+
_context?: AgentToolContext,
|
|
40
|
+
): Promise<AgentToolResult<EnterPlanModeDetails>> {
|
|
41
|
+
const state = this.session.getPlanModeState?.();
|
|
42
|
+
if (state?.enabled) {
|
|
43
|
+
throw new ToolError("Plan mode is already active.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sessionId = this.session.getSessionId?.();
|
|
47
|
+
if (!sessionId) {
|
|
48
|
+
throw new ToolError("Plan mode requires an active session.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const settingsManager = this.session.settingsManager;
|
|
52
|
+
if (!settingsManager) {
|
|
53
|
+
throw new ToolError("Settings manager unavailable for plan mode.");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const planFilePath = `plan://${sessionId}/plan.md`;
|
|
57
|
+
const resolvedPlanPath = resolvePlanUrlToPath(planFilePath, {
|
|
58
|
+
getPlansDirectory: settingsManager.getPlansDirectory.bind(settingsManager),
|
|
59
|
+
cwd: this.session.cwd,
|
|
60
|
+
});
|
|
61
|
+
let planExists = false;
|
|
62
|
+
try {
|
|
63
|
+
const stat = await fs.stat(resolvedPlanPath);
|
|
64
|
+
planExists = stat.isFile();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!isEnoent(error)) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: "Plan mode requested." }],
|
|
73
|
+
details: { planFilePath, planExists, workflow: params.workflow },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
6
|
+
import exitPlanModeDescription from "../prompts/tools/exit-plan-mode.md" with { type: "text" };
|
|
7
|
+
import type { ToolSession } from ".";
|
|
8
|
+
import { resolvePlanPath } from "./plan-mode-guard";
|
|
9
|
+
import { ToolError } from "./tool-errors";
|
|
10
|
+
|
|
11
|
+
const exitPlanModeSchema = Type.Object({});
|
|
12
|
+
|
|
13
|
+
export interface ExitPlanModeDetails {
|
|
14
|
+
planFilePath: string;
|
|
15
|
+
planExists: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, ExitPlanModeDetails> {
|
|
19
|
+
public readonly name = "exit_plan_mode";
|
|
20
|
+
public readonly label = "ExitPlanMode";
|
|
21
|
+
public readonly description: string;
|
|
22
|
+
public readonly parameters = exitPlanModeSchema;
|
|
23
|
+
|
|
24
|
+
private readonly session: ToolSession;
|
|
25
|
+
|
|
26
|
+
constructor(session: ToolSession) {
|
|
27
|
+
this.session = session;
|
|
28
|
+
this.description = renderPromptTemplate(exitPlanModeDescription);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async execute(
|
|
32
|
+
_toolCallId: string,
|
|
33
|
+
_params: Record<string, never>,
|
|
34
|
+
_signal?: AbortSignal,
|
|
35
|
+
_onUpdate?: AgentToolUpdateCallback<ExitPlanModeDetails>,
|
|
36
|
+
_context?: AgentToolContext,
|
|
37
|
+
): Promise<AgentToolResult<ExitPlanModeDetails>> {
|
|
38
|
+
const state = this.session.getPlanModeState?.();
|
|
39
|
+
if (!state?.enabled) {
|
|
40
|
+
throw new ToolError("Plan mode is not active.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resolvedPlanPath = resolvePlanPath(this.session, state.planFilePath);
|
|
44
|
+
let planExists = false;
|
|
45
|
+
try {
|
|
46
|
+
const stat = await fs.stat(resolvedPlanPath);
|
|
47
|
+
planExists = stat.isFile();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (!isEnoent(error)) {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: "Plan ready for approval." }],
|
|
56
|
+
details: {
|
|
57
|
+
planFilePath: state.planFilePath,
|
|
58
|
+
planExists,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/tools/find.ts
CHANGED
|
@@ -127,7 +127,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
127
127
|
params: Static<typeof findSchema>,
|
|
128
128
|
signal?: AbortSignal,
|
|
129
129
|
_onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
|
|
130
|
-
|
|
130
|
+
context?: AgentToolContext,
|
|
131
131
|
): Promise<AgentToolResult<FindToolDetails>> {
|
|
132
132
|
const { pattern, path: searchDir, limit, hidden, type } = params;
|
|
133
133
|
|
|
@@ -196,7 +196,10 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
// Default: use fd
|
|
199
|
-
const fdPath = await ensureTool("fd",
|
|
199
|
+
const fdPath = await ensureTool("fd", {
|
|
200
|
+
silent: true,
|
|
201
|
+
notify: message => context?.ui?.notify(message, "info"),
|
|
202
|
+
});
|
|
200
203
|
if (!fdPath) {
|
|
201
204
|
throw new ToolError("fd is not available and could not be downloaded");
|
|
202
205
|
}
|