{"record":{"id":"c8f9c8e3617cd284","repo":"can1357/oh-my-pi","slug":"ops-must-include-at-least-one-op-entry","errorCode":null,"errorMessage":"`ops` must include at least one op entry","messagePattern":"`ops` must include at least one op entry","errorType":"validation","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/ast-edit.ts","lineNumber":275,"sourceCode":"\t\tthis.description = prompt.render(astEditDescription);\n\t}\n\n\tasync execute(\n\t\t_toolCallId: string,\n\t\tparams: AstEditSchemaInfer,\n\t\tsignal?: AbortSignal,\n\t\t_onUpdate?: AgentToolUpdateCallback<AstEditToolDetails>,\n\t\t_context?: AgentToolContext,\n\t): Promise<AgentToolResult<AstEditToolDetails>> {\n\t\treturn untilAborted(signal, async () => {\n\t\t\tconst ops = params.ops.map((entry, index) => {\n\t\t\t\tif (entry.pat.length === 0) {\n\t\t\t\t\tthrow new ToolError(`\\`ops[${index}].pat\\` must be a non-empty pattern`);\n\t\t\t\t}\n\t\t\t\treturn [entry.pat, entry.out] as const;\n\t\t\t});\n\t\t\tif (ops.length === 0) {\n\t\t\t\tthrow new ToolError(\"`ops` must include at least one op entry\");\n\t\t\t}\n\t\t\tconst seenPatterns = new Set<string>();\n\t\t\tfor (const [pat] of ops) {\n\t\t\t\tif (seenPatterns.has(pat)) {\n\t\t\t\t\tthrow new ToolError(`Duplicate rewrite pattern: ${pat}`);\n\t\t\t\t}\n\t\t\t\tseenPatterns.add(pat);\n\t\t\t}\n\t\t\tconst normalizedRewrites = Object.fromEntries(ops);\n\t\t\tconst maxFiles = $envpos(\"PI_MAX_AST_FILES\", 1000);\n\n\t\t\tconst scope = await resolveToolSearchScope({\n\t\t\t\trawPaths: params.paths,\n\t\t\t\tcwd: this.session.cwd,\n\t\t\t\tinternalUrlAction: \"rewrite\",\n\t\t\t\tsettings: this.session.settings,\n\t\t\t\tsignal,\n\t\t\t\tsessionFile: this.session.getSessionFile() ?? undefined,","sourceCodeStart":257,"sourceCodeEnd":293,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/ast-edit.ts#L257-L293","documentation":"ast_edit requires at least one rewrite op. After mapping (and possibly filtering) params.ops into the ops tuples, an empty array means the call would do nothing, so the tool throws ToolError instead of performing a no-op run.","triggerScenarios":"Calling ast_edit with `ops: []`, or with an ops value that maps to an empty list (e.g. empty params.ops from defaulted/omitted arguments).","commonSituations":"Generated tool calls where the ops list was omitted or emptied by preprocessing; a caller that conditionally builds ops and invokes the tool even when no rewrites are needed.","solutions":["Include at least one { pat, out } entry in ops.","Skip the ast_edit call entirely in the caller when no rewrites are needed instead of sending an empty list.","Fix argument construction so ops is not dropped/emptied before dispatch."],"exampleFix":"// before\nif (ops.length >= 0) await astEdit.execute({ path, ops });\n// after\nif (ops.length > 0) await astEdit.execute({ path, ops });","handlingStrategy":"validation","validationCode":"if (!Array.isArray(ops) || ops.length === 0) {\n  throw new Error('ast_edit requires at least one op');\n}","typeGuard":"function hasOps(p: { ops?: unknown }): p is { ops: unknown[] } {\n  return Array.isArray(p.ops) && p.ops.length > 0;\n}","tryCatchPattern":null,"preventionTips":["Guard the call site: only invoke ast_edit when ops.length > 0.","When ops are built conditionally, skip the tool call entirely instead of sending an empty array.","Enforce minItems: 1 in the tool schema for callers that validate JSON schema."],"tags":["validation","tool-arguments","empty-array"],"backgroundTag":"empty-array-argument","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}