can1357/oh-my-pi · error · ToolError

`ops[${index}].pat` must be a non-empty pattern

Error message

`ops[${index}].pat` must be a non-empty pattern

What it means

The ast_edit tool validates each op entry before executing: `pat` (the AST pattern to match) must be a non-empty string. An empty pattern would match nothing or be structurally invalid, so the tool fails fast with a ToolError naming the offending index.

Source

Thrown at packages/coding-agent/src/tools/ast-edit.ts:270

		},
	];
	readonly deferrable = true;
	readonly loadMode = "discoverable";
	constructor(private readonly session: ToolSession) {
		this.description = prompt.render(astEditDescription);
	}

	async execute(
		_toolCallId: string,
		params: AstEditSchemaInfer,
		signal?: AbortSignal,
		_onUpdate?: AgentToolUpdateCallback<AstEditToolDetails>,
		_context?: AgentToolContext,
	): Promise<AgentToolResult<AstEditToolDetails>> {
		return untilAborted(signal, async () => {
			const ops = params.ops.map((entry, index) => {
				if (entry.pat.length === 0) {
					throw new ToolError(`\`ops[${index}].pat\` must be a non-empty pattern`);
				}
				return [entry.pat, entry.out] as const;
			});
			if (ops.length === 0) {
				throw new ToolError("`ops` must include at least one op entry");
			}
			const seenPatterns = new Set<string>();
			for (const [pat] of ops) {
				if (seenPatterns.has(pat)) {
					throw new ToolError(`Duplicate rewrite pattern: ${pat}`);
				}
				seenPatterns.add(pat);
			}
			const normalizedRewrites = Object.fromEntries(ops);
			const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);

			const scope = await resolveToolSearchScope({
				rawPaths: params.paths,

View on GitHub (pinned to 9690622007)

Solutions

  1. Supply the actual AST pattern string for ops[i].pat.
  2. If patterns are generated dynamically, assert non-empty before invoking the tool.
  3. Check the tool schema (required non-empty pat) and fix the calling prompt/code.

Example fix

// before
await astEdit.execute({ path: 'a.ts', ops: [{ pat: '', out: 'foo()' }] });
// after
await astEdit.execute({ path: 'a.ts', ops: [{ pat: 'bar()', out: 'foo()' }] });
Defensive patterns

Strategy: validation

Validate before calling

ops.forEach((op, i) => {
  if (typeof op.pat !== 'string' || op.pat.length === 0) {
    throw new Error(`ops[${i}].pat must be a non-empty string`);
  }
});

Type guard

function isValidOp(op: unknown): op is { pat: string; out: string } {
  return typeof op === 'object' && op !== null &&
    typeof (op as { pat?: unknown }).pat === 'string' && (op as { pat: string }).pat.length > 0 &&
    typeof (op as { out?: unknown }).out === 'string';
}

Try / catch

try {
  await astEditTool.execute({ path, ops }, signal);
} catch (err) {
  if (err instanceof ToolError && err.message.includes('.pat` must be a non-empty')) {
    // fix ops and retry once
  }
}

Prevention

When it happens

Trigger: Calling ast_edit with an ops array entry where pat is "" — e.g. `{ ops: [{ pat: "", out: "newCode" }] }`, or a pat built by string interpolation from an empty variable.

Common situations: LLM-generated tool arguments with a placeholder pattern left blank; template code where the pattern variable was never populated; copy-paste dropping the pattern.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/006b48f8ae9bf0c8. Report an issue: GitHub.