@oh-my-pi/pi-coding-agent 8.4.5 → 8.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +6 -6
- package/src/modes/controllers/event-controller.ts +0 -6
- package/src/modes/interactive-mode.ts +1 -20
- package/src/modes/types.ts +0 -1
- package/src/prompts/system/custom-system-prompt.md +14 -0
- package/src/prompts/system/system-prompt.md +12 -0
- package/src/prompts/tools/task.md +1 -0
- package/src/sdk.ts +4 -0
- package/src/session/session-manager.ts +29 -2
- package/src/system-prompt.ts +26 -1
- package/src/task/executor.ts +99 -13
- package/src/task/index.ts +58 -11
- package/src/task/template.ts +3 -1
- package/src/task/types.ts +5 -0
- package/src/task/worker-protocol.ts +1 -0
- package/src/task/worker.ts +9 -0
- package/src/tools/index.ts +0 -3
- package/src/prompts/tools/enter-plan-mode.md +0 -98
- package/src/tools/enter-plan-mode.ts +0 -81
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [8.5.0] - 2026-01-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added subagent support for preloading skill contents into the system prompt instead of listing available skills
|
|
9
|
+
- Added session init entries to capture system prompt, task, tools, and output schema for subagent session logs
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Reduced Task tool progress update overhead to keep the UI responsive during high-volume streaming output
|
|
13
|
+
- Fixed subagent session logs dropping pre-assistant entries (user/task metadata) before the first assistant response
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
- Removed enter-plan-mode tool
|
|
5
17
|
## [8.4.5] - 2026-01-26
|
|
6
18
|
|
|
7
19
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.5.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
"test": "bun test"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@oh-my-pi/omp-stats": "8.
|
|
87
|
-
"@oh-my-pi/pi-agent-core": "8.
|
|
88
|
-
"@oh-my-pi/pi-ai": "8.
|
|
89
|
-
"@oh-my-pi/pi-tui": "8.
|
|
90
|
-
"@oh-my-pi/pi-utils": "8.
|
|
86
|
+
"@oh-my-pi/omp-stats": "8.5.0",
|
|
87
|
+
"@oh-my-pi/pi-agent-core": "8.5.0",
|
|
88
|
+
"@oh-my-pi/pi-ai": "8.5.0",
|
|
89
|
+
"@oh-my-pi/pi-tui": "8.5.0",
|
|
90
|
+
"@oh-my-pi/pi-utils": "8.5.0",
|
|
91
91
|
"@openai/agents": "^0.4.3",
|
|
92
92
|
"@sinclair/typebox": "^0.34.46",
|
|
93
93
|
"ajv": "^8.17.1",
|
|
@@ -247,12 +247,6 @@ export class EventController {
|
|
|
247
247
|
this.ctx.setTodos(details.todos);
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
|
-
if (event.toolName === "enter_plan_mode" && !event.isError) {
|
|
251
|
-
const details = event.result.details as import("../../tools").EnterPlanModeDetails | undefined;
|
|
252
|
-
if (details) {
|
|
253
|
-
await this.ctx.handleEnterPlanModeTool(details);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
250
|
if (event.toolName === "exit_plan_mode" && !event.isError) {
|
|
257
251
|
const details = event.result.details as ExitPlanModeDetails | undefined;
|
|
258
252
|
if (details) {
|
|
@@ -29,7 +29,7 @@ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
|
29
29
|
import { HistoryStorage } from "../session/history-storage";
|
|
30
30
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
31
31
|
import { getRecentSessions } from "../session/session-manager";
|
|
32
|
-
import type {
|
|
32
|
+
import type { ExitPlanModeDetails } from "../tools";
|
|
33
33
|
import { setTerminalTitle } from "../utils/title-generator";
|
|
34
34
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
35
35
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
@@ -631,25 +631,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
631
631
|
await this.enterPlanMode();
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
-
async handleEnterPlanModeTool(details: EnterPlanModeDetails): Promise<void> {
|
|
635
|
-
if (this.planModeEnabled) {
|
|
636
|
-
this.showWarning("Plan mode is already active.");
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const confirmed = await this.showHookConfirm(
|
|
641
|
-
"Enter plan mode?",
|
|
642
|
-
"This enables read-only planning and creates a plan file for approval.",
|
|
643
|
-
);
|
|
644
|
-
if (!confirmed) {
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const planFilePath = details.planFilePath || this.getPlanFilePath();
|
|
649
|
-
this.planModePlanFilePath = planFilePath;
|
|
650
|
-
await this.enterPlanMode({ planFilePath, workflow: details.workflow });
|
|
651
|
-
}
|
|
652
|
-
|
|
653
634
|
async handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void> {
|
|
654
635
|
if (!this.planModeEnabled) {
|
|
655
636
|
this.showWarning("Plan mode is not active.");
|
package/src/modes/types.ts
CHANGED
|
@@ -178,7 +178,6 @@ export interface InteractiveModeContext {
|
|
|
178
178
|
openExternalEditor(): void;
|
|
179
179
|
registerExtensionShortcuts(): void;
|
|
180
180
|
handlePlanModeCommand(): Promise<void>;
|
|
181
|
-
handleEnterPlanModeTool(details: import("../tools").EnterPlanModeDetails): Promise<void>;
|
|
182
181
|
handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
|
|
183
182
|
|
|
184
183
|
// Hook UI methods
|
|
@@ -43,6 +43,20 @@ Use the read tool to load a skill's file when the task matches its description.
|
|
|
43
43
|
{{/list}}
|
|
44
44
|
</available_skills>
|
|
45
45
|
{{/if}}
|
|
46
|
+
{{#if preloadedSkills.length}}
|
|
47
|
+
The following skills are preloaded in full. Apply their instructions directly.
|
|
48
|
+
|
|
49
|
+
<preloaded_skills>
|
|
50
|
+
{{#list preloadedSkills join="\n"}}
|
|
51
|
+
<skill name="{{name}}">
|
|
52
|
+
<location>skill://{{escapeXml name}}</location>
|
|
53
|
+
<content>
|
|
54
|
+
{{content}}
|
|
55
|
+
</content>
|
|
56
|
+
</skill>
|
|
57
|
+
{{/list}}
|
|
58
|
+
</preloaded_skills>
|
|
59
|
+
{{/if}}
|
|
46
60
|
{{#if rules.length}}
|
|
47
61
|
The following rules define project-specific guidelines and constraints:
|
|
48
62
|
|
|
@@ -264,6 +264,18 @@ If a skill covers what you're producing, read it before proceeding.
|
|
|
264
264
|
{{/list}}
|
|
265
265
|
</skills>
|
|
266
266
|
{{/if}}
|
|
267
|
+
{{#if preloadedSkills.length}}
|
|
268
|
+
<preloaded_skills>
|
|
269
|
+
The following skills are preloaded in full. Apply their instructions directly.
|
|
270
|
+
|
|
271
|
+
{{#list preloadedSkills join="\n"}}
|
|
272
|
+
<skill name="{{name}}">
|
|
273
|
+
<location>skill://{{escapeXml name}}</location>
|
|
274
|
+
{{content}}
|
|
275
|
+
</skill>
|
|
276
|
+
{{/list}}
|
|
277
|
+
</preloaded_skills>
|
|
278
|
+
{{/if}}
|
|
267
279
|
{{#if rules.length}}
|
|
268
280
|
<rules>
|
|
269
281
|
Rules are local constraints.
|
|
@@ -38,6 +38,7 @@ Agents with `output="structured"` have a fixed schema enforced via frontmatter;
|
|
|
38
38
|
- `id`: Short CamelCase identifier (max 32 chars, e.g., "SessionStore", "LspRefactor")
|
|
39
39
|
- `description`: Short human-readable description of what the task does
|
|
40
40
|
- `args`: Object with keys matching `\{{placeholders}}` in context (always include this, even if empty)
|
|
41
|
+
- `skills`: (optional) Array of skill names to preload into this task's system prompt. When set, the skills index section is omitted and the full SKILL.md contents are embedded.
|
|
41
42
|
- `output`: (optional) JTD schema for structured subagent output (used by the complete tool)
|
|
42
43
|
</parameters>
|
|
43
44
|
|
package/src/sdk.ts
CHANGED
|
@@ -156,6 +156,8 @@ export interface CreateAgentSessionOptions {
|
|
|
156
156
|
|
|
157
157
|
/** Skills. Default: discovered from multiple locations */
|
|
158
158
|
skills?: Skill[];
|
|
159
|
+
/** Skills to inline into the system prompt instead of listing available skills. */
|
|
160
|
+
preloadedSkills?: Skill[];
|
|
159
161
|
/** Context files (AGENTS.md content). Default: discovered walking up from cwd */
|
|
160
162
|
contextFiles?: Array<{ path: string; content: string }>;
|
|
161
163
|
/** Prompt templates. Default: discovered from cwd/.omp/prompts/ + agentDir/prompts/ */
|
|
@@ -993,6 +995,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
993
995
|
const defaultPrompt = await buildSystemPromptInternal({
|
|
994
996
|
cwd,
|
|
995
997
|
skills,
|
|
998
|
+
preloadedSkills: options.preloadedSkills,
|
|
996
999
|
contextFiles,
|
|
997
1000
|
tools,
|
|
998
1001
|
toolNames,
|
|
@@ -1007,6 +1010,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1007
1010
|
return await buildSystemPromptInternal({
|
|
1008
1011
|
cwd,
|
|
1009
1012
|
skills,
|
|
1013
|
+
preloadedSkills: options.preloadedSkills,
|
|
1010
1014
|
contextFiles,
|
|
1011
1015
|
tools,
|
|
1012
1016
|
toolNames,
|
|
@@ -110,6 +110,19 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
|
|
|
110
110
|
injectedRules: string[];
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/** Session init entry - captures initial context for subagent sessions (debugging/replay). */
|
|
114
|
+
export interface SessionInitEntry extends SessionEntryBase {
|
|
115
|
+
type: "session_init";
|
|
116
|
+
/** Full system prompt sent to the model */
|
|
117
|
+
systemPrompt: string;
|
|
118
|
+
/** Initial task/user message */
|
|
119
|
+
task: string;
|
|
120
|
+
/** Tools available to the agent */
|
|
121
|
+
tools: string[];
|
|
122
|
+
/** Output schema if structured output was requested */
|
|
123
|
+
outputSchema?: unknown;
|
|
124
|
+
}
|
|
125
|
+
|
|
113
126
|
/**
|
|
114
127
|
* Custom message entry for extensions to inject messages into LLM context.
|
|
115
128
|
* Use customType to identify your extension's entries.
|
|
@@ -140,7 +153,8 @@ export type SessionEntry =
|
|
|
140
153
|
| CustomEntry
|
|
141
154
|
| CustomMessageEntry
|
|
142
155
|
| LabelEntry
|
|
143
|
-
| TtsrInjectionEntry
|
|
156
|
+
| TtsrInjectionEntry
|
|
157
|
+
| SessionInitEntry;
|
|
144
158
|
|
|
145
159
|
/** Raw file entry (includes header) */
|
|
146
160
|
export type FileEntry = SessionHeader | SessionEntry;
|
|
@@ -1263,7 +1277,7 @@ export class SessionManager {
|
|
|
1263
1277
|
if (this.persistError) throw this.persistError;
|
|
1264
1278
|
|
|
1265
1279
|
const hasAssistant = this.fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
|
|
1266
|
-
if (!hasAssistant) return;
|
|
1280
|
+
if (!hasAssistant && !this.flushed) return;
|
|
1267
1281
|
|
|
1268
1282
|
if (!this.flushed) {
|
|
1269
1283
|
this.flushed = true;
|
|
@@ -1368,6 +1382,19 @@ export class SessionManager {
|
|
|
1368
1382
|
return entry.id;
|
|
1369
1383
|
}
|
|
1370
1384
|
|
|
1385
|
+
/** Append session init metadata (for subagent debugging/replay). Returns entry id. */
|
|
1386
|
+
appendSessionInit(init: { systemPrompt: string; task: string; tools: string[]; outputSchema?: unknown }): string {
|
|
1387
|
+
const entry: SessionInitEntry = {
|
|
1388
|
+
type: "session_init",
|
|
1389
|
+
id: generateId(this.byId),
|
|
1390
|
+
parentId: this.leafId,
|
|
1391
|
+
timestamp: new Date().toISOString(),
|
|
1392
|
+
...init,
|
|
1393
|
+
};
|
|
1394
|
+
this._appendEntry(entry);
|
|
1395
|
+
return entry.id;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1371
1398
|
/** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
|
|
1372
1399
|
appendCompaction<T = unknown>(
|
|
1373
1400
|
summary: string,
|
package/src/system-prompt.ts
CHANGED
|
@@ -23,6 +23,24 @@ interface GitContext {
|
|
|
23
23
|
commits: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
type PreloadedSkill = { name: string; content: string };
|
|
27
|
+
|
|
28
|
+
async function loadPreloadedSkillContents(preloadedSkills: Skill[]): Promise<PreloadedSkill[]> {
|
|
29
|
+
const contents = await Promise.all(
|
|
30
|
+
preloadedSkills.map(async skill => {
|
|
31
|
+
try {
|
|
32
|
+
const content = await Bun.file(skill.filePath).text();
|
|
33
|
+
return { name: skill.name, content };
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
36
|
+
throw new Error(`Failed to load skill "${skill.name}" from ${skill.filePath}: ${message}`);
|
|
37
|
+
}
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return contents;
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
/**
|
|
27
45
|
* Load git context for the system prompt.
|
|
28
46
|
* Returns structured git data or null if not in a git repo.
|
|
@@ -643,6 +661,8 @@ export interface BuildSystemPromptOptions {
|
|
|
643
661
|
contextFiles?: Array<{ path: string; content: string; depth?: number }>;
|
|
644
662
|
/** Pre-loaded skills (skips discovery if provided). */
|
|
645
663
|
skills?: Skill[];
|
|
664
|
+
/** Skills to inline into the system prompt instead of listing available skills. */
|
|
665
|
+
preloadedSkills?: Skill[];
|
|
646
666
|
/** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
|
|
647
667
|
rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
|
|
648
668
|
}
|
|
@@ -662,6 +682,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
662
682
|
cwd,
|
|
663
683
|
contextFiles: providedContextFiles,
|
|
664
684
|
skills: providedSkills,
|
|
685
|
+
preloadedSkills: providedPreloadedSkills,
|
|
665
686
|
rules,
|
|
666
687
|
} = options;
|
|
667
688
|
const resolvedCwd = cwd ?? process.cwd();
|
|
@@ -707,13 +728,15 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
707
728
|
const skills =
|
|
708
729
|
providedSkills ??
|
|
709
730
|
(skillsSettings?.enabled !== false ? (await loadSkills({ ...skillsSettings, cwd: resolvedCwd })).skills : []);
|
|
731
|
+
const preloadedSkills = providedPreloadedSkills;
|
|
732
|
+
const preloadedSkillContents = preloadedSkills ? await loadPreloadedSkillContents(preloadedSkills) : [];
|
|
710
733
|
|
|
711
734
|
// Get git context
|
|
712
735
|
const git = await loadGitContext(resolvedCwd);
|
|
713
736
|
|
|
714
737
|
// Filter skills to only include those with read tool
|
|
715
738
|
const hasRead = tools?.has("read");
|
|
716
|
-
const filteredSkills = hasRead ? skills : [];
|
|
739
|
+
const filteredSkills = preloadedSkills === undefined && hasRead ? skills : [];
|
|
717
740
|
|
|
718
741
|
if (resolvedCustomPrompt) {
|
|
719
742
|
return renderPromptTemplate(customSystemPromptTemplate, {
|
|
@@ -724,6 +747,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
724
747
|
agentsMdSearch,
|
|
725
748
|
git,
|
|
726
749
|
skills: filteredSkills,
|
|
750
|
+
preloadedSkills: preloadedSkillContents,
|
|
727
751
|
rules: rules ?? [],
|
|
728
752
|
dateTime,
|
|
729
753
|
cwd: resolvedCwd,
|
|
@@ -738,6 +762,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
738
762
|
agentsMdSearch,
|
|
739
763
|
git,
|
|
740
764
|
skills: filteredSkills,
|
|
765
|
+
preloadedSkills: preloadedSkillContents,
|
|
741
766
|
rules: rules ?? [],
|
|
742
767
|
dateTime,
|
|
743
768
|
cwd: resolvedCwd,
|
package/src/task/executor.ts
CHANGED
|
@@ -75,6 +75,7 @@ export interface ExecutorOptions {
|
|
|
75
75
|
eventBus?: EventBus;
|
|
76
76
|
contextFiles?: ContextFileEntry[];
|
|
77
77
|
skills?: Skill[];
|
|
78
|
+
preloadedSkills?: Skill[];
|
|
78
79
|
promptTemplates?: PromptTemplate[];
|
|
79
80
|
mcpManager?: MCPManager;
|
|
80
81
|
authStorage?: AuthStorage;
|
|
@@ -361,6 +362,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
361
362
|
|
|
362
363
|
const outputChunks: string[] = [];
|
|
363
364
|
const finalOutputChunks: string[] = [];
|
|
365
|
+
const RECENT_OUTPUT_TAIL_BYTES = 8 * 1024;
|
|
366
|
+
let recentOutputTail = "";
|
|
364
367
|
let stderr = "";
|
|
365
368
|
let resolved = false;
|
|
366
369
|
type AbortReason = "signal" | "terminate";
|
|
@@ -513,7 +516,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
513
516
|
signal.addEventListener("abort", onAbort, { once: true, signal: listenerSignal });
|
|
514
517
|
}
|
|
515
518
|
|
|
516
|
-
const
|
|
519
|
+
const PROGRESS_COALESCE_MS = 150;
|
|
520
|
+
let lastProgressEmitMs = 0;
|
|
521
|
+
let progressTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
522
|
+
|
|
523
|
+
const emitProgressNow = () => {
|
|
517
524
|
progress.durationMs = Date.now() - startTime;
|
|
518
525
|
onProgress?.({ ...progress });
|
|
519
526
|
if (options.eventBus) {
|
|
@@ -525,6 +532,33 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
525
532
|
progress: { ...progress },
|
|
526
533
|
});
|
|
527
534
|
}
|
|
535
|
+
lastProgressEmitMs = Date.now();
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const scheduleProgress = (flush = false) => {
|
|
539
|
+
if (flush) {
|
|
540
|
+
if (progressTimeoutId) {
|
|
541
|
+
clearTimeout(progressTimeoutId);
|
|
542
|
+
progressTimeoutId = null;
|
|
543
|
+
}
|
|
544
|
+
emitProgressNow();
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const now = Date.now();
|
|
548
|
+
const elapsed = now - lastProgressEmitMs;
|
|
549
|
+
if (lastProgressEmitMs === 0 || elapsed >= PROGRESS_COALESCE_MS) {
|
|
550
|
+
if (progressTimeoutId) {
|
|
551
|
+
clearTimeout(progressTimeoutId);
|
|
552
|
+
progressTimeoutId = null;
|
|
553
|
+
}
|
|
554
|
+
emitProgressNow();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (progressTimeoutId) return;
|
|
558
|
+
progressTimeoutId = setTimeout(() => {
|
|
559
|
+
progressTimeoutId = null;
|
|
560
|
+
emitProgressNow();
|
|
561
|
+
}, PROGRESS_COALESCE_MS - elapsed);
|
|
528
562
|
};
|
|
529
563
|
|
|
530
564
|
const getMessageContent = (message: unknown): unknown => {
|
|
@@ -541,6 +575,40 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
541
575
|
return undefined;
|
|
542
576
|
};
|
|
543
577
|
|
|
578
|
+
const updateRecentOutputLines = () => {
|
|
579
|
+
const lines = recentOutputTail.split("\n").filter(line => line.trim());
|
|
580
|
+
progress.recentOutput = lines.slice(-8).reverse();
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const appendRecentOutputTail = (text: string) => {
|
|
584
|
+
if (!text) return;
|
|
585
|
+
recentOutputTail += text;
|
|
586
|
+
if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
|
|
587
|
+
recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
|
|
588
|
+
}
|
|
589
|
+
updateRecentOutputLines();
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const replaceRecentOutputFromContent = (content: unknown[]) => {
|
|
593
|
+
recentOutputTail = "";
|
|
594
|
+
for (const block of content) {
|
|
595
|
+
if (!block || typeof block !== "object") continue;
|
|
596
|
+
const record = block as { type?: unknown; text?: unknown };
|
|
597
|
+
if (record.type !== "text" || typeof record.text !== "string") continue;
|
|
598
|
+
if (!record.text) continue;
|
|
599
|
+
recentOutputTail += record.text;
|
|
600
|
+
if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
|
|
601
|
+
recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
updateRecentOutputLines();
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const resetRecentOutput = () => {
|
|
608
|
+
recentOutputTail = "";
|
|
609
|
+
progress.recentOutput = [];
|
|
610
|
+
};
|
|
611
|
+
|
|
544
612
|
const processEvent = (event: AgentEvent) => {
|
|
545
613
|
if (resolved) return;
|
|
546
614
|
|
|
@@ -555,8 +623,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
555
623
|
}
|
|
556
624
|
|
|
557
625
|
const now = Date.now();
|
|
626
|
+
let flushProgress = false;
|
|
558
627
|
|
|
559
628
|
switch (event.type) {
|
|
629
|
+
case "message_start":
|
|
630
|
+
if (event.message?.role === "assistant") {
|
|
631
|
+
resetRecentOutput();
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
|
|
560
635
|
case "tool_execution_start":
|
|
561
636
|
progress.toolCount++;
|
|
562
637
|
progress.currentTool = event.toolName;
|
|
@@ -616,23 +691,28 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
616
691
|
schedulePendingTermination();
|
|
617
692
|
}
|
|
618
693
|
}
|
|
694
|
+
flushProgress = true;
|
|
619
695
|
break;
|
|
620
696
|
}
|
|
621
697
|
|
|
622
698
|
case "message_update": {
|
|
623
|
-
|
|
699
|
+
if (event.message?.role !== "assistant") break;
|
|
700
|
+
const assistantEvent = (
|
|
701
|
+
event as AgentEvent & {
|
|
702
|
+
assistantMessageEvent?: { type?: string; delta?: string };
|
|
703
|
+
}
|
|
704
|
+
).assistantMessageEvent;
|
|
705
|
+
if (assistantEvent?.type === "text_delta" && typeof assistantEvent.delta === "string") {
|
|
706
|
+
appendRecentOutputTail(assistantEvent.delta);
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
if (assistantEvent && assistantEvent.type !== "text_delta") {
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
624
712
|
const updateContent =
|
|
625
713
|
getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
|
|
626
714
|
if (updateContent && Array.isArray(updateContent)) {
|
|
627
|
-
|
|
628
|
-
for (const block of updateContent) {
|
|
629
|
-
if (block.type === "text" && block.text) {
|
|
630
|
-
const lines = block.text.split("\n").filter((l: string) => l.trim());
|
|
631
|
-
allText.push(...lines);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
// Show last 8 lines from current state (not accumulated)
|
|
635
|
-
progress.recentOutput = allText.slice(-8).reverse();
|
|
715
|
+
replaceRecentOutputFromContent(updateContent);
|
|
636
716
|
}
|
|
637
717
|
break;
|
|
638
718
|
}
|
|
@@ -698,10 +778,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
698
778
|
}
|
|
699
779
|
}
|
|
700
780
|
}
|
|
781
|
+
flushProgress = true;
|
|
701
782
|
break;
|
|
702
783
|
}
|
|
703
784
|
|
|
704
|
-
|
|
785
|
+
scheduleProgress(flushProgress);
|
|
705
786
|
};
|
|
706
787
|
|
|
707
788
|
const startMessage: SubagentWorkerRequest = {
|
|
@@ -724,6 +805,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
724
805
|
pythonPreludeDocs: pythonPreludeDocsPayload,
|
|
725
806
|
contextFiles: options.contextFiles,
|
|
726
807
|
skills: options.skills,
|
|
808
|
+
preloadedSkills: options.preloadedSkills,
|
|
727
809
|
promptTemplates: options.promptTemplates,
|
|
728
810
|
mcpTools: options.mcpManager ? extractMCPToolMetadata(options.mcpManager) : undefined,
|
|
729
811
|
pythonToolProxy: pythonProxyEnabled,
|
|
@@ -972,6 +1054,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
972
1054
|
clearTimeout(terminationTimeoutId);
|
|
973
1055
|
terminationTimeoutId = null;
|
|
974
1056
|
}
|
|
1057
|
+
if (progressTimeoutId) {
|
|
1058
|
+
clearTimeout(progressTimeoutId);
|
|
1059
|
+
progressTimeoutId = null;
|
|
1060
|
+
}
|
|
975
1061
|
cancelPendingTermination();
|
|
976
1062
|
if (!terminated) {
|
|
977
1063
|
terminated = true;
|
|
@@ -1066,7 +1152,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1066
1152
|
// Update final progress
|
|
1067
1153
|
const wasAborted = abortedViaComplete || (!hasComplete && (done.aborted || signal?.aborted || false));
|
|
1068
1154
|
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1069
|
-
|
|
1155
|
+
scheduleProgress(true);
|
|
1070
1156
|
|
|
1071
1157
|
return {
|
|
1072
1158
|
index,
|
package/src/task/index.ts
CHANGED
|
@@ -391,12 +391,57 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
391
391
|
// Build full prompts with context prepended
|
|
392
392
|
const tasksWithContext = tasksWithUniqueIds.map(t => renderTemplate(context, t));
|
|
393
393
|
const contextFiles = this.session.contextFiles;
|
|
394
|
-
const
|
|
394
|
+
const availableSkills = this.session.skills;
|
|
395
|
+
const availableSkillList = availableSkills ?? [];
|
|
395
396
|
const promptTemplates = this.session.promptTemplates;
|
|
397
|
+
const skillLookup = new Map(availableSkillList.map(skill => [skill.name, skill]));
|
|
398
|
+
const missingSkillsByTask: Array<{ id: string; missing: string[] }> = [];
|
|
399
|
+
const tasksWithSkills = tasksWithContext.map(task => {
|
|
400
|
+
if (task.skills === undefined) {
|
|
401
|
+
return { ...task, resolvedSkills: availableSkills, preloadedSkills: undefined };
|
|
402
|
+
}
|
|
403
|
+
const requested = task.skills;
|
|
404
|
+
const resolved = [] as typeof availableSkillList;
|
|
405
|
+
const missing: string[] = [];
|
|
406
|
+
const seen = new Set<string>();
|
|
407
|
+
for (const name of requested) {
|
|
408
|
+
const trimmed = name.trim();
|
|
409
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
410
|
+
seen.add(trimmed);
|
|
411
|
+
const skill = skillLookup.get(trimmed);
|
|
412
|
+
if (skill) {
|
|
413
|
+
resolved.push(skill);
|
|
414
|
+
} else {
|
|
415
|
+
missing.push(trimmed);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (missing.length > 0) {
|
|
419
|
+
missingSkillsByTask.push({ id: task.id, missing });
|
|
420
|
+
}
|
|
421
|
+
return { ...task, resolvedSkills: resolved, preloadedSkills: resolved };
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
if (missingSkillsByTask.length > 0) {
|
|
425
|
+
const available = availableSkillList.map(skill => skill.name).join(", ") || "none";
|
|
426
|
+
const details = missingSkillsByTask.map(entry => `${entry.id}: ${entry.missing.join(", ")}`).join("; ");
|
|
427
|
+
return {
|
|
428
|
+
content: [
|
|
429
|
+
{
|
|
430
|
+
type: "text",
|
|
431
|
+
text: `Unknown skills requested: ${details}. Available skills: ${available}`,
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
details: {
|
|
435
|
+
projectAgentsDir,
|
|
436
|
+
results: [],
|
|
437
|
+
totalDurationMs: Date.now() - startTime,
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
396
441
|
|
|
397
442
|
// Initialize progress for all tasks
|
|
398
|
-
for (let i = 0; i <
|
|
399
|
-
const t =
|
|
443
|
+
for (let i = 0; i < tasksWithSkills.length; i++) {
|
|
444
|
+
const t = tasksWithSkills[i];
|
|
400
445
|
progressMap.set(i, {
|
|
401
446
|
index: i,
|
|
402
447
|
id: t.id,
|
|
@@ -416,7 +461,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
416
461
|
}
|
|
417
462
|
emitProgress();
|
|
418
463
|
|
|
419
|
-
const runTask = async (task: (typeof
|
|
464
|
+
const runTask = async (task: (typeof tasksWithSkills)[number], index: number) => {
|
|
420
465
|
if (!isIsolated) {
|
|
421
466
|
return runSubprocess({
|
|
422
467
|
cwd: this.session.cwd,
|
|
@@ -438,7 +483,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
438
483
|
onProgress: progress => {
|
|
439
484
|
progressMap.set(index, {
|
|
440
485
|
...structuredClone(progress),
|
|
441
|
-
args:
|
|
486
|
+
args: tasksWithSkills[index]?.args,
|
|
442
487
|
});
|
|
443
488
|
emitProgress();
|
|
444
489
|
},
|
|
@@ -447,7 +492,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
447
492
|
settingsManager: this.session.settingsManager,
|
|
448
493
|
mcpManager: this.session.mcpManager,
|
|
449
494
|
contextFiles,
|
|
450
|
-
skills,
|
|
495
|
+
skills: task.resolvedSkills,
|
|
496
|
+
preloadedSkills: task.preloadedSkills,
|
|
451
497
|
promptTemplates,
|
|
452
498
|
});
|
|
453
499
|
}
|
|
@@ -481,7 +527,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
481
527
|
onProgress: progress => {
|
|
482
528
|
progressMap.set(index, {
|
|
483
529
|
...structuredClone(progress),
|
|
484
|
-
args:
|
|
530
|
+
args: tasksWithSkills[index]?.args,
|
|
485
531
|
});
|
|
486
532
|
emitProgress();
|
|
487
533
|
},
|
|
@@ -490,7 +536,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
490
536
|
settingsManager: this.session.settingsManager,
|
|
491
537
|
mcpManager: this.session.mcpManager,
|
|
492
538
|
contextFiles,
|
|
493
|
-
skills,
|
|
539
|
+
skills: task.resolvedSkills,
|
|
540
|
+
preloadedSkills: task.preloadedSkills,
|
|
494
541
|
promptTemplates,
|
|
495
542
|
});
|
|
496
543
|
const patch = await captureDeltaPatch(worktreeDir, baseline);
|
|
@@ -527,7 +574,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
527
574
|
|
|
528
575
|
// Execute in parallel with concurrency limit
|
|
529
576
|
const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
|
|
530
|
-
|
|
577
|
+
tasksWithSkills,
|
|
531
578
|
MAX_CONCURRENCY,
|
|
532
579
|
runTask,
|
|
533
580
|
signal,
|
|
@@ -538,10 +585,10 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
538
585
|
if (result !== undefined) {
|
|
539
586
|
return {
|
|
540
587
|
...result,
|
|
541
|
-
args:
|
|
588
|
+
args: tasksWithSkills[index]?.args,
|
|
542
589
|
};
|
|
543
590
|
}
|
|
544
|
-
const task =
|
|
591
|
+
const task = tasksWithSkills[index];
|
|
545
592
|
return {
|
|
546
593
|
index,
|
|
547
594
|
id: task.id,
|
package/src/task/template.ts
CHANGED
|
@@ -5,10 +5,11 @@ type RenderResult = {
|
|
|
5
5
|
args: Record<string, string>;
|
|
6
6
|
id: string;
|
|
7
7
|
description: string;
|
|
8
|
+
skills?: string[];
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
export function renderTemplate(template: string, task: TaskItem): RenderResult {
|
|
11
|
-
const { id, description, args } = task;
|
|
12
|
+
const { id, description, args, skills } = task;
|
|
12
13
|
|
|
13
14
|
let usedPlaceholder = false;
|
|
14
15
|
const unknownArguments: string[] = [];
|
|
@@ -43,5 +44,6 @@ export function renderTemplate(template: string, task: TaskItem): RenderResult {
|
|
|
43
44
|
args: { id, description, ...args },
|
|
44
45
|
id,
|
|
45
46
|
description,
|
|
47
|
+
skills,
|
|
46
48
|
};
|
|
47
49
|
}
|
package/src/task/types.ts
CHANGED
|
@@ -49,6 +49,11 @@ export const taskItemSchema = Type.Object({
|
|
|
49
49
|
description: "Arguments to fill {{placeholders}} in context",
|
|
50
50
|
}),
|
|
51
51
|
),
|
|
52
|
+
skills: Type.Optional(
|
|
53
|
+
Type.Array(Type.String(), {
|
|
54
|
+
description: "Skill names to preload into the subagent system prompt",
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
52
57
|
});
|
|
53
58
|
|
|
54
59
|
export type TaskItem = Static<typeof taskItemSchema>;
|
|
@@ -107,6 +107,7 @@ export interface SubagentWorkerStartPayload {
|
|
|
107
107
|
pythonPreludeDocs?: PreludeHelper[];
|
|
108
108
|
contextFiles?: ContextFileEntry[];
|
|
109
109
|
skills?: Skill[];
|
|
110
|
+
preloadedSkills?: Skill[];
|
|
110
111
|
promptTemplates?: PromptTemplate[];
|
|
111
112
|
mcpTools?: MCPToolMetadata[];
|
|
112
113
|
pythonToolProxy?: boolean;
|
package/src/task/worker.ts
CHANGED
|
@@ -608,6 +608,7 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
|
|
608
608
|
requireCompleteTool: true,
|
|
609
609
|
contextFiles: payload.contextFiles,
|
|
610
610
|
skills: payload.skills,
|
|
611
|
+
preloadedSkills: payload.preloadedSkills,
|
|
611
612
|
promptTemplates: payload.promptTemplates,
|
|
612
613
|
// Append system prompt (equivalent to CLI's --append-system-prompt)
|
|
613
614
|
systemPrompt: defaultPrompt =>
|
|
@@ -627,6 +628,14 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
|
|
627
628
|
runState.session = session;
|
|
628
629
|
checkAbort();
|
|
629
630
|
|
|
631
|
+
// Write session init metadata for debugging/replay
|
|
632
|
+
session.sessionManager.appendSessionInit({
|
|
633
|
+
systemPrompt: session.agent.state.systemPrompt,
|
|
634
|
+
task: payload.task,
|
|
635
|
+
tools: session.getAllToolNames(),
|
|
636
|
+
outputSchema: payload.outputSchema,
|
|
637
|
+
});
|
|
638
|
+
|
|
630
639
|
signal.addEventListener(
|
|
631
640
|
"abort",
|
|
632
641
|
() => {
|
package/src/tools/index.ts
CHANGED
|
@@ -19,7 +19,6 @@ import { AskTool } from "./ask";
|
|
|
19
19
|
import { BashTool } from "./bash";
|
|
20
20
|
import { CalculatorTool } from "./calculator";
|
|
21
21
|
import { CompleteTool } from "./complete";
|
|
22
|
-
import { EnterPlanModeTool } from "./enter-plan-mode";
|
|
23
22
|
import { ExitPlanModeTool } from "./exit-plan-mode";
|
|
24
23
|
import { FetchTool } from "./fetch";
|
|
25
24
|
import { FindTool } from "./find";
|
|
@@ -73,7 +72,6 @@ export { AskTool, type AskToolDetails } from "./ask";
|
|
|
73
72
|
export { BashTool, type BashToolDetails, type BashToolOptions } from "./bash";
|
|
74
73
|
export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
|
|
75
74
|
export { CompleteTool } from "./complete";
|
|
76
|
-
export { type EnterPlanModeDetails, EnterPlanModeTool } from "./enter-plan-mode";
|
|
77
75
|
export { type ExitPlanModeDetails, ExitPlanModeTool } from "./exit-plan-mode";
|
|
78
76
|
export { FetchTool, type FetchToolDetails } from "./fetch";
|
|
79
77
|
export { type FindOperations, FindTool, type FindToolDetails, type FindToolOptions } from "./find";
|
|
@@ -199,7 +197,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
199
197
|
fetch: s => new FetchTool(s),
|
|
200
198
|
web_search: s => new WebSearchTool(s),
|
|
201
199
|
write: s => new WriteTool(s),
|
|
202
|
-
enter_plan_mode: s => new EnterPlanModeTool(s),
|
|
203
200
|
};
|
|
204
201
|
|
|
205
202
|
export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
Transitions to plan mode for designing implementation approaches before writing code.
|
|
2
|
-
|
|
3
|
-
<conditions>
|
|
4
|
-
Prefer using EnterPlanMode for implementation tasks unless they're simple. Use it when ANY of these conditions apply:
|
|
5
|
-
1. **New Feature Implementation**: Adding meaningful new functionality
|
|
6
|
-
- Example: "Add a logout button" — where should it go? What should happen on click?
|
|
7
|
-
- Example: "Add form validation" — what rules? What error messages?
|
|
8
|
-
2. **Multiple Valid Approaches**: The task can be solved in several different ways
|
|
9
|
-
- Example: "Add caching to the API" — could use Redis, in-memory, file-based, etc.
|
|
10
|
-
- Example: "Improve performance" — many optimization strategies possible
|
|
11
|
-
3. **Code Modifications**: Changes that affect existing behavior or structure
|
|
12
|
-
- Example: "Update the login flow" — what exactly should change?
|
|
13
|
-
- Example: "Refactor this component" — what's the target architecture?
|
|
14
|
-
4. **Architectural Decisions**: The task requires choosing between patterns or technologies
|
|
15
|
-
- Example: "Add real-time updates" — WebSockets vs SSE vs polling
|
|
16
|
-
- Example: "Implement state management" — Redux vs Context vs custom solution
|
|
17
|
-
5. **Multi-File Changes**: The task will likely touch more than 2-3 files
|
|
18
|
-
- Example: "Refactor the authentication system"
|
|
19
|
-
- Example: "Add a new API endpoint with tests"
|
|
20
|
-
6. **Unclear Requirements**: You need to explore before understanding the full scope
|
|
21
|
-
- Example: "Make the app faster" — need to profile and identify bottlenecks
|
|
22
|
-
- Example: "Fix the bug in checkout" — need to investigate root cause
|
|
23
|
-
7. **User Preferences Matter**: The implementation could reasonably go multiple ways
|
|
24
|
-
- If you would use `ask` to clarify the approach, use EnterPlanMode instead
|
|
25
|
-
- Plan mode lets you explore first, then present options with context
|
|
26
|
-
</conditions>
|
|
27
|
-
|
|
28
|
-
<instruction>
|
|
29
|
-
In plan mode:
|
|
30
|
-
1. Explore codebase with `find`, `grep`, `read`, `ls`
|
|
31
|
-
2. Understand existing patterns and architecture
|
|
32
|
-
3. Design implementation approach
|
|
33
|
-
4. Use `ask` if clarification needed
|
|
34
|
-
5. Call `exit_plan_mode` when ready
|
|
35
|
-
</instruction>
|
|
36
|
-
|
|
37
|
-
<output>
|
|
38
|
-
Requires user approval to enter. Once approved, you enter read-only exploration mode with restricted tool access.
|
|
39
|
-
</output>
|
|
40
|
-
|
|
41
|
-
<parameters>
|
|
42
|
-
Optional parameters:
|
|
43
|
-
- `parallel`: Explore independent threads in parallel before synthesizing.
|
|
44
|
-
- `iterative`: One thread at a time with checkpoints between steps.
|
|
45
|
-
</parameters>
|
|
46
|
-
|
|
47
|
-
<example name="auth">
|
|
48
|
-
User: "Add user authentication to the app"
|
|
49
|
-
→ Use plan mode: architectural decisions (session vs JWT, where to store tokens, middleware structure)
|
|
50
|
-
</example>
|
|
51
|
-
|
|
52
|
-
<example name="optimization">
|
|
53
|
-
User: "Optimize the database queries"
|
|
54
|
-
→ Use plan mode: multiple approaches possible, need to profile first, significant impact
|
|
55
|
-
</example>
|
|
56
|
-
|
|
57
|
-
<example name="dark-mode">
|
|
58
|
-
User: "Implement dark mode"
|
|
59
|
-
→ Use plan mode: architectural decision on theme system, affects many components
|
|
60
|
-
</example>
|
|
61
|
-
|
|
62
|
-
<example name="delete-button">
|
|
63
|
-
User: "Add a delete button to the user profile"
|
|
64
|
-
→ Use plan mode: seems simple but involves placement, confirmation dialog, API call, error handling, state updates
|
|
65
|
-
</example>
|
|
66
|
-
|
|
67
|
-
<example name="error-handling">
|
|
68
|
-
User: "Update the error handling in the API"
|
|
69
|
-
→ Use plan mode: affects multiple files, user should approve the approach
|
|
70
|
-
</example>
|
|
71
|
-
|
|
72
|
-
<example name="typo-skip">
|
|
73
|
-
User: "Fix the typo in the README"
|
|
74
|
-
→ Skip plan mode: straightforward, no planning needed
|
|
75
|
-
</example>
|
|
76
|
-
|
|
77
|
-
<example name="debug-skip">
|
|
78
|
-
User: "Add a console.log to debug this function"
|
|
79
|
-
→ Skip plan mode: simple, obvious implementation
|
|
80
|
-
</example>
|
|
81
|
-
|
|
82
|
-
<example name="research-skip">
|
|
83
|
-
User: "What files handle routing?"
|
|
84
|
-
→ Skip plan mode: research task, not implementation planning
|
|
85
|
-
</example>
|
|
86
|
-
|
|
87
|
-
<avoid>
|
|
88
|
-
- Single-line or few-line fixes (typos, obvious bugs)
|
|
89
|
-
- Adding a single function with clear requirements
|
|
90
|
-
- Tasks with very specific, detailed instructions
|
|
91
|
-
- Pure research/exploration tasks
|
|
92
|
-
</avoid>
|
|
93
|
-
|
|
94
|
-
<critical>
|
|
95
|
-
- This tool REQUIRES user approval — they must consent to entering plan mode
|
|
96
|
-
- If unsure whether to use it, err on the side of planning — alignment upfront beats rework
|
|
97
|
-
- Users appreciate being consulted before significant changes are made to their codebase
|
|
98
|
-
</critical>
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import { Type } from "@sinclair/typebox";
|
|
6
|
-
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
7
|
-
import { resolvePlanUrlToPath } from "../internal-urls";
|
|
8
|
-
import enterPlanModeDescription from "../prompts/tools/enter-plan-mode.md" with { type: "text" };
|
|
9
|
-
import type { ToolSession } from ".";
|
|
10
|
-
import { ToolError } from "./tool-errors";
|
|
11
|
-
|
|
12
|
-
const enterPlanModeSchema = Type.Object({
|
|
13
|
-
workflow: Type.Optional(
|
|
14
|
-
StringEnum(["parallel", "iterative"], {
|
|
15
|
-
description: "Planning workflow to use",
|
|
16
|
-
}),
|
|
17
|
-
),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
export interface EnterPlanModeDetails {
|
|
21
|
-
planFilePath: string;
|
|
22
|
-
planExists: boolean;
|
|
23
|
-
workflow?: "parallel" | "iterative";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class EnterPlanModeTool implements AgentTool<typeof enterPlanModeSchema, EnterPlanModeDetails> {
|
|
27
|
-
public readonly name = "enter_plan_mode";
|
|
28
|
-
public readonly label = "EnterPlanMode";
|
|
29
|
-
public readonly description: string;
|
|
30
|
-
public readonly parameters = enterPlanModeSchema;
|
|
31
|
-
|
|
32
|
-
private readonly session: ToolSession;
|
|
33
|
-
|
|
34
|
-
constructor(session: ToolSession) {
|
|
35
|
-
this.session = session;
|
|
36
|
-
this.description = renderPromptTemplate(enterPlanModeDescription);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
public async execute(
|
|
40
|
-
_toolCallId: string,
|
|
41
|
-
params: { workflow?: "parallel" | "iterative" },
|
|
42
|
-
_signal?: AbortSignal,
|
|
43
|
-
_onUpdate?: AgentToolUpdateCallback<EnterPlanModeDetails>,
|
|
44
|
-
_context?: AgentToolContext,
|
|
45
|
-
): Promise<AgentToolResult<EnterPlanModeDetails>> {
|
|
46
|
-
const state = this.session.getPlanModeState?.();
|
|
47
|
-
if (state?.enabled) {
|
|
48
|
-
throw new ToolError("Plan mode is already active.");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const sessionId = this.session.getSessionId?.();
|
|
52
|
-
if (!sessionId) {
|
|
53
|
-
throw new ToolError("Plan mode requires an active session.");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const settingsManager = this.session.settingsManager;
|
|
57
|
-
if (!settingsManager) {
|
|
58
|
-
throw new ToolError("Settings manager unavailable for plan mode.");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const planFilePath = `plan://${sessionId}/plan.md`;
|
|
62
|
-
const resolvedPlanPath = resolvePlanUrlToPath(planFilePath, {
|
|
63
|
-
getPlansDirectory: settingsManager.getPlansDirectory.bind(settingsManager),
|
|
64
|
-
cwd: this.session.cwd,
|
|
65
|
-
});
|
|
66
|
-
let planExists = false;
|
|
67
|
-
try {
|
|
68
|
-
const stat = await fs.stat(resolvedPlanPath);
|
|
69
|
-
planExists = stat.isFile();
|
|
70
|
-
} catch (error) {
|
|
71
|
-
if (!isEnoent(error)) {
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
content: [{ type: "text", text: "Plan mode requested." }],
|
|
78
|
-
details: { planFilePath, planExists, workflow: params.workflow },
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
}
|