@oh-my-pi/pi-coding-agent 13.5.1 → 13.5.3
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 +36 -0
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +2 -1
- package/src/config/settings-schema.ts +18 -0
- package/src/debug/index.ts +11 -0
- package/src/exa/mcp-client.ts +57 -2
- package/src/modes/controllers/command-controller.ts +20 -0
- package/src/modes/controllers/extension-ui-controller.ts +52 -7
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/theme/mermaid-cache.ts +16 -57
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/patch/hashline.ts +41 -0
- package/src/prompts/system/plan-mode-active.md +12 -11
- package/src/prompts/system/plan-mode-subagent.md +3 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
- package/src/prompts/tools/bash.md +6 -4
- package/src/prompts/tools/checkpoint.md +16 -0
- package/src/prompts/tools/hashline.md +26 -69
- package/src/prompts/tools/render-mermaid.md +9 -0
- package/src/prompts/tools/rewind.md +13 -0
- package/src/sdk.ts +2 -0
- package/src/session/agent-session.ts +150 -3
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/tools/ask.ts +83 -51
- package/src/tools/bash.ts +5 -1
- package/src/tools/checkpoint.ts +128 -0
- package/src/tools/index.ts +31 -0
- package/src/tools/render-mermaid.ts +67 -0
- package/src/utils/prompt-format.ts +16 -18
package/src/tools/ask.ts
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
* - Use recommended: <index> to mark the default option; "(Recommended)" suffix is added automatically
|
|
15
15
|
* - Questions may time out and auto-select the recommended option (configurable, disabled in plan mode)
|
|
16
16
|
*/
|
|
17
|
+
|
|
17
18
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
18
19
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
19
20
|
import { TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
21
|
+
import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
|
20
22
|
import { type Static, Type } from "@sinclair/typebox";
|
|
21
23
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
22
24
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -25,6 +27,7 @@ import askDescription from "../prompts/tools/ask.md" with { type: "text" };
|
|
|
25
27
|
import { renderStatusLine } from "../tui";
|
|
26
28
|
import type { ToolSession } from ".";
|
|
27
29
|
import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
|
|
30
|
+
import { ToolAbortError } from "./tool-errors";
|
|
28
31
|
|
|
29
32
|
// =============================================================================
|
|
30
33
|
// Types
|
|
@@ -110,14 +113,9 @@ interface UIContext {
|
|
|
110
113
|
select(
|
|
111
114
|
prompt: string,
|
|
112
115
|
options: string[],
|
|
113
|
-
options_?: { initialIndex?: number;
|
|
116
|
+
options_?: { initialIndex?: number; signal?: AbortSignal; outline?: boolean },
|
|
114
117
|
): Promise<string | undefined>;
|
|
115
|
-
input(prompt: string): Promise<string | undefined>;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
interface AskQuestionOptions {
|
|
119
|
-
/** Timeout in milliseconds, null/undefined to disable */
|
|
120
|
-
timeout?: number | null;
|
|
118
|
+
input(prompt: string, options_?: { signal?: AbortSignal }): Promise<string | undefined>;
|
|
121
119
|
}
|
|
122
120
|
|
|
123
121
|
async function askSingleQuestion(
|
|
@@ -126,9 +124,8 @@ async function askSingleQuestion(
|
|
|
126
124
|
optionLabels: string[],
|
|
127
125
|
multi: boolean,
|
|
128
126
|
recommended?: number,
|
|
129
|
-
|
|
127
|
+
signal?: AbortSignal,
|
|
130
128
|
): Promise<SelectionResult> {
|
|
131
|
-
const timeout = options?.timeout ?? undefined;
|
|
132
129
|
const doneLabel = getDoneOptionLabel();
|
|
133
130
|
let selectedOptions: string[] = [];
|
|
134
131
|
let customInput: string | undefined;
|
|
@@ -152,22 +149,27 @@ async function askSingleQuestion(
|
|
|
152
149
|
opts.push(OTHER_OPTION);
|
|
153
150
|
|
|
154
151
|
const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
152
|
+
const choice = signal
|
|
153
|
+
? await untilAborted(signal, () =>
|
|
154
|
+
ui.select(`${prefix}${question}`, opts, {
|
|
155
|
+
initialIndex: cursorIndex,
|
|
156
|
+
signal,
|
|
157
|
+
outline: true,
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
: await ui.select(`${prefix}${question}`, opts, {
|
|
161
|
+
initialIndex: cursorIndex,
|
|
162
|
+
signal,
|
|
163
|
+
outline: true,
|
|
164
|
+
});
|
|
163
165
|
|
|
164
166
|
if (choice === undefined || choice === doneLabel) break;
|
|
165
167
|
|
|
166
168
|
if (choice === OTHER_OPTION) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
const input = signal
|
|
170
|
+
? await untilAborted(signal, () => ui.input("Enter your response:", { signal }))
|
|
171
|
+
: await ui.input("Enter your response:", { signal });
|
|
172
|
+
if (input) customInput = input;
|
|
171
173
|
break;
|
|
172
174
|
}
|
|
173
175
|
|
|
@@ -192,21 +194,28 @@ async function askSingleQuestion(
|
|
|
192
194
|
selected.add(opt);
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
|
-
|
|
196
|
-
if (timedOut) {
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
197
|
}
|
|
200
198
|
selectedOptions = Array.from(selected);
|
|
201
199
|
} else {
|
|
202
200
|
const displayLabels = addRecommendedSuffix(optionLabels, recommended);
|
|
203
|
-
const choice =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
201
|
+
const choice = signal
|
|
202
|
+
? await untilAborted(signal, () =>
|
|
203
|
+
ui.select(question, [...displayLabels, OTHER_OPTION], {
|
|
204
|
+
initialIndex: recommended,
|
|
205
|
+
signal,
|
|
206
|
+
outline: true,
|
|
207
|
+
}),
|
|
208
|
+
)
|
|
209
|
+
: await ui.select(question, [...displayLabels, OTHER_OPTION], {
|
|
210
|
+
initialIndex: recommended,
|
|
211
|
+
signal,
|
|
212
|
+
outline: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
208
215
|
if (choice === OTHER_OPTION) {
|
|
209
|
-
const input =
|
|
216
|
+
const input = signal
|
|
217
|
+
? await untilAborted(signal, () => ui.input("Enter your response:", { signal }))
|
|
218
|
+
: await ui.input("Enter your response:", { signal });
|
|
210
219
|
if (input) customInput = input;
|
|
211
220
|
} else if (choice) {
|
|
212
221
|
selectedOptions = [stripRecommendedSuffix(choice)];
|
|
@@ -265,7 +274,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
265
274
|
async execute(
|
|
266
275
|
_toolCallId: string,
|
|
267
276
|
params: AskParams,
|
|
268
|
-
|
|
277
|
+
signal?: AbortSignal,
|
|
269
278
|
_onUpdate?: AgentToolUpdateCallback<AskToolDetails>,
|
|
270
279
|
context?: AgentToolContext,
|
|
271
280
|
): Promise<AgentToolResult<AskToolDetails>> {
|
|
@@ -277,7 +286,11 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
277
286
|
};
|
|
278
287
|
}
|
|
279
288
|
|
|
280
|
-
const
|
|
289
|
+
const extensionUi = context.ui;
|
|
290
|
+
const ui: UIContext = {
|
|
291
|
+
select: (prompt, options, dialogOptions) => extensionUi.select(prompt, options, dialogOptions),
|
|
292
|
+
input: (prompt, dialogOptions) => extensionUi.input(prompt, undefined, dialogOptions),
|
|
293
|
+
};
|
|
281
294
|
|
|
282
295
|
// Determine timeout based on settings and plan mode
|
|
283
296
|
const planModeEnabled = this.session.getPlanModeState?.()?.enabled ?? false;
|
|
@@ -296,18 +309,41 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
296
309
|
};
|
|
297
310
|
}
|
|
298
311
|
|
|
312
|
+
const askQuestion = async (q: AskParams["questions"][number]) => {
|
|
313
|
+
const optionLabels = q.options.map(o => o.label);
|
|
314
|
+
const timeoutSignal = timeout == null ? undefined : AbortSignal.timeout(timeout);
|
|
315
|
+
const questionSignal = ptree.combineSignals(signal, timeoutSignal);
|
|
316
|
+
try {
|
|
317
|
+
const { selectedOptions, customInput } = await askSingleQuestion(
|
|
318
|
+
ui,
|
|
319
|
+
q.question,
|
|
320
|
+
optionLabels,
|
|
321
|
+
q.multi ?? false,
|
|
322
|
+
q.recommended,
|
|
323
|
+
questionSignal,
|
|
324
|
+
);
|
|
325
|
+
return { optionLabels, selectedOptions, customInput, timedOut: false };
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
328
|
+
if (signal?.aborted) {
|
|
329
|
+
throw new ToolAbortError("Ask input was cancelled");
|
|
330
|
+
}
|
|
331
|
+
if (timeoutSignal?.aborted) {
|
|
332
|
+
return { optionLabels, selectedOptions: [], customInput: undefined, timedOut: true };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
299
339
|
if (params.questions.length === 1) {
|
|
300
340
|
const [q] = params.questions;
|
|
301
|
-
const optionLabels = q
|
|
302
|
-
const { selectedOptions, customInput } = await askSingleQuestion(
|
|
303
|
-
ui,
|
|
304
|
-
q.question,
|
|
305
|
-
optionLabels,
|
|
306
|
-
q.multi ?? false,
|
|
307
|
-
q.recommended,
|
|
308
|
-
{ timeout },
|
|
309
|
-
);
|
|
341
|
+
const { optionLabels, selectedOptions, customInput, timedOut } = await askQuestion(q);
|
|
310
342
|
|
|
343
|
+
if (!timedOut && selectedOptions.length === 0 && !customInput) {
|
|
344
|
+
context.abort();
|
|
345
|
+
throw new ToolAbortError("Ask tool was cancelled by the user");
|
|
346
|
+
}
|
|
311
347
|
const details: AskToolDetails = {
|
|
312
348
|
question: q.question,
|
|
313
349
|
options: optionLabels,
|
|
@@ -333,16 +369,12 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
333
369
|
const results: QuestionResult[] = [];
|
|
334
370
|
|
|
335
371
|
for (const q of params.questions) {
|
|
336
|
-
const optionLabels = q
|
|
337
|
-
const { selectedOptions, customInput } = await askSingleQuestion(
|
|
338
|
-
ui,
|
|
339
|
-
q.question,
|
|
340
|
-
optionLabels,
|
|
341
|
-
q.multi ?? false,
|
|
342
|
-
q.recommended,
|
|
343
|
-
{ timeout },
|
|
344
|
-
);
|
|
372
|
+
const { optionLabels, selectedOptions, customInput, timedOut } = await askQuestion(q);
|
|
345
373
|
|
|
374
|
+
if (!timedOut && selectedOptions.length === 0 && !customInput) {
|
|
375
|
+
context.abort();
|
|
376
|
+
throw new ToolAbortError("Ask tool was cancelled by the user");
|
|
377
|
+
}
|
|
346
378
|
results.push({
|
|
347
379
|
id: q.id,
|
|
348
380
|
question: q.question,
|
package/src/tools/bash.ts
CHANGED
|
@@ -98,7 +98,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
98
98
|
constructor(private readonly session: ToolSession) {
|
|
99
99
|
this.#asyncEnabled = this.session.settings.get("async.enabled");
|
|
100
100
|
this.parameters = this.#asyncEnabled ? bashSchemaWithAsync : bashSchemaBase;
|
|
101
|
-
this.description = renderPromptTemplate(bashDescription, {
|
|
101
|
+
this.description = renderPromptTemplate(bashDescription, {
|
|
102
|
+
asyncEnabled: this.#asyncEnabled,
|
|
103
|
+
hasAstGrep: this.session.settings.get("astGrep.enabled"),
|
|
104
|
+
hasAstEdit: this.session.settings.get("astEdit.enabled"),
|
|
105
|
+
});
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
#formatResultOutput(result: BashResult | BashInteractiveResult, headLines?: number, tailLines?: number): string {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
3
|
+
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
4
|
+
import checkpointDescription from "../prompts/tools/checkpoint.md" with { type: "text" };
|
|
5
|
+
import rewindDescription from "../prompts/tools/rewind.md" with { type: "text" };
|
|
6
|
+
import type { ToolSession } from ".";
|
|
7
|
+
import type { OutputMeta } from "./output-meta";
|
|
8
|
+
import { ToolError } from "./tool-errors";
|
|
9
|
+
import { toolResult } from "./tool-result";
|
|
10
|
+
|
|
11
|
+
export interface CheckpointState {
|
|
12
|
+
/** Number of in-memory messages at checkpoint (AFTER checkpoint tool result is appended) */
|
|
13
|
+
checkpointMessageCount: number;
|
|
14
|
+
/** Session entry ID at checkpoint (for session tree branching) */
|
|
15
|
+
checkpointEntryId: string | null;
|
|
16
|
+
/** Timestamp */
|
|
17
|
+
startedAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const checkpointSchema = Type.Object({
|
|
21
|
+
goal: Type.String({ description: "What you are investigating and why" }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type CheckpointParams = Static<typeof checkpointSchema>;
|
|
25
|
+
|
|
26
|
+
const rewindSchema = Type.Object({
|
|
27
|
+
report: Type.String({ description: "Concise investigation findings to retain after rewind" }),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
type RewindParams = Static<typeof rewindSchema>;
|
|
31
|
+
|
|
32
|
+
export interface CheckpointToolDetails {
|
|
33
|
+
goal: string;
|
|
34
|
+
startedAt: string;
|
|
35
|
+
meta?: OutputMeta;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RewindToolDetails {
|
|
39
|
+
report: string;
|
|
40
|
+
rewound: boolean;
|
|
41
|
+
meta?: OutputMeta;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isTopLevelSession(session: ToolSession): boolean {
|
|
45
|
+
const depth = session.taskDepth;
|
|
46
|
+
return depth === undefined || depth === 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class CheckpointTool implements AgentTool<typeof checkpointSchema, CheckpointToolDetails> {
|
|
50
|
+
readonly name = "checkpoint";
|
|
51
|
+
readonly label = "Checkpoint";
|
|
52
|
+
readonly description: string;
|
|
53
|
+
readonly parameters = checkpointSchema;
|
|
54
|
+
readonly strict = true;
|
|
55
|
+
|
|
56
|
+
constructor(private readonly session: ToolSession) {
|
|
57
|
+
this.description = renderPromptTemplate(checkpointDescription);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static createIf(session: ToolSession): CheckpointTool | null {
|
|
61
|
+
if (!isTopLevelSession(session)) return null;
|
|
62
|
+
return new CheckpointTool(session);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async execute(
|
|
66
|
+
_toolCallId: string,
|
|
67
|
+
params: CheckpointParams,
|
|
68
|
+
_signal?: AbortSignal,
|
|
69
|
+
_onUpdate?: AgentToolUpdateCallback<CheckpointToolDetails>,
|
|
70
|
+
_context?: AgentToolContext,
|
|
71
|
+
): Promise<AgentToolResult<CheckpointToolDetails>> {
|
|
72
|
+
if (!isTopLevelSession(this.session)) {
|
|
73
|
+
throw new ToolError("Checkpoint not available in subagents.");
|
|
74
|
+
}
|
|
75
|
+
if (this.session.getCheckpointState?.()) {
|
|
76
|
+
throw new ToolError("Checkpoint already active.");
|
|
77
|
+
}
|
|
78
|
+
const startedAt = new Date().toISOString();
|
|
79
|
+
return toolResult<CheckpointToolDetails>({ goal: params.goal, startedAt })
|
|
80
|
+
.text(
|
|
81
|
+
[
|
|
82
|
+
"Checkpoint created.",
|
|
83
|
+
`Goal: ${params.goal}`,
|
|
84
|
+
"Run your investigation, then call rewind with a concise report.",
|
|
85
|
+
].join("\n"),
|
|
86
|
+
)
|
|
87
|
+
.done();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class RewindTool implements AgentTool<typeof rewindSchema, RewindToolDetails> {
|
|
92
|
+
readonly name = "rewind";
|
|
93
|
+
readonly label = "Rewind";
|
|
94
|
+
readonly description: string;
|
|
95
|
+
readonly parameters = rewindSchema;
|
|
96
|
+
readonly strict = true;
|
|
97
|
+
|
|
98
|
+
constructor(private readonly session: ToolSession) {
|
|
99
|
+
this.description = renderPromptTemplate(rewindDescription);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static createIf(session: ToolSession): RewindTool | null {
|
|
103
|
+
if (!isTopLevelSession(session)) return null;
|
|
104
|
+
return new RewindTool(session);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async execute(
|
|
108
|
+
_toolCallId: string,
|
|
109
|
+
params: RewindParams,
|
|
110
|
+
_signal?: AbortSignal,
|
|
111
|
+
_onUpdate?: AgentToolUpdateCallback<RewindToolDetails>,
|
|
112
|
+
_context?: AgentToolContext,
|
|
113
|
+
): Promise<AgentToolResult<RewindToolDetails>> {
|
|
114
|
+
if (!isTopLevelSession(this.session)) {
|
|
115
|
+
throw new ToolError("Checkpoint not available in subagents.");
|
|
116
|
+
}
|
|
117
|
+
if (!this.session.getCheckpointState?.()) {
|
|
118
|
+
throw new ToolError("No active checkpoint.");
|
|
119
|
+
}
|
|
120
|
+
const report = params.report.trim();
|
|
121
|
+
if (report.length === 0) {
|
|
122
|
+
throw new ToolError("Report cannot be empty.");
|
|
123
|
+
}
|
|
124
|
+
return toolResult<RewindToolDetails>({ report, rewound: true })
|
|
125
|
+
.text(["Rewind requested.", "Report captured for context replacement."].join("\n"))
|
|
126
|
+
.done();
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { BashTool } from "./bash";
|
|
|
22
22
|
import { BrowserTool } from "./browser";
|
|
23
23
|
import { CalculatorTool } from "./calculator";
|
|
24
24
|
import { CancelJobTool } from "./cancel-job";
|
|
25
|
+
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
25
26
|
import { ExitPlanModeTool } from "./exit-plan-mode";
|
|
26
27
|
import { FetchTool } from "./fetch";
|
|
27
28
|
import { FindTool } from "./find";
|
|
@@ -30,6 +31,7 @@ import { NotebookTool } from "./notebook";
|
|
|
30
31
|
import { wrapToolWithMetaNotice } from "./output-meta";
|
|
31
32
|
import { PythonTool } from "./python";
|
|
32
33
|
import { ReadTool } from "./read";
|
|
34
|
+
import { RenderMermaidTool } from "./render-mermaid";
|
|
33
35
|
import { ResolveTool } from "./resolve";
|
|
34
36
|
import { reportFindingTool } from "./review";
|
|
35
37
|
import { loadSshTool } from "./ssh";
|
|
@@ -54,6 +56,7 @@ export * from "./bash";
|
|
|
54
56
|
export * from "./browser";
|
|
55
57
|
export * from "./calculator";
|
|
56
58
|
export * from "./cancel-job";
|
|
59
|
+
export * from "./checkpoint";
|
|
57
60
|
export * from "./exit-plan-mode";
|
|
58
61
|
export * from "./fetch";
|
|
59
62
|
export * from "./find";
|
|
@@ -63,6 +66,7 @@ export * from "./notebook";
|
|
|
63
66
|
export * from "./pending-action";
|
|
64
67
|
export * from "./python";
|
|
65
68
|
export * from "./read";
|
|
69
|
+
export * from "./render-mermaid";
|
|
66
70
|
export * from "./resolve";
|
|
67
71
|
export * from "./review";
|
|
68
72
|
export * from "./ssh";
|
|
@@ -143,6 +147,10 @@ export interface ToolSession {
|
|
|
143
147
|
setTodoPhases?: (phases: TodoPhase[]) => void;
|
|
144
148
|
/** Pending action store for preview/apply workflows */
|
|
145
149
|
pendingActionStore?: import("./pending-action").PendingActionStore;
|
|
150
|
+
/** Get active checkpoint state if any. */
|
|
151
|
+
getCheckpointState?: () => CheckpointState | undefined;
|
|
152
|
+
/** Set or clear active checkpoint state. */
|
|
153
|
+
setCheckpointState?: (state: CheckpointState | null) => void;
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
|
|
@@ -150,6 +158,7 @@ type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
|
|
|
150
158
|
export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
151
159
|
ast_grep: s => new AstGrepTool(s),
|
|
152
160
|
ast_edit: s => new AstEditTool(s),
|
|
161
|
+
render_mermaid: s => new RenderMermaidTool(s),
|
|
153
162
|
ask: AskTool.createIf,
|
|
154
163
|
bash: s => new BashTool(s),
|
|
155
164
|
python: s => new PythonTool(s),
|
|
@@ -162,6 +171,8 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
162
171
|
notebook: s => new NotebookTool(s),
|
|
163
172
|
read: s => new ReadTool(s),
|
|
164
173
|
browser: s => new BrowserTool(s),
|
|
174
|
+
checkpoint: CheckpointTool.createIf,
|
|
175
|
+
rewind: RewindTool.createIf,
|
|
165
176
|
task: TaskTool.create,
|
|
166
177
|
cancel_job: CancelJobTool.createIf,
|
|
167
178
|
await: AwaitTool.createIf,
|
|
@@ -271,6 +282,24 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
271
282
|
) {
|
|
272
283
|
requestedTools.push("bash");
|
|
273
284
|
}
|
|
285
|
+
|
|
286
|
+
// Auto-include AST counterparts when their text-based sibling is present
|
|
287
|
+
if (requestedTools) {
|
|
288
|
+
if (
|
|
289
|
+
requestedTools.includes("grep") &&
|
|
290
|
+
!requestedTools.includes("ast_grep") &&
|
|
291
|
+
session.settings.get("astGrep.enabled")
|
|
292
|
+
) {
|
|
293
|
+
requestedTools.push("ast_grep");
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
requestedTools.includes("edit") &&
|
|
297
|
+
!requestedTools.includes("ast_edit") &&
|
|
298
|
+
session.settings.get("astEdit.enabled")
|
|
299
|
+
) {
|
|
300
|
+
requestedTools.push("ast_edit");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
274
303
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
275
304
|
const isToolAllowed = (name: string) => {
|
|
276
305
|
if (name === "lsp") return enableLsp;
|
|
@@ -281,12 +310,14 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
281
310
|
if (name === "grep") return session.settings.get("grep.enabled");
|
|
282
311
|
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
|
|
283
312
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|
|
313
|
+
if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
|
|
284
314
|
if (name === "notebook") return session.settings.get("notebook.enabled");
|
|
285
315
|
if (name === "fetch") return session.settings.get("fetch.enabled");
|
|
286
316
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
287
317
|
if (name === "lsp") return session.settings.get("lsp.enabled");
|
|
288
318
|
if (name === "calc") return session.settings.get("calc.enabled");
|
|
289
319
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
320
|
+
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
290
321
|
if (name === "task") {
|
|
291
322
|
const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
|
|
292
323
|
const currentDepth = session.taskDepth ?? 0;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { type MermaidAsciiRenderOptions, renderMermaidAscii } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
5
|
+
import renderMermaidDescription from "../prompts/tools/render-mermaid.md" with { type: "text" };
|
|
6
|
+
import type { ToolSession } from "./index";
|
|
7
|
+
|
|
8
|
+
const renderMermaidSchema = Type.Object({
|
|
9
|
+
mermaid: Type.String({ description: "Mermaid graph source text" }),
|
|
10
|
+
config: Type.Optional(
|
|
11
|
+
Type.Object({
|
|
12
|
+
useAscii: Type.Optional(Type.Boolean()),
|
|
13
|
+
paddingX: Type.Optional(Type.Number()),
|
|
14
|
+
paddingY: Type.Optional(Type.Number()),
|
|
15
|
+
boxBorderPadding: Type.Optional(Type.Number()),
|
|
16
|
+
}),
|
|
17
|
+
),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
type RenderMermaidParams = Static<typeof renderMermaidSchema>;
|
|
21
|
+
|
|
22
|
+
function sanitizeRenderConfig(config: MermaidAsciiRenderOptions | undefined): MermaidAsciiRenderOptions | undefined {
|
|
23
|
+
if (!config) return undefined;
|
|
24
|
+
return {
|
|
25
|
+
useAscii: config.useAscii,
|
|
26
|
+
boxBorderPadding:
|
|
27
|
+
config.boxBorderPadding === undefined ? undefined : Math.max(0, Math.floor(config.boxBorderPadding)),
|
|
28
|
+
paddingX: config.paddingX === undefined ? undefined : Math.max(0, Math.floor(config.paddingX)),
|
|
29
|
+
paddingY: config.paddingY === undefined ? undefined : Math.max(0, Math.floor(config.paddingY)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface RenderMermaidToolDetails {
|
|
33
|
+
artifactId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema, RenderMermaidToolDetails> {
|
|
37
|
+
readonly name = "render_mermaid";
|
|
38
|
+
readonly label = "RenderMermaid";
|
|
39
|
+
readonly description: string;
|
|
40
|
+
readonly parameters = renderMermaidSchema;
|
|
41
|
+
readonly strict = true;
|
|
42
|
+
|
|
43
|
+
constructor(private readonly session: ToolSession) {
|
|
44
|
+
this.description = renderPromptTemplate(renderMermaidDescription);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async execute(
|
|
48
|
+
_toolCallId: string,
|
|
49
|
+
params: RenderMermaidParams,
|
|
50
|
+
_signal?: AbortSignal,
|
|
51
|
+
_onUpdate?: AgentToolUpdateCallback<RenderMermaidToolDetails>,
|
|
52
|
+
_context?: AgentToolContext,
|
|
53
|
+
): Promise<AgentToolResult<RenderMermaidToolDetails>> {
|
|
54
|
+
const ascii = renderMermaidAscii(params.mermaid, sanitizeRenderConfig(params.config));
|
|
55
|
+
const { path: artifactPath, id: artifactId } =
|
|
56
|
+
(await this.session.allocateOutputArtifact?.("render_mermaid")) ?? {};
|
|
57
|
+
if (artifactPath) {
|
|
58
|
+
await Bun.write(artifactPath, ascii);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const artifactLine = artifactId ? `\n\nSaved artifact: artifact://${artifactId}` : "";
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: `${ascii}${artifactLine}` }],
|
|
64
|
+
details: { artifactId },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -86,9 +86,8 @@ export function formatPromptContent(content: string, options: PromptFormatOption
|
|
|
86
86
|
|
|
87
87
|
for (let i = 0; i < lines.length; i++) {
|
|
88
88
|
let line = lines[i].trimEnd();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (CODE_FENCE.test(trimmed)) {
|
|
89
|
+
let trimmedStart = line.trimStart();
|
|
90
|
+
if (CODE_FENCE.test(trimmedStart)) {
|
|
92
91
|
inCodeBlock = !inCodeBlock;
|
|
93
92
|
result.push(line);
|
|
94
93
|
continue;
|
|
@@ -102,30 +101,29 @@ export function formatPromptContent(content: string, options: PromptFormatOption
|
|
|
102
101
|
if (replaceAsciiSymbols) {
|
|
103
102
|
line = replaceCommonAsciiSymbols(line);
|
|
104
103
|
}
|
|
104
|
+
trimmedStart = line.trimStart();
|
|
105
|
+
const trimmed = line.trim();
|
|
105
106
|
|
|
106
|
-
const isOpeningXml = OPENING_XML.test(
|
|
107
|
-
if (isOpeningXml && line.length ===
|
|
108
|
-
const match = OPENING_XML.exec(
|
|
107
|
+
const isOpeningXml = OPENING_XML.test(trimmedStart) && !trimmedStart.endsWith("/>");
|
|
108
|
+
if (isOpeningXml && line.length === trimmedStart.length) {
|
|
109
|
+
const match = OPENING_XML.exec(trimmedStart);
|
|
109
110
|
if (match) topLevelTags.push(match[1]);
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
const closingMatch = CLOSING_XML.exec(
|
|
113
|
+
const closingMatch = CLOSING_XML.exec(trimmedStart);
|
|
113
114
|
if (closingMatch) {
|
|
114
115
|
const tagName = closingMatch[1];
|
|
115
116
|
if (topLevelTags.length > 0 && topLevelTags[topLevelTags.length - 1] === tagName) {
|
|
116
|
-
line = trimmed;
|
|
117
117
|
topLevelTags.pop();
|
|
118
|
-
} else {
|
|
119
|
-
line = line.trimEnd();
|
|
120
118
|
}
|
|
121
|
-
} else if (isPreRender &&
|
|
122
|
-
|
|
123
|
-
} else if (TABLE_SEP.test(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
line =
|
|
119
|
+
} else if (isPreRender && trimmedStart.startsWith("{{")) {
|
|
120
|
+
/* keep indentation as-is in pre-render for Handlebars markers */
|
|
121
|
+
} else if (TABLE_SEP.test(trimmedStart)) {
|
|
122
|
+
const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
|
|
123
|
+
line = `${leadingWhitespace}${compactTableSep(trimmedStart)}`;
|
|
124
|
+
} else if (TABLE_ROW.test(trimmedStart)) {
|
|
125
|
+
const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
|
|
126
|
+
line = `${leadingWhitespace}${compactTableRow(trimmedStart)}`;
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
if (shouldBoldRfc2119) {
|