feat: misc tool alignment (#137)
This commit is contained in:
80
src/tools/impl/AskUserQuestion.ts
Normal file
80
src/tools/impl/AskUserQuestion.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { validateRequiredParams } from "./validation.js";
|
||||
|
||||
interface QuestionOption {
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Question {
|
||||
question: string;
|
||||
header: string;
|
||||
options: QuestionOption[];
|
||||
multiSelect: boolean;
|
||||
}
|
||||
|
||||
interface AskUserQuestionArgs {
|
||||
questions: Question[];
|
||||
answers?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface AskUserQuestionResult {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function ask_user_question(
|
||||
args: AskUserQuestionArgs,
|
||||
): Promise<AskUserQuestionResult> {
|
||||
validateRequiredParams(args, ["questions"], "AskUserQuestion");
|
||||
|
||||
if (!Array.isArray(args.questions) || args.questions.length === 0) {
|
||||
throw new Error("questions must be a non-empty array");
|
||||
}
|
||||
|
||||
if (args.questions.length > 4) {
|
||||
throw new Error("Maximum of 4 questions allowed");
|
||||
}
|
||||
|
||||
for (const q of args.questions) {
|
||||
if (!q.question || typeof q.question !== "string") {
|
||||
throw new Error("Each question must have a question string");
|
||||
}
|
||||
if (!q.header || typeof q.header !== "string") {
|
||||
throw new Error("Each question must have a header string");
|
||||
}
|
||||
if (
|
||||
!Array.isArray(q.options) ||
|
||||
q.options.length < 2 ||
|
||||
q.options.length > 4
|
||||
) {
|
||||
throw new Error("Each question must have 2-4 options");
|
||||
}
|
||||
if (typeof q.multiSelect !== "boolean") {
|
||||
throw new Error("Each question must have a multiSelect boolean");
|
||||
}
|
||||
for (const opt of q.options) {
|
||||
if (!opt.label || typeof opt.label !== "string") {
|
||||
throw new Error("Each option must have a label string");
|
||||
}
|
||||
if (!opt.description || typeof opt.description !== "string") {
|
||||
throw new Error("Each option must have a description string");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If answers are provided (filled in by UI layer), format the response
|
||||
if (args.answers && Object.keys(args.answers).length > 0) {
|
||||
const answerParts = args.questions.map((q) => {
|
||||
const answer = args.answers?.[q.question] || "";
|
||||
return `"${q.question}"="${answer}"`;
|
||||
});
|
||||
return {
|
||||
message: `User has answered your questions: ${answerParts.join(", ")}. You can now continue with the user's answers in mind.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, return a placeholder - the UI layer should intercept this tool call
|
||||
// and show the question UI before returning the actual response
|
||||
return {
|
||||
message: "Waiting for user response...",
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { LIMITS, truncateByChars } from "./truncation.js";
|
||||
import { validateRequiredParams } from "./validation.js";
|
||||
|
||||
interface BashOutputArgs {
|
||||
bash_id: string;
|
||||
shell_id: string;
|
||||
filter?: string;
|
||||
}
|
||||
interface BashOutputResult {
|
||||
@@ -13,11 +13,11 @@ interface BashOutputResult {
|
||||
export async function bash_output(
|
||||
args: BashOutputArgs,
|
||||
): Promise<BashOutputResult> {
|
||||
validateRequiredParams(args, ["bash_id"], "BashOutput");
|
||||
const { bash_id, filter } = args;
|
||||
const proc = backgroundProcesses.get(bash_id);
|
||||
validateRequiredParams(args, ["shell_id"], "BashOutput");
|
||||
const { shell_id, filter } = args;
|
||||
const proc = backgroundProcesses.get(shell_id);
|
||||
if (!proc)
|
||||
return { message: `No background process found with ID: ${bash_id}` };
|
||||
return { message: `No background process found with ID: ${shell_id}` };
|
||||
const stdout = proc.stdout.join("\n");
|
||||
const stderr = proc.stderr.join("\n");
|
||||
let text = stdout;
|
||||
|
||||
32
src/tools/impl/EnterPlanMode.ts
Normal file
32
src/tools/impl/EnterPlanMode.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
interface EnterPlanModeArgs {
|
||||
[key: string]: never;
|
||||
}
|
||||
|
||||
interface EnterPlanModeResult {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function enter_plan_mode(
|
||||
_args: EnterPlanModeArgs,
|
||||
): Promise<EnterPlanModeResult> {
|
||||
// This is handled by the UI layer which will:
|
||||
// 1. Show approval dialog
|
||||
// 2. On approve: toggle plan mode on, generate plan file path, inject system reminder
|
||||
// 3. On reject: send rejection, agent proceeds without plan mode
|
||||
//
|
||||
// The message below is returned on successful entry into plan mode.
|
||||
// The UI harness will also inject a <system-reminder> with the plan file path.
|
||||
return {
|
||||
message: `Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach.
|
||||
|
||||
In plan mode, you should:
|
||||
1. Thoroughly explore the codebase to understand existing patterns
|
||||
2. Identify similar features and architectural approaches
|
||||
3. Consider multiple approaches and their trade-offs
|
||||
4. Use AskUserQuestion if you need to clarify the approach
|
||||
5. Design a concrete implementation strategy
|
||||
6. When ready, use ExitPlanMode to present your plan for approval
|
||||
|
||||
Remember: DO NOT write or edit any files yet. This is a read-only exploration and planning phase.`,
|
||||
};
|
||||
}
|
||||
@@ -1,22 +1,11 @@
|
||||
/**
|
||||
* ExitPlanMode tool implementation
|
||||
* Exits plan mode by presenting the plan to the user for approval
|
||||
* Exits plan mode - the plan is read from the plan file by the UI
|
||||
*/
|
||||
|
||||
import { validateRequiredParams } from "./validation.js";
|
||||
|
||||
interface ExitPlanModeArgs {
|
||||
plan: string;
|
||||
}
|
||||
|
||||
export async function exit_plan_mode(
|
||||
args: ExitPlanModeArgs,
|
||||
): Promise<{ message: string }> {
|
||||
validateRequiredParams(args, ["plan"], "ExitPlanMode");
|
||||
const { plan: _plan } = args;
|
||||
|
||||
export async function exit_plan_mode(): Promise<{ message: string }> {
|
||||
// Return confirmation message that plan was approved
|
||||
// Note: The plan itself should be displayed by the UI/system before this return is shown
|
||||
// Note: The plan is read from the plan file by the UI before this return is shown
|
||||
return {
|
||||
message:
|
||||
"User has approved your plan. You can now start coding.\nStart with updating your todo list if applicable",
|
||||
|
||||
@@ -21,6 +21,18 @@ function getRipgrepPath(): string {
|
||||
|
||||
const rgPath = getRipgrepPath();
|
||||
|
||||
function applyOffsetAndLimit<T>(
|
||||
items: T[],
|
||||
offset: number,
|
||||
limit: number,
|
||||
): T[] {
|
||||
const sliced = items.slice(offset);
|
||||
if (limit > 0) {
|
||||
return sliced.slice(0, limit);
|
||||
}
|
||||
return sliced; // 0 = unlimited
|
||||
}
|
||||
|
||||
export interface GrepArgs {
|
||||
pattern: string;
|
||||
path?: string;
|
||||
@@ -32,6 +44,8 @@ export interface GrepArgs {
|
||||
"-n"?: boolean;
|
||||
"-i"?: boolean;
|
||||
type?: string;
|
||||
head_limit?: number;
|
||||
offset?: number;
|
||||
multiline?: boolean;
|
||||
}
|
||||
|
||||
@@ -51,9 +65,11 @@ export async function grep(args: GrepArgs): Promise<GrepResult> {
|
||||
"-B": before,
|
||||
"-A": after,
|
||||
"-C": context,
|
||||
"-n": lineNumbers,
|
||||
"-n": lineNumbers = true,
|
||||
"-i": ignoreCase,
|
||||
type: fileType,
|
||||
head_limit = 100,
|
||||
offset = 0,
|
||||
multiline,
|
||||
} = args;
|
||||
|
||||
@@ -88,12 +104,14 @@ export async function grep(args: GrepArgs): Promise<GrepResult> {
|
||||
cwd: userCwd,
|
||||
});
|
||||
if (output_mode === "files_with_matches") {
|
||||
const files = stdout.trim().split("\n").filter(Boolean);
|
||||
const allFiles = stdout.trim().split("\n").filter(Boolean);
|
||||
const files = applyOffsetAndLimit(allFiles, offset, head_limit);
|
||||
const fileCount = files.length;
|
||||
if (fileCount === 0) return { output: "No files found", files: 0 };
|
||||
const totalCount = allFiles.length;
|
||||
if (totalCount === 0) return { output: "No files found", files: 0 };
|
||||
|
||||
const fileList = files.join("\n");
|
||||
const fullOutput = `Found ${fileCount} file${fileCount !== 1 ? "s" : ""}\n${fileList}`;
|
||||
const fullOutput = `Found ${totalCount} file${totalCount !== 1 ? "s" : ""}${fileCount < totalCount ? ` (showing ${fileCount})` : ""}\n${fileList}`;
|
||||
|
||||
// Apply character limit to prevent large file lists
|
||||
const { content: truncatedOutput } = truncateByChars(
|
||||
@@ -104,13 +122,14 @@ export async function grep(args: GrepArgs): Promise<GrepResult> {
|
||||
|
||||
return {
|
||||
output: truncatedOutput,
|
||||
files: fileCount,
|
||||
files: totalCount,
|
||||
};
|
||||
} else if (output_mode === "count") {
|
||||
const lines = stdout.trim().split("\n").filter(Boolean);
|
||||
const allLines = stdout.trim().split("\n").filter(Boolean);
|
||||
const lines = applyOffsetAndLimit(allLines, offset, head_limit);
|
||||
let totalMatches = 0;
|
||||
let filesWithMatches = 0;
|
||||
for (const line of lines) {
|
||||
for (const line of allLines) {
|
||||
const parts = line.split(":");
|
||||
if (parts.length >= 2) {
|
||||
const lastPart = parts[parts.length - 1];
|
||||
@@ -138,16 +157,20 @@ export async function grep(args: GrepArgs): Promise<GrepResult> {
|
||||
if (!stdout || stdout.trim() === "")
|
||||
return { output: "No matches found", matches: 0 };
|
||||
|
||||
const allLines = stdout.split("\n");
|
||||
const lines = applyOffsetAndLimit(allLines, offset, head_limit);
|
||||
const content = lines.join("\n");
|
||||
|
||||
// Apply character limit to content output
|
||||
const { content: truncatedOutput } = truncateByChars(
|
||||
stdout,
|
||||
content,
|
||||
LIMITS.GREP_OUTPUT_CHARS,
|
||||
"Grep",
|
||||
);
|
||||
|
||||
return {
|
||||
output: truncatedOutput,
|
||||
matches: stdout.split("\n").filter(Boolean).length,
|
||||
matches: allLines.filter(Boolean).length,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -133,6 +133,11 @@ export async function read(args: ReadArgs): Promise<ReadResult> {
|
||||
if (await isBinaryFile(file_path))
|
||||
throw new Error(`Cannot read binary file: ${file_path}`);
|
||||
const content = await fs.readFile(file_path, "utf-8");
|
||||
if (content.trim() === "") {
|
||||
return {
|
||||
content: `<system-reminder>\nThe file ${file_path} exists but has empty contents.\n</system-reminder>`,
|
||||
};
|
||||
}
|
||||
const formattedContent = formatWithLineNumbers(content, offset, limit);
|
||||
return { content: formattedContent };
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,8 +3,7 @@ import { validateRequiredParams } from "./validation.js";
|
||||
interface TodoItem {
|
||||
content: string;
|
||||
status: "pending" | "in_progress" | "completed";
|
||||
id: string;
|
||||
priority?: "high" | "medium" | "low";
|
||||
activeForm: string;
|
||||
}
|
||||
interface TodoWriteArgs {
|
||||
todos: TodoItem[];
|
||||
@@ -29,10 +28,8 @@ export async function todo_write(
|
||||
throw new Error(
|
||||
"Each todo must have a valid status (pending, in_progress, or completed)",
|
||||
);
|
||||
if (!todo.id || typeof todo.id !== "string")
|
||||
throw new Error("Each todo must have an id string");
|
||||
if (todo.priority && !["high", "medium", "low"].includes(todo.priority))
|
||||
throw new Error("If provided, priority must be high, medium, or low");
|
||||
if (!todo.activeForm || typeof todo.activeForm !== "string")
|
||||
throw new Error("Each todo must have an activeForm string");
|
||||
}
|
||||
return {
|
||||
message:
|
||||
|
||||
Reference in New Issue
Block a user