Compare commits

...
Author SHA1 Message Date
abeatrix 671033856c system prompt 2026-06-24 15:59:16 -07:00
abeatrix 6672b0d463 turn off submit and exit 2026-06-24 14:00:08 -07:00
abeatrix c2485b12b4 Merge branch 'bee/handle-command-string' into bee/test-yolo 2026-06-24 13:49:10 -07:00
abeatrix d6a6498939 use apply patch for glm 2026-06-24 13:48:51 -07:00
abeatrix d270941bcd keeps union validation 2026-06-24 13:45:16 -07:00
abeatrix 2dc3a83201 test system prompt 2026-06-24 13:32:09 -07:00
abeatrix 86e8b61206 fix(tools): parse JSON-encoded command arrays in shell tools
Handle cases where the `commands` field is passed as a JSON-encoded
string array instead of a native array. Refactor input handling in
`createBashTool` and `createWindowsShellTool` to support both string
and structured command inputs, and update `coalesceSplitHeredocCommands`
to skip non-string entries. Removes the `RunCommandsInputUnionSchema`
in favor of direct validation.
2026-06-24 12:53:30 -07:00
7 changed files with 178 additions and 25 deletions
@@ -535,6 +535,111 @@ describe("default run_commands tool", () => {
);
});
it("accepts object input with commands as a JSON-encoded string array", async () => {
const execute = vi.fn(async (command: string | { command: string }) =>
typeof command === "string" ? `ran:${command}` : `ran:${command.command}`,
);
const tool = createBashTool(execute);
const command = "cd /repo && bunx tsc --noEmit --pretty 2>&1 | head -40";
const result = await tool.execute(
{ commands: JSON.stringify([command]) } as never,
{
agentId: "agent-1",
conversationId: "conv-1",
iteration: 1,
},
);
expect(result).toEqual([
{
query: command,
result: `ran:${command}`,
success: true,
},
]);
expect(execute).toHaveBeenCalledTimes(1);
expect(execute).toHaveBeenCalledWith(
command,
process.cwd(),
expect.objectContaining({
agentId: "agent-1",
conversationId: "conv-1",
iteration: 1,
}),
);
});
it("accepts JSON-encoded command arrays in the Windows shell tool", async () => {
const execute = vi.fn(async (command: string | { command: string }) =>
typeof command === "string" ? `ran:${command}` : `ran:${command.command}`,
);
const tool = createWindowsShellTool(execute);
const result = await tool.execute(
{ commands: JSON.stringify(["git status --short"]) } as never,
{
agentId: "agent-1",
conversationId: "conv-1",
iteration: 1,
},
);
expect(result).toEqual([
{
query: "git status --short",
result: "ran:git status --short",
success: true,
},
]);
expect(execute).toHaveBeenCalledWith(
"git status --short",
process.cwd(),
expect.objectContaining({
agentId: "agent-1",
conversationId: "conv-1",
iteration: 1,
}),
);
});
it("accepts nested JSON-encoded commands payloads", async () => {
const execute = vi.fn(async (command: string | { command: string }) =>
typeof command === "string" ? `ran:${command}` : `ran:${command.command}`,
);
const tool = createBashTool(execute);
const result = await tool.execute(
{
commands: JSON.stringify({
commands: JSON.stringify(["git status --short"]),
}),
} as never,
{
agentId: "agent-1",
conversationId: "conv-1",
iteration: 1,
},
);
expect(result).toEqual([
{
query: "git status --short",
result: "ran:git status --short",
success: true,
},
]);
expect(execute).toHaveBeenCalledWith(
"git status --short",
process.cwd(),
expect.objectContaining({
agentId: "agent-1",
conversationId: "conv-1",
iteration: 1,
}),
);
});
it("accepts common single-command aliases", async () => {
const execute = vi.fn(
async (command: string | { command: string }) =>
@@ -26,6 +26,7 @@ import {
formatRunCommandQueryPreview,
getEditorSizeError,
getReadFileRangeError,
normalizeJsonLikeRunCommandsInput,
normalizeRunCommandsInput,
TimeoutError,
withTimeout,
@@ -118,10 +119,21 @@ function getHeredocDelimiter(command: string): string | undefined {
return match?.[1] ?? match?.[2] ?? match?.[3];
}
function coalesceSplitHeredocCommands(commands: string[]): string[] {
const coalesced: string[] = [];
function coalesceSplitHeredocCommands(commands: string[]): string[];
function coalesceSplitHeredocCommands(
commands: Array<string | StructuredCommandInput>,
): Array<string | StructuredCommandInput>;
function coalesceSplitHeredocCommands(
commands: Array<string | StructuredCommandInput>,
): Array<string | StructuredCommandInput> {
const coalesced: Array<string | StructuredCommandInput> = [];
for (let index = 0; index < commands.length; index += 1) {
const command = commands[index];
if (typeof command !== "string") {
coalesced.push(command);
continue;
}
const delimiter = getHeredocDelimiter(command);
if (!delimiter) {
coalesced.push(command);
@@ -130,7 +142,9 @@ function coalesceSplitHeredocCommands(commands: string[]): string[] {
const endIndex = commands.findIndex(
(nextCommand, nextIndex) =>
nextIndex > index && nextCommand.trim() === delimiter,
nextIndex > index &&
typeof nextCommand === "string" &&
nextCommand.trim() === delimiter,
);
if (endIndex === -1) {
coalesced.push(command);
@@ -141,7 +155,9 @@ function coalesceSplitHeredocCommands(commands: string[]): string[] {
while (index < endIndex) {
index += 1;
const nextCommand = commands[index];
parts.push(nextCommand);
if (typeof nextCommand === "string") {
parts.push(nextCommand);
}
}
coalesced.push(parts.join("\n"));
}
@@ -326,17 +342,18 @@ export function createBashTool(
return createTool<RunCommandsInput, ToolOperationResult[]>({
name: "run_commands",
description:
"Run shell commands from the root of the workspace. " +
"Use for listing files, checking git status, running builds, executing tests, etc. " +
"Commands should be properly shell-escaped and targeted to avoid error or timeout. Include multiple commands in the same call when they are independent complete shell commands and safe to run concurrently; multiline scripts and heredocs must be a single command string. When independent reads, searches, or edits are also needed, call those tools in the same response. " +
`Output beyond ~${Math.round(MAX_COMMAND_OUTPUT_CHARS / 1000)}k characters is middle-truncated (start and end preserved); pipe through grep/head/tail when you need specific sections of large output. ` +
"For long-running commands, run them in background and redirect output to a tmp file that you can read from later.",
"Run shell commands from the root of user's workspace. " +
"Commands should be properly shell-escaped and targeted to avoid error or timeout (30s max). " +
"For long-running command, run it in background and redirect output to a tmp file that you can read from later.",
inputSchema: zodToJsonSchema(RunCommandsInputSchema),
timeoutMs: timeoutMs * 2,
retryable: false, // Shell commands often have side effects
maxRetries: 0,
execute: async (input, context) => {
const validate = validateWithZod(RunCommandsInputUnionSchema, input);
const validate = validateWithZod(
RunCommandsInputUnionSchema,
normalizeJsonLikeRunCommandsInput(input),
);
let commands: string[];
if (typeof validate === "string") {
commands = [validate];
@@ -77,10 +77,48 @@ export function getReadFileRangeError(request: ReadFileRequest): string | null {
return `start_line must be less than or equal to end_line (received start_line: ${start_line}, end_line: ${end_line})`;
}
function parseJsonLikeString(input: unknown): unknown {
if (typeof input !== "string") {
return input;
}
const trimmed = input.trim();
if (!trimmed.startsWith("[") && !trimmed.startsWith("{")) {
return input;
}
try {
return JSON.parse(trimmed) as unknown;
} catch {
return input;
}
}
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input != null && !Array.isArray(input);
}
export function normalizeJsonLikeRunCommandsInput(input: unknown): unknown {
const parsed = parseJsonLikeString(input);
if (!isRecord(parsed) || !("commands" in parsed)) {
return parsed;
}
const commands = parseJsonLikeString(parsed.commands);
if (isRecord(commands) && "commands" in commands) {
return { ...parsed, commands: parseJsonLikeString(commands.commands) };
}
return { ...parsed, commands };
}
export function normalizeRunCommandsInput(
input: unknown,
): Array<string | StructuredCommandInput> {
const validate = validateWithZod(StructuredCommandsInputUnionSchema, input);
const validate = validateWithZod(
StructuredCommandsInputUnionSchema,
normalizeJsonLikeRunCommandsInput(input),
);
if (typeof validate === "string") {
return [validate];
@@ -68,7 +68,7 @@ export const DEFAULT_MODEL_TOOL_ROUTING_RULES: ToolRoutingRule[] = [
{
name: "codex-and-gpt-use-apply-patch",
mode: "act",
modelIdIncludes: ["codex", "gpt"],
modelIdIncludes: ["codex", "gpt", "glm"],
enableTools: ["apply_patch"],
disableTools: ["editor"],
},
@@ -102,7 +102,7 @@ export const ToolPresets = {
enableEditor: true,
enableSkills: false,
enableAskQuestion: false,
enableSubmitAndExit: true,
enableSubmitAndExit: false,
enableSpawnAgent: false,
enableAgentTeams: false,
},
@@ -155,15 +155,11 @@ export const StructuredCommandsInputSchema = z.object({
* Union schema for run_commands tool input. More flexible.
*/
export const StructuredCommandsInputUnionSchema = z.union([
RunCommandsInputSchema,
StructuredCommandsInputSchema,
z.object({ commands: StructuredCommandEntrySchema }),
z.array(StructuredCommandInputSchema),
StructuredCommandInputSchema,
z.object({ command: CommandInputSchema }),
z.object({ cmd: CommandInputSchema }),
z.array(z.string()),
z.string(),
RunCommandsInputUnionSchema,
]);
/**
+4 -7
View File
@@ -19,8 +19,6 @@ Remember:
- Be explicit about any assumptions or limitations in your solution.
- Always show your planning process before executing any task. This will help ensure that you have a clear understanding of the requirements and that your approach aligns with the user's needs.
- Always use absolute paths when referring to files.
- You can call multiple tools in a single response. Before using tools, identify every independent read, search, command, or edit needed for the next step and emit all of those tool calls now, either as multiple tool calls or as one batched input for tools that accept arrays. Do not wait for one independent result before requesting another. Do not split independent reads, searches, checks, or edits across separate turns.
- Good parallelism examples: read all known relevant files in one read_files call; run independent inspection commands in one run_commands call; emit independent read_files, search_codebase, and run_commands calls together in one response; emit multiple editor calls together when editing different files or non-overlapping regions.
- Always verify the files you have edited or created at the end of the task to ensure they are completed and working as expected.
Begin by analyzing the user's input and gathering any necessary additional context. Then, present your plan at the start of your response along with tool calls before proceeding with the task. It's OK for this section to be quite long.
@@ -43,10 +41,7 @@ RULES:
- Always match output format exactly as shown in examples or existing files.
- Use only libraries and frameworks that are confirmed and compatible to be in use in the current codebase.
- Provide complete and functional code without omissions or placeholders.
- Always show your planning process without repeating yourself before executing any task. This will help ensure that you have a clear understanding of the requirements and that your approach aligns with the user's request.
- Always use absolute paths when referring to files.
- You can call multiple tools in a single response. Before using tools, identify every independent read, search, command, or edit needed for the next step and emit all of those tool calls now, either as multiple tool calls or as one batched input for tools that accept arrays. Do not wait for one independent result before requesting another. Do not split independent reads, searches, checks, or edits across separate turns.
- Good parallelism examples: read all known relevant files in one read_files call; run independent inspection commands in one run_commands call; emit independent read_files, search_codebase, and run_commands calls together in one response; emit multiple editor calls together when editing different files or non-overlapping regions.
- Always verify the files you have edited or created at the end of the task to ensure they are completed and working as expected.
Environment you are running in:
@@ -62,7 +57,9 @@ IMPORTANT:
- A correct fix means the underlying behavior is fixed — not just the symptoms addressed superficially.
- After applying your fix, you must run the relevant test suite to confirm your changes actually resolve the problem. If tests fail, analyze the failures, revise your fix, and re-run until tests pass.
- Do not consider the task complete until the test suite related to the files you have touched passes.
- Always includes tool calls in your response until the task is completed. You should only end the task when all the requirements are met by calling the 'submit_and_exit' tool.
- Response without the submit_and_exit tool call will considered not completed and the task will continue.
- Be concise with your output. You do not need to explain your thoughts or summarize your works. Just do it!
You have about 10 minutes to complete the task, so work efficiently but carefully.
{{CLINE_RULES}}
{{CLINE_METADATA}}`;