can1357/oh-my-pi · error · ToolError

${XD_URL_PREFIX}${device.name} content must be a JSON object

Error message

${XD_URL_PREFIX}${device.name} content must be a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}.

What it means

After JSON parsing succeeds, parseDeviceArgs requires the parsed value to be a plain JSON object (null, arrays, and primitives are rejected) because device arguments are keyed by parameter name. It throws this ToolError stating what was received instead (array, null, string, number, etc.).

Source

Thrown at packages/coding-agent/src/tools/xdev.ts:164

 * schema declares one. Throws ToolError; schema-mismatch errors carry `docs()`
 * for repair.
 */
function parseDeviceArgs(
	device: AiTool,
	content: string,
	toolCallId: string,
	docs: () => string,
): Record<string, unknown> {
	let parsed: unknown;
	try {
		parsed = JSON.parse(content);
	} catch (error) {
		throw new ToolError(
			`${XD_URL_PREFIX}${device.name} expects a JSON args object as content (${error instanceof Error ? error.message : String(error)}). Write \`?\` for docs.`,
		);
	}
	if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
		throw new ToolError(
			`${XD_URL_PREFIX}${device.name} content must be a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}.`,
		);
	}
	// The harness only injects the intent field into top-level schemas; strip a
	// habitual `i` from inner args unless the wrapped schema really declares it.
	const args: Record<string, unknown> = { ...(parsed as Record<string, unknown>) };
	if ("i" in args && !schemaDeclaresIntentField(toolWireSchema(device))) delete args.i;
	try {
		return validateToolArguments(device, {
			type: "toolCall",
			id: toolCallId,
			name: device.name,
			arguments: args,
		});
	} catch (error) {
		const message = error instanceof Error ? error.message : String(error);
		throw new ToolError(`Invalid args for ${XD_URL_PREFIX}${device.name}: ${message}\n\n${docs()}`);
	}

View on GitHub (pinned to 9690622007)

Solutions

  1. Wrap the payload in a JSON object with named fields matching the device schema: {"items": [...]}.
  2. Write `?` as content to `xd://<device>` to see the expected schema.
  3. Use the device's docs to find the object field that should hold a list, instead of sending a top-level array.

Example fix

// before
write({ path: "xd://search", content: "[\"query\"]" })
// after
write({ path: "xd://search", content: "{\"query\":\"...\"}" })
Defensive patterns

Strategy: type-guard

Validate before calling

const parsed = JSON.parse(content); if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("xd:// args must be a JSON object");

Type guard

function isJsonObject(v: unknown): v is Record<string, unknown> { return typeof v === "object" && v !== null && !Array.isArray(v); }

Try / catch

try { await write({ path, content }); } catch (e) { if (String(e.message).includes("must be a JSON object, got")) { const parsed = JSON.parse(content); await write({ path, content: JSON.stringify({ items: parsed }) }); } else throw e; }

Prevention

When it happens

Trigger: Writing `[]`, `[1,2]`, `"text"`, `42`, `true`, or `null` as content to `xd://<device>` — syntactically valid JSON but not an object.

Common situations: An agent sends a JSON array of items when the device schema expects named fields, or sends a bare JSON string/number it believed was already the payload.

Related errors


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